@stitchdb/cli 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +129 -36
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -420,6 +420,11 @@ async function cmdHook(args) {
420
420
  const cfg = loadConfig();
421
421
  if (!cfg.apiKey)
422
422
  return; // not logged in, silently skip
423
+ // When the Stitch CLI spawns its own `claude -p` (e.g. for distillation),
424
+ // we don't want THAT inner conversation logged as a user/assistant turn —
425
+ // it would pollute the project thread with the distill prompt and JSON.
426
+ if (process.env.STITCH_HOOKS_DISABLED === '1')
427
+ return;
423
428
  let raw = '';
424
429
  try {
425
430
  raw = await readStdinAll();
@@ -646,29 +651,24 @@ function saveDistillState(s) {
646
651
  fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
647
652
  fs.writeFileSync(DISTILL_STATE_FILE, JSON.stringify(s, null, 2));
648
653
  }
649
- const DISTILL_PROMPT = `You are a memory distiller for an AI coding assistant.
654
+ const DISTILL_PROMPT = `You are a memory distiller. Output ONLY a JSON array of memory objects. Be GENEROUS — capture every concrete fact about the project that future sessions will care about. Aim for 10–30 memories on a substantial conversation.
655
+
656
+ Each memory is one atomic, self-contained statement (1–4 sentences) that someone reading it months later — without the conversation around it — will fully understand.
650
657
 
651
- Read the conversation below (between a developer and an AI). Extract ONLY
652
- durable, project-specific facts the developer explicitly stated or decisions
653
- they explicitly made. Do not invent. Do not extrapolate from implications.
654
- Skip anything that's tentative, exploratory, or not directly stated.
658
+ Format:
659
+ [{"kind":"fact|decision|snippet|preference","content":"...","tags":["short","keywords"]}, ...]
655
660
 
656
- Output a JSON array. Each entry:
657
- {
658
- "kind": "fact" | "decision" | "snippet" | "preference",
659
- "content": "<self-contained, single-paragraph statement>",
660
- "tags": ["<short-keyword>", ...]
661
- }
661
+ Capture:
662
+ - fact — anything concrete: file paths, endpoint URLs, commands, version
663
+ numbers, architecture, deployed services, schemas, dependencies,
664
+ bugs found, fixes shipped.
665
+ - decision choices with rationale.
666
+ - snippet — reusable commands/config/code/CLI flags/env var names.
667
+ - preference — how the developer wants the AI to behave on this project.
662
668
 
663
- Rules:
664
- - "fact" : stable knowledge about this project (architecture, services, schema)
665
- - "decision" : explicit choice with rationale (e.g. "we use X because Y")
666
- - "snippet" : a reusable code/config/command pattern the user wants kept
667
- - "preference" : how the developer wants the AI to behave on this project
668
- - Each entry must stand alone — readable months later without context.
669
- - Tags should be 1–3 short keywords.
670
- - If nothing durable was said, output exactly: []
671
- - Output ONLY the JSON array. No prose, no code fences, no commentary.
669
+ Skip pleasantries and questions still being explored. One atomic idea per
670
+ memory don't bundle. If truly nothing durable was discussed, output: [].
671
+ Output the JSON array and nothing else no prose, no markdown fences.
672
672
 
673
673
  Conversation:
674
674
  `;
@@ -701,7 +701,29 @@ async function cmdDistill(args) {
701
701
  }
702
702
  const claudeBin = process.env.STITCH_CLAUDE_BIN || 'claude';
703
703
  process.stdout.write(` → asking ${claudeBin} -p to extract facts… `);
704
- const result = await run(claudeBin, ['-p', DISTILL_PROMPT + conversation], { cwd: process.cwd() });
704
+ const fullPrompt = DISTILL_PROMPT + conversation;
705
+ // Pipe the prompt via stdin: long conversations blow past ARG_MAX when
706
+ // passed as `-p <prompt>`, and stdin avoids claude interpreting any
707
+ // prompt-internal characters as flags. `claude -p` with no value reads
708
+ // from stdin. STITCH_HOOKS_DISABLED stops our own _hook command from
709
+ // logging this nested distill conversation as project thread turns.
710
+ const result = await runWithStdin(claudeBin, ['-p'], fullPrompt, {
711
+ cwd: process.cwd(),
712
+ env: { STITCH_HOOKS_DISABLED: '1' },
713
+ });
714
+ // Debug capture: STITCH_DEBUG_DISTILL=1 writes raw stdout/stderr/prompt
715
+ // to /tmp so we can see exactly what claude actually returned.
716
+ if (process.env.STITCH_DEBUG_DISTILL === '1') {
717
+ const ts = Date.now();
718
+ const dbgDir = '/tmp';
719
+ try {
720
+ fs.writeFileSync(path.join(dbgDir, `stitch-distill-${ts}.prompt.txt`), fullPrompt);
721
+ fs.writeFileSync(path.join(dbgDir, `stitch-distill-${ts}.stdout.txt`), result.stdout);
722
+ fs.writeFileSync(path.join(dbgDir, `stitch-distill-${ts}.stderr.txt`), result.stderr);
723
+ console.log(`\n [debug] wrote /tmp/stitch-distill-${ts}.{prompt,stdout,stderr}.txt`);
724
+ }
725
+ catch { /* ignore */ }
726
+ }
705
727
  if (result.exit_code !== 0) {
706
728
  console.log('failed');
707
729
  console.error(result.stderr.trim().slice(0, 400));
@@ -710,6 +732,10 @@ async function cmdDistill(args) {
710
732
  const memories = parseDistillationOutput(result.stdout);
711
733
  console.log(`extracted ${memories.length} memories`);
712
734
  if (memories.length === 0) {
735
+ if (process.env.STITCH_DEBUG_DISTILL === '1') {
736
+ console.error(' [debug] claude stdout (first 600 chars):');
737
+ console.error(' ' + result.stdout.slice(0, 600).replace(/\n/g, '\n '));
738
+ }
713
739
  bumpDistillCooldown(thread);
714
740
  return;
715
741
  }
@@ -739,25 +765,76 @@ function bumpDistillCooldown(thread) {
739
765
  saveDistillState(state);
740
766
  }
741
767
  function parseDistillationOutput(stdout) {
742
- // claude -p sometimes wraps in markdown fences or adds a "Here are the memories:" preamble.
743
- // Strip everything outside the first balanced [..] array.
768
+ // claude -p often wraps the array in markdown fences, prepends prose like
769
+ // "Here are the extracted memories:", or follows it with a sign-off line.
770
+ // Strategy: try several extraction modes from most-precise to most-lenient,
771
+ // returning the first one that parses to a non-empty valid array.
744
772
  const text = stdout.trim();
745
- const start = text.indexOf('[');
746
- const end = text.lastIndexOf(']');
747
- if (start === -1 || end === -1 || end < start)
773
+ if (!text)
748
774
  return [];
749
- const json = text.slice(start, end + 1);
750
- try {
751
- const parsed = JSON.parse(json);
752
- if (!Array.isArray(parsed))
753
- return [];
754
- return parsed.filter((m) => m && typeof m === 'object'
755
- && typeof m.content === 'string' && m.content.length > 0
756
- && typeof m.kind === 'string' && ['fact', 'decision', 'snippet', 'preference', 'note'].includes(m.kind));
775
+ const candidates = [];
776
+ // 1. Fenced ```json block
777
+ const fenced = text.match(/```(?:json)?\s*\n([\s\S]*?)\n```/i);
778
+ if (fenced)
779
+ candidates.push(fenced[1].trim());
780
+ // 2. The first `[ {` last `} ]` window (skips prose-level stray brackets).
781
+ const objStart = text.indexOf('[');
782
+ const objEnd = text.lastIndexOf(']');
783
+ if (objStart !== -1 && objEnd > objStart) {
784
+ candidates.push(text.slice(objStart, objEnd + 1));
785
+ }
786
+ // 3. Largest `[\n{ ... }\n]` block via balanced scan from each `[`
787
+ for (let i = 0; i < text.length; i++) {
788
+ if (text[i] !== '[')
789
+ continue;
790
+ let depth = 0;
791
+ let inStr = false;
792
+ let esc = false;
793
+ for (let j = i; j < text.length; j++) {
794
+ const ch = text[j];
795
+ if (esc) {
796
+ esc = false;
797
+ continue;
798
+ }
799
+ if (ch === '\\' && inStr) {
800
+ esc = true;
801
+ continue;
802
+ }
803
+ if (ch === '"') {
804
+ inStr = !inStr;
805
+ continue;
806
+ }
807
+ if (inStr)
808
+ continue;
809
+ if (ch === '[')
810
+ depth++;
811
+ else if (ch === ']') {
812
+ depth--;
813
+ if (depth === 0) {
814
+ candidates.push(text.slice(i, j + 1));
815
+ break;
816
+ }
817
+ }
818
+ }
757
819
  }
758
- catch {
759
- return [];
820
+ const seen = new Set();
821
+ for (const cand of candidates) {
822
+ if (seen.has(cand))
823
+ continue;
824
+ seen.add(cand);
825
+ try {
826
+ const parsed = JSON.parse(cand);
827
+ if (!Array.isArray(parsed))
828
+ continue;
829
+ const valid = parsed.filter((m) => m && typeof m === 'object'
830
+ && typeof m.content === 'string' && m.content.length > 0
831
+ && typeof m.kind === 'string' && ['fact', 'decision', 'snippet', 'preference', 'note'].includes(m.kind));
832
+ if (valid.length > 0)
833
+ return valid;
834
+ }
835
+ catch { /* try next candidate */ }
760
836
  }
837
+ return [];
761
838
  }
762
839
  async function distillReview(args) {
763
840
  const cfg = loadConfig();
@@ -1387,6 +1464,22 @@ function run(cmd, args, opts = {}) {
1387
1464
  child.on('close', (code) => resolve({ stdout, stderr, exit_code: code ?? 0 }));
1388
1465
  });
1389
1466
  }
1467
+ function runWithStdin(cmd, args, stdinData, opts = {}) {
1468
+ return new Promise((resolve) => {
1469
+ const child = spawn(cmd, args, {
1470
+ cwd: opts.cwd || process.cwd(),
1471
+ env: { ...process.env, ...(opts.env || {}) },
1472
+ stdio: ['pipe', 'pipe', 'pipe'],
1473
+ });
1474
+ let stdout = '';
1475
+ let stderr = '';
1476
+ child.stdout.on('data', (d) => stdout += d.toString());
1477
+ child.stderr.on('data', (d) => stderr += d.toString());
1478
+ child.on('error', (err) => resolve({ stdout, stderr: stderr + '\n[spawn error] ' + err.message, exit_code: 127 }));
1479
+ child.on('close', (code) => resolve({ stdout, stderr, exit_code: code ?? 0 }));
1480
+ child.stdin.end(stdinData);
1481
+ });
1482
+ }
1390
1483
  function readLine() {
1391
1484
  return new Promise((resolve) => {
1392
1485
  const onData = (d) => { process.stdin.off('data', onData); resolve(d.toString().trim()); };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stitchdb/cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Stitch CLI — manage memory + run agents from your terminal",
5
5
  "type": "module",
6
6
  "bin": {