JuliusBrussee/caveman · error · Error

duplicate

Error message

duplicate ${arg}

What it means

parseStatsOptions rejects repeating any argument on the command line, tracking every flag in a Set. Passing the same flag twice (even a value flag with different values) is treated as a likely mistake and throws rather than silently taking the last occurrence.

Solutions

  1. Remove the duplicated flag, keeping a single occurrence
  2. If the duplicate comes from a wrapper script, deduplicate flags before invoking the CLI (or pass the final value only)
  3. Check the exact CLI usage printed by the error: it echoes the duplicated arg name

Example fix

// before
myapp stats --days 7 --days 30
// after
myapp stats --days 30
Defensive patterns

Strategy: validation

Validate before calling

const flags = argv.filter(a => a.startsWith("--"));
const dupes = flags.filter((f, i) => flags.indexOf(f) !== i);
if (dupes.length) throw new Error(`duplicate ${dupes[0]}`);

Try / catch

try {
  const opts = parseStatsOptions(argv);
} catch (e) {
  if (String(e.message).startsWith("duplicate ")) {
    console.error(e.message); process.exitCode = 2;
  } else throw e;
}

Prevention

When it happens

Trigger: Running the stats CLI with the same flag twice, e.g. `--days 7 --days 30`, `--model a --model b`, or repeating boolean flags like `--json --json` (including via shell alias or script that appends flags).

Common situations: Wrapper scripts that append their own flags onto a user-provided string that already contains them; copy-paste errors; composability where two config layers both add --provider.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/870db6a8e5de6fa7. Report an issue: GitHub.

Appendix: source

Thrown at packages/cli/src/stats-cli.ts:38

API cost estimates and subscription API equivalents are separate. Neither
proves an invoice reduction. Unknown models and billing modes stay unpriced.
`;

export type StatsCLIOptions = {
  json: boolean;
  plain: boolean;
  open: boolean;
  out?: string;
  filters: string[];
};

export function parseStatsOptions(argv: string[]): StatsCLIOptions {
  const result: StatsCLIOptions = { json: false, plain: false, open: false, filters: [] };
  const seen = new Set<string>();
  const valueFlags = new Set(["--days", "--provider", "--model", "--agent", "--auth", "--out"]);
  for (let i = 0; i < argv.length; i++) {
    const arg = argv[i]!;
    if (seen.has(arg)) throw new Error(`duplicate ${arg}`);
    seen.add(arg);
    if (valueFlags.has(arg)) {
      const value = argv[++i];
      if (!value || value.startsWith("--") || /[\u0000-\u001f\u007f]/u.test(value)) throw new Error(`${arg} requires a value`);
      if (arg === "--days" && (!/^\d+$/u.test(value) || Number(value) < 1 || Number(value) > 3660)) {
        throw new Error("--days must be an integer from 1 to 3660");
      }
      if (arg === "--out") result.out = value;
      else result.filters.push(arg, value);
    } else if (arg === "--json") result.json = true;
    else if (arg === "--plain" || arg === "--no-open") result.plain = true;
    else if (arg === "--open") result.open = true;
    else if (arg === "--all-time") result.filters.push("--days", "0");
    else throw new Error(`unknown option ${arg}`);
  }
  if (seen.has("--days") && seen.has("--all-time")) throw new Error("choose --days or --all-time");
  if (result.plain && result.open) throw new Error("choose --plain or --open");
  if (result.json && result.open) throw new Error("--json cannot open a browser");

View on GitHub (pinned to 3ee70a1026)