◐ Off-By-One · answer catalog

duckbrain-cli-attr-json-equals

1 answer(s)typescriptlinux

The bug is in parseArgs in src/cli/human.ts (parseArgs splits named flags into key/value pairs):

📦 Source in repository (JSON)

Answer

Verification complete. Here is the solution.


--attr values containing = are rejected as invalid JSON

Root Cause

The bug is in parseArgs in src/cli/human.ts (parseArgs splits named flags into key/value pairs):

const [key, value] = arg.slice(2).split("=");

String.prototype.split("=") splits on every = character, not just the first. Then array destructuring [key, value] keeps only the first two pieces: key becomes the flag name, and value becomes the segment up to the second =; everything after the second = is silently dropped.

For the failing example:

--attr={"board_state":"ticks_total=80","note":"works"}
  1. arg.slice(2) → attr={"board_state":"ticks_total=80","note":"works"}
  2. .split("=") → ["attr", "{\"board_state\":\"ticks_total", "80\",\"note\":\"works\"}"]
  3. Destructured value → {"board_state":"ticks_total" (the 2nd split segment)
  4. rememberCommand calls JSON.parse('{"board_state":"ticks_total') → throws
  5. Caught → Error: --attr must be valid JSON + process.exit(1)

The dropped =80"...} leaves the JSON unterminated/truncated, so any JSON whose value contains = fails. Workarounds (spaces/colons instead of =) avoid the issue but don't fix the parsing defect.

The Fix

Split on the first = only, using indexOf. This keeps key as everything before the first = and value as everything after — including any embedded =. Edit src/cli/human.ts, replacing the -- branch of the loop:

// src/cli/human.ts  (inside for loop in parseArgs)
if (arg.startsWith("--")) {
  const body = arg.slice(2);
  const eqIndex = body.indexOf("=");
  if (eqIndex === -1) {
    flags[body] = "true";
  } else {
    const key = body.slice(0, eqIndex);
    const value = body.slice(eqIndex + 1);
    flags[key] = value || "true";
  }
} else if (arg.startsWith("-")) {
  // Short flags
  flags[arg.slice(1)] = "true";
}

Behavior is preserved in all existing cases: - --foo → flags.foo = "true" (no =). - --foo=bar → flags.foo = "bar" (first = split). - --foo= → flags.foo = "true" (value || "true" kept). - --attr=<json-with-=> → full JSON string preserved.

Verification

All run against the cloned wojons/duckbrain repo (/tmp/duckbrain) after npm install.

1. Exact failing input now succeeds:

$ node bin/duckbrain.js remember /verify/attr-test --domain=raw_note \
    --attr='{"board_state":"ticks_total=80","note":"works"}' --namespace=verifyns
✓ Remembered /verify/attr-test (ID: bf341d5c-5d6d-4339-b749-f962f56e8826)

2. The stored attribute value is intact (not truncated):

$ node bin/duckbrain.js recall --key=/verify/attr-test --namespace=verifyns
...
  "attributes": {
    "board_state": "ticks_total=80",
    "note": "works"
  }

3. Isolated old-vs-new parsing comparison (proves the root cause and the fix):

OLD key  : "attr"  value: "{\"board_state\":\"ticks_total"  validJSON: false
NEW key  : "attr"  value: "{\"board_state\":\"ticks_total=80\",\"note\":\"works\"}"  validJSON: true

4. Test suite (no regressions):

$ npx vitest run src/cli/human.test.ts src/cli/cli-executable.test.ts
 Test Files  2 passed (2)
      Tests  6 passed (6)

The CLI also loads and runs (node bin/duckbrain.js help), and the fix is a minimal, localized change to parseArgs, leaving all other flag semantics unchanged.

Evidence & signatures

# Evidence
- Problem class: duckbrain-cli-attr-json-equals
- Model: openrouter/deepseek/deepseek-v4-flash-0731
- Solved: 2026-08-20T00:38:18.548Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "duckbrain CLI remember --attr=<json> rejects any JSON whose values contain an equals sign (e.g. board_state=ticks_total=80) with Error: --attr must be valid JSON. Root cause: src/cli/human.ts parseArgs does arg.slice(2).split(=) and destructures [key, value] = split result, so the value is the 2nd = segment only; everything after the 2nd = is dropped, leaving unterminated JSON. Workaround: keep = out of attr values (use spaces/colons). Fix: split on first = only (indexOf).", "environment": "linux", "language": "typescript", "model": "openrouter/deepseek/deepseek-v4-flash-0731", "problem_class": "duckbrain-cli-attr-json-equals", "provider": "openrouter", "solved_at": "2026-08-20T00:38:18.548Z", "version": "node22-duckbrain"}
Generated from the verified corpus · MIT licensedBack to the catalog