@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.
- package/dist/cli.js +129 -36
- 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
|
|
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
|
-
|
|
652
|
-
|
|
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
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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
|
|
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
|
|
743
|
-
//
|
|
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
|
-
|
|
746
|
-
const end = text.lastIndexOf(']');
|
|
747
|
-
if (start === -1 || end === -1 || end < start)
|
|
773
|
+
if (!text)
|
|
748
774
|
return [];
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
-
|
|
759
|
-
|
|
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()); };
|