@stitchdb/cli 0.4.0 → 0.6.0
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 +350 -51
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -339,36 +339,8 @@ async function cmdAgent(args) {
|
|
|
339
339
|
}
|
|
340
340
|
// ── Threads — append / recall / current ───────────────────────────────────
|
|
341
341
|
function inferThread() {
|
|
342
|
-
//
|
|
343
|
-
|
|
344
|
-
try {
|
|
345
|
-
const dir = process.cwd();
|
|
346
|
-
let cur = dir;
|
|
347
|
-
for (let i = 0; i < 8; i++) {
|
|
348
|
-
if (fs.existsSync(path.join(cur, '.git'))) {
|
|
349
|
-
const parent = path.dirname(cur);
|
|
350
|
-
const repoName = path.basename(cur);
|
|
351
|
-
let branch = 'main';
|
|
352
|
-
try {
|
|
353
|
-
const head = fs.readFileSync(path.join(cur, '.git', 'HEAD'), 'utf8').trim();
|
|
354
|
-
const m = head.match(/^ref:\s+refs\/heads\/(.+)$/);
|
|
355
|
-
if (m)
|
|
356
|
-
branch = m[1];
|
|
357
|
-
}
|
|
358
|
-
catch { /* detached */ }
|
|
359
|
-
return `${repoName}/${branch}`;
|
|
360
|
-
void parent;
|
|
361
|
-
}
|
|
362
|
-
const next = path.dirname(cur);
|
|
363
|
-
if (next === cur)
|
|
364
|
-
break;
|
|
365
|
-
cur = next;
|
|
366
|
-
}
|
|
367
|
-
return path.basename(dir);
|
|
368
|
-
}
|
|
369
|
-
catch {
|
|
370
|
-
return 'default';
|
|
371
|
-
}
|
|
342
|
+
// Single source of truth: `.stitch/project.json` if present, else cwd basename.
|
|
343
|
+
return inferThreadFor(process.cwd());
|
|
372
344
|
}
|
|
373
345
|
async function cmdThread(args) {
|
|
374
346
|
const cfg = loadConfig();
|
|
@@ -455,18 +427,62 @@ async function cmdHook(args) {
|
|
|
455
427
|
catch {
|
|
456
428
|
return;
|
|
457
429
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
catch {
|
|
465
|
-
return;
|
|
430
|
+
let event = {};
|
|
431
|
+
if (raw) {
|
|
432
|
+
try {
|
|
433
|
+
event = JSON.parse(raw);
|
|
434
|
+
}
|
|
435
|
+
catch { /* keep empty */ }
|
|
466
436
|
}
|
|
437
|
+
// hook_event_name may be missing for SessionStart on some Claude Code
|
|
438
|
+
// versions. Allow the caller to pass it as the first positional arg too.
|
|
467
439
|
const eventName = event?.hook_event_name || args[0] || '';
|
|
468
|
-
|
|
440
|
+
// SessionStart's payload doesn't include cwd — fall back to process.cwd()
|
|
441
|
+
// (which is the directory `claude` was launched from).
|
|
442
|
+
const cwd = event?.cwd || process.cwd();
|
|
469
443
|
const threadName = inferThreadFor(cwd) || 'default';
|
|
444
|
+
// ── SessionStart: inject prior context (token-efficient) ─────────────
|
|
445
|
+
// Strategy: prefer distilled memories (dense facts) over raw turns. Only
|
|
446
|
+
// include raw turns for the last 5 to give the agent immediate continuation.
|
|
447
|
+
if (eventName === 'SessionStart') {
|
|
448
|
+
try {
|
|
449
|
+
const stitch = client(cfg);
|
|
450
|
+
const projectTag = (threadName.split('/')[0] || threadName).toLowerCase();
|
|
451
|
+
const [thread, memHits] = await Promise.all([
|
|
452
|
+
stitch.thread(threadName).recall({ last: 5 }).catch(() => ({ thread_id: '', recent: [], semantic: [] })),
|
|
453
|
+
stitch.recall(projectTag, { k: 8 }).catch(() => []),
|
|
454
|
+
]);
|
|
455
|
+
const lines = [];
|
|
456
|
+
lines.push('<stitch-context>');
|
|
457
|
+
lines.push(`Project: ${threadName} · Stitch MCP server is available with tools: recall, remember, thread_recall, thread_append.`);
|
|
458
|
+
lines.push('');
|
|
459
|
+
if (Array.isArray(memHits) && memHits.length > 0) {
|
|
460
|
+
// Prefer distilled (auto-tagged) facts and decisions; fall back to anything else
|
|
461
|
+
const sortedMems = [...memHits].sort((a, b) => Number(b.score ?? 0) - Number(a.score ?? 0)).slice(0, 8);
|
|
462
|
+
lines.push('### Durable memories for this project');
|
|
463
|
+
for (const m of sortedMems) {
|
|
464
|
+
const isAuto = Array.isArray(m.tags) && m.tags.includes('auto');
|
|
465
|
+
const txt = String(m.content || '').replace(/\n+/g, ' ').slice(0, 350);
|
|
466
|
+
lines.push(`- **[${m.kind}${isAuto ? '·auto' : ''}]** ${txt}`);
|
|
467
|
+
}
|
|
468
|
+
lines.push('');
|
|
469
|
+
}
|
|
470
|
+
if (thread.recent && thread.recent.length > 0) {
|
|
471
|
+
lines.push('### Most recent turns (continue from here)');
|
|
472
|
+
for (const t of thread.recent.slice(-5)) {
|
|
473
|
+
const txt = String(t.content || '').replace(/\n+/g, ' ').slice(0, 300);
|
|
474
|
+
lines.push(`- **${t.role}**: ${txt}`);
|
|
475
|
+
}
|
|
476
|
+
lines.push('');
|
|
477
|
+
}
|
|
478
|
+
lines.push('Call `recall` for deeper search or `thread_recall` for older turns when needed.');
|
|
479
|
+
lines.push('</stitch-context>');
|
|
480
|
+
process.stdout.write(lines.join('\n'));
|
|
481
|
+
}
|
|
482
|
+
catch { /* silent — never break a session start */ }
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
// ── UserPromptSubmit / Stop: log a turn ───────────────────────────────
|
|
470
486
|
let role = null;
|
|
471
487
|
let content = '';
|
|
472
488
|
if (eventName === 'UserPromptSubmit') {
|
|
@@ -490,29 +506,55 @@ async function cmdHook(args) {
|
|
|
490
506
|
catch {
|
|
491
507
|
/* silent */
|
|
492
508
|
}
|
|
509
|
+
// After Stop, opportunistically kick off a distillation pass in the
|
|
510
|
+
// background (fire-and-forget). Won't block the session; debouncing
|
|
511
|
+
// (cooldown + min-new-turns) is enforced inside maybeAutoDistill.
|
|
512
|
+
if (eventName === 'Stop') {
|
|
513
|
+
maybeAutoDistill(threadName).catch(() => { });
|
|
514
|
+
}
|
|
493
515
|
}
|
|
516
|
+
/**
|
|
517
|
+
* Derive a thread name for the project at `cwd`. Strategy:
|
|
518
|
+
*
|
|
519
|
+
* 1. Walk up looking for a `.stitch/project.json` containing { "thread": "x" }
|
|
520
|
+
* — this is the authoritative cross-machine pin. Stays exactly the same
|
|
521
|
+
* on every machine that has the file. Created by `stitch link`.
|
|
522
|
+
* 2. Fall back to the folder basename (which already works fine if the user
|
|
523
|
+
* keeps the same folder name across their machines).
|
|
524
|
+
*
|
|
525
|
+
* Deliberately does NOT require git — works for any project structure.
|
|
526
|
+
*/
|
|
494
527
|
function inferThreadFor(cwd) {
|
|
495
528
|
const dir = cwd && fs.existsSync(cwd) ? cwd : process.cwd();
|
|
496
529
|
let cur = dir;
|
|
497
530
|
for (let i = 0; i < 8; i++) {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
let branch = 'main';
|
|
531
|
+
const projectFile = path.join(cur, '.stitch', 'project.json');
|
|
532
|
+
if (fs.existsSync(projectFile)) {
|
|
501
533
|
try {
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
branch = m[1];
|
|
534
|
+
const cfg = JSON.parse(fs.readFileSync(projectFile, 'utf8'));
|
|
535
|
+
if (cfg.thread && typeof cfg.thread === 'string')
|
|
536
|
+
return cfg.thread;
|
|
506
537
|
}
|
|
507
|
-
catch { /*
|
|
508
|
-
return `${repoName}/${branch}`;
|
|
538
|
+
catch { /* malformed — fall through */ }
|
|
509
539
|
}
|
|
510
540
|
const next = path.dirname(cur);
|
|
511
541
|
if (next === cur)
|
|
512
542
|
break;
|
|
513
543
|
cur = next;
|
|
514
544
|
}
|
|
515
|
-
return path.basename(dir);
|
|
545
|
+
return path.basename(dir) || 'default';
|
|
546
|
+
}
|
|
547
|
+
function findProjectRoot(cwd) {
|
|
548
|
+
let cur = cwd;
|
|
549
|
+
for (let i = 0; i < 8; i++) {
|
|
550
|
+
if (fs.existsSync(path.join(cur, '.stitch', 'project.json')))
|
|
551
|
+
return cur;
|
|
552
|
+
const next = path.dirname(cur);
|
|
553
|
+
if (next === cur)
|
|
554
|
+
break;
|
|
555
|
+
cur = next;
|
|
556
|
+
}
|
|
557
|
+
return null;
|
|
516
558
|
}
|
|
517
559
|
function lastAssistantTextFromTranscript(transcriptPath) {
|
|
518
560
|
if (!transcriptPath || !fs.existsSync(transcriptPath))
|
|
@@ -557,6 +599,238 @@ function lastAssistantTextFromTranscript(transcriptPath) {
|
|
|
557
599
|
catch { /* ignore */ }
|
|
558
600
|
return text;
|
|
559
601
|
}
|
|
602
|
+
// ── stitch link — pin a project's thread name so it's the same on every machine
|
|
603
|
+
async function cmdLink(args) {
|
|
604
|
+
const positionals = positional(args);
|
|
605
|
+
const explicit = positionals[0];
|
|
606
|
+
const name = (explicit || path.basename(process.cwd()) || 'default').trim();
|
|
607
|
+
const dir = path.join(process.cwd(), '.stitch');
|
|
608
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
609
|
+
const file = path.join(dir, 'project.json');
|
|
610
|
+
let cfg = {};
|
|
611
|
+
try {
|
|
612
|
+
cfg = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
613
|
+
}
|
|
614
|
+
catch { }
|
|
615
|
+
cfg.thread = name;
|
|
616
|
+
cfg.linked_at = new Date().toISOString();
|
|
617
|
+
fs.writeFileSync(file, JSON.stringify(cfg, null, 2));
|
|
618
|
+
console.log(`Project pinned to thread "${name}"`);
|
|
619
|
+
console.log(` → ${file}`);
|
|
620
|
+
console.log();
|
|
621
|
+
console.log('Commit `.stitch/project.json` to your repo (or sync the file across');
|
|
622
|
+
console.log('machines) so every machine pinning this project uses the same thread.');
|
|
623
|
+
}
|
|
624
|
+
// ── stitch distill — extract durable facts from recent conversation ─────────
|
|
625
|
+
//
|
|
626
|
+
// Uses the user's own `claude -p` to run a strict-JSON extraction prompt over
|
|
627
|
+
// the last N turns of the project's thread. Each extracted fact becomes a
|
|
628
|
+
// Stitch memory tagged `auto:true` so the user can review or wipe them later.
|
|
629
|
+
//
|
|
630
|
+
// Triggered manually (`stitch distill`), and automatically by the Stop hook
|
|
631
|
+
// when conditions are met (cooldown + new-turn threshold).
|
|
632
|
+
const DISTILL_STATE_FILE = path.join(CONFIG_DIR, 'distill-state.json');
|
|
633
|
+
const DISTILL_COOLDOWN_MS = 30 * 60 * 1000; // don't distill more than once per 30 min
|
|
634
|
+
const DISTILL_MIN_NEW_TURNS = 10; // need 10 new turns before bothering
|
|
635
|
+
const DISTILL_BATCH_SIZE = 30; // turns per distillation pass
|
|
636
|
+
function loadDistillState() {
|
|
637
|
+
try {
|
|
638
|
+
return JSON.parse(fs.readFileSync(DISTILL_STATE_FILE, 'utf8'));
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
return { threads: {} };
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function saveDistillState(s) {
|
|
645
|
+
if (!fs.existsSync(CONFIG_DIR))
|
|
646
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
647
|
+
fs.writeFileSync(DISTILL_STATE_FILE, JSON.stringify(s, null, 2));
|
|
648
|
+
}
|
|
649
|
+
const DISTILL_PROMPT = `You are a memory distiller for an AI coding assistant.
|
|
650
|
+
|
|
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.
|
|
655
|
+
|
|
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
|
+
}
|
|
662
|
+
|
|
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.
|
|
672
|
+
|
|
673
|
+
Conversation:
|
|
674
|
+
`;
|
|
675
|
+
async function cmdDistill(args) {
|
|
676
|
+
if (hasFlag(args, ['--review']))
|
|
677
|
+
return distillReview(args);
|
|
678
|
+
if (hasFlag(args, ['--clear']))
|
|
679
|
+
return distillClear(args);
|
|
680
|
+
const cfg = loadConfig();
|
|
681
|
+
const stitch = client(cfg);
|
|
682
|
+
const explicitThread = parseFlag(args, ['--thread', '-t']);
|
|
683
|
+
const thread = explicitThread || inferThread();
|
|
684
|
+
const dryRun = hasFlag(args, ['--dry-run']);
|
|
685
|
+
const batchSize = Number(parseFlag(args, ['--n']) || String(DISTILL_BATCH_SIZE));
|
|
686
|
+
console.log(`Distilling last ${batchSize} turns of "${thread}"…`);
|
|
687
|
+
// Pull recent turns from the thread.
|
|
688
|
+
const recall = await stitch.thread(thread).recall({ last: batchSize });
|
|
689
|
+
if (!recall.thread_id || recall.recent.length === 0) {
|
|
690
|
+
console.log('No turns to distill yet.');
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
// Format conversation as plain text for the prompt.
|
|
694
|
+
const conversation = recall.recent
|
|
695
|
+
.map((t) => `[${t.role}] ${String(t.content || '').trim()}`)
|
|
696
|
+
.join('\n\n');
|
|
697
|
+
if (dryRun) {
|
|
698
|
+
console.log(`Would distill ${recall.recent.length} turns. Sample (first 200 chars):`);
|
|
699
|
+
console.log(conversation.slice(0, 200));
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
const claudeBin = process.env.STITCH_CLAUDE_BIN || 'claude';
|
|
703
|
+
process.stdout.write(` → asking ${claudeBin} -p to extract facts… `);
|
|
704
|
+
const result = await run(claudeBin, ['-p', DISTILL_PROMPT + conversation], { cwd: process.cwd() });
|
|
705
|
+
if (result.exit_code !== 0) {
|
|
706
|
+
console.log('failed');
|
|
707
|
+
console.error(result.stderr.trim().slice(0, 400));
|
|
708
|
+
process.exit(1);
|
|
709
|
+
}
|
|
710
|
+
const memories = parseDistillationOutput(result.stdout);
|
|
711
|
+
console.log(`extracted ${memories.length} memories`);
|
|
712
|
+
if (memories.length === 0) {
|
|
713
|
+
bumpDistillCooldown(thread);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
// Push each to Stitch as a memory with auto:true tag.
|
|
717
|
+
const projectTag = thread.split('/')[0] || thread;
|
|
718
|
+
let saved = 0;
|
|
719
|
+
for (const m of memories) {
|
|
720
|
+
try {
|
|
721
|
+
const tags = ['auto', 'auto:distill', `thread:${thread}`, `project:${projectTag}`, ...(m.tags || [])];
|
|
722
|
+
await stitch.remember(m.content, { kind: m.kind, tags });
|
|
723
|
+
saved++;
|
|
724
|
+
}
|
|
725
|
+
catch (e) {
|
|
726
|
+
console.error(' ! failed to save:', e.message);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
console.log(` saved ${saved}/${memories.length} to Stitch`);
|
|
730
|
+
bumpDistillCooldown(thread);
|
|
731
|
+
}
|
|
732
|
+
function bumpDistillCooldown(thread) {
|
|
733
|
+
const state = loadDistillState();
|
|
734
|
+
state.threads[thread] = {
|
|
735
|
+
lastDistilledAt: Date.now(),
|
|
736
|
+
lastTurnAt: Date.now(),
|
|
737
|
+
lastTurnCount: state.threads[thread]?.lastTurnCount ?? 0,
|
|
738
|
+
};
|
|
739
|
+
saveDistillState(state);
|
|
740
|
+
}
|
|
741
|
+
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.
|
|
744
|
+
const text = stdout.trim();
|
|
745
|
+
const start = text.indexOf('[');
|
|
746
|
+
const end = text.lastIndexOf(']');
|
|
747
|
+
if (start === -1 || end === -1 || end < start)
|
|
748
|
+
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));
|
|
757
|
+
}
|
|
758
|
+
catch {
|
|
759
|
+
return [];
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
async function distillReview(args) {
|
|
763
|
+
const cfg = loadConfig();
|
|
764
|
+
const stitch = client(cfg);
|
|
765
|
+
const limit = Number(parseFlag(args, ['--limit']) || '20');
|
|
766
|
+
const all = await stitch.list({ limit: 200 });
|
|
767
|
+
const autos = all.filter((m) => m.tags.includes('auto'));
|
|
768
|
+
console.log(`${autos.length} auto-distilled memories (showing first ${limit}):\n`);
|
|
769
|
+
for (const m of autos.slice(0, limit)) {
|
|
770
|
+
const date = new Date(m.created_at).toISOString().slice(0, 19).replace('T', ' ');
|
|
771
|
+
console.log(` ${m.id} ${date} [${m.kind}]`);
|
|
772
|
+
console.log(` ${m.content.split('\n')[0].slice(0, 200)}`);
|
|
773
|
+
if (m.content.length > 200 || m.content.includes('\n'))
|
|
774
|
+
console.log(` ...`);
|
|
775
|
+
console.log();
|
|
776
|
+
}
|
|
777
|
+
if (autos.length > limit)
|
|
778
|
+
console.log(`(${autos.length - limit} more — use --limit to see)`);
|
|
779
|
+
}
|
|
780
|
+
async function distillClear(args) {
|
|
781
|
+
const cfg = loadConfig();
|
|
782
|
+
const stitch = client(cfg);
|
|
783
|
+
const all = await stitch.list({ limit: 500 });
|
|
784
|
+
const autos = all.filter((m) => m.tags.includes('auto'));
|
|
785
|
+
if (autos.length === 0) {
|
|
786
|
+
console.log('No auto-distilled memories to clear.');
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
if (!hasFlag(args, ['--yes', '-y'])) {
|
|
790
|
+
console.log(`Will soft-delete ${autos.length} auto-distilled memories. Re-run with --yes to confirm.`);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
let deleted = 0;
|
|
794
|
+
for (const m of autos) {
|
|
795
|
+
if (await stitch.forget(m.id))
|
|
796
|
+
deleted++;
|
|
797
|
+
}
|
|
798
|
+
console.log(`Cleared ${deleted} memories.`);
|
|
799
|
+
}
|
|
800
|
+
// Triggered from the Stop hook (fire-and-forget, never blocks the user).
|
|
801
|
+
async function maybeAutoDistill(thread) {
|
|
802
|
+
const state = loadDistillState();
|
|
803
|
+
const meta = state.threads[thread] || { lastDistilledAt: 0, lastTurnAt: 0, lastTurnCount: 0 };
|
|
804
|
+
// Cool-down: don't distill more than once per 30 min per thread.
|
|
805
|
+
if (Date.now() - meta.lastDistilledAt < DISTILL_COOLDOWN_MS)
|
|
806
|
+
return;
|
|
807
|
+
// Need at least N new turns since last pass.
|
|
808
|
+
const cfg = loadConfig();
|
|
809
|
+
const stitch = client(cfg);
|
|
810
|
+
let recallSize = 0;
|
|
811
|
+
try {
|
|
812
|
+
const r = await stitch.thread(thread).recall({ last: 200 });
|
|
813
|
+
recallSize = r.recent.length;
|
|
814
|
+
}
|
|
815
|
+
catch {
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
if (recallSize - meta.lastTurnCount < DISTILL_MIN_NEW_TURNS)
|
|
819
|
+
return;
|
|
820
|
+
// Mark BEFORE running so we don't double-fire on overlapping Stop events.
|
|
821
|
+
state.threads[thread] = { lastDistilledAt: Date.now(), lastTurnAt: Date.now(), lastTurnCount: recallSize };
|
|
822
|
+
saveDistillState(state);
|
|
823
|
+
// Detach: spawn a background process so the Stop hook returns immediately.
|
|
824
|
+
// The detached child runs `stitch distill` for this thread.
|
|
825
|
+
try {
|
|
826
|
+
const child = spawn(process.argv[0], [process.argv[1] || (await import('node:url')).fileURLToPath(import.meta.url), 'distill', '--thread', thread, '--n', String(DISTILL_BATCH_SIZE)], {
|
|
827
|
+
detached: true,
|
|
828
|
+
stdio: 'ignore',
|
|
829
|
+
});
|
|
830
|
+
child.unref();
|
|
831
|
+
}
|
|
832
|
+
catch { /* ignore */ }
|
|
833
|
+
}
|
|
560
834
|
// ── Sync Claude's local memory dir into Stitch ────────────────────────────
|
|
561
835
|
// Claude Code keeps per-project memory at ~/.claude/projects/<encoded>/memory/
|
|
562
836
|
// Each file is a markdown memory with optional YAML frontmatter (name,
|
|
@@ -846,15 +1120,16 @@ async function cmdInstall(args) {
|
|
|
846
1120
|
else
|
|
847
1121
|
console.log(`failed (${stderr.trim().slice(0, 120)})`);
|
|
848
1122
|
}
|
|
849
|
-
// 2. Hooks
|
|
1123
|
+
// 2. Hooks: auto-log every turn + auto-inject prior context at session start
|
|
850
1124
|
if (!noHooks) {
|
|
851
|
-
process.stdout.write('• Wiring
|
|
1125
|
+
process.stdout.write('• Wiring hooks (SessionStart + UserPromptSubmit + Stop)… ');
|
|
852
1126
|
try {
|
|
853
1127
|
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
854
1128
|
const existing = fs.existsSync(settingsPath)
|
|
855
1129
|
? JSON.parse(fs.readFileSync(settingsPath, 'utf8'))
|
|
856
1130
|
: {};
|
|
857
1131
|
existing.hooks = existing.hooks || {};
|
|
1132
|
+
existing.hooks.SessionStart = mergeHook(existing.hooks.SessionStart, STITCH_SESSION_START_HOOK);
|
|
858
1133
|
existing.hooks.UserPromptSubmit = mergeHook(existing.hooks.UserPromptSubmit, STITCH_USER_HOOK);
|
|
859
1134
|
existing.hooks.Stop = mergeHook(existing.hooks.Stop, STITCH_STOP_HOOK);
|
|
860
1135
|
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
@@ -957,6 +1232,10 @@ const STITCH_STOP_HOOK = {
|
|
|
957
1232
|
matcher: '*',
|
|
958
1233
|
hooks: [{ type: 'command', command: 'stitch _hook' }],
|
|
959
1234
|
};
|
|
1235
|
+
const STITCH_SESSION_START_HOOK = {
|
|
1236
|
+
matcher: '*',
|
|
1237
|
+
hooks: [{ type: 'command', command: 'stitch _hook SessionStart' }],
|
|
1238
|
+
};
|
|
960
1239
|
function mergeHook(existing, entry) {
|
|
961
1240
|
const arr = Array.isArray(existing) ? existing.slice() : [];
|
|
962
1241
|
// Replace any earlier Stitch entry; identify by the marker.
|
|
@@ -1130,6 +1409,24 @@ function help() {
|
|
|
1130
1409
|
stitch whoami Show the configured key.
|
|
1131
1410
|
stitch logout
|
|
1132
1411
|
|
|
1412
|
+
stitch link [name] Pin this project to a canonical
|
|
1413
|
+
thread name. Writes
|
|
1414
|
+
.stitch/project.json — commit it
|
|
1415
|
+
(or sync the file across machines)
|
|
1416
|
+
so every machine that pins this
|
|
1417
|
+
project shares the same memory.
|
|
1418
|
+
|
|
1419
|
+
stitch distill [--thread <t>] [--n 30] [--dry-run]
|
|
1420
|
+
Read the last N turns of a thread
|
|
1421
|
+
and have your local Claude Code
|
|
1422
|
+
extract durable facts/decisions
|
|
1423
|
+
into Stitch memories. Tagged
|
|
1424
|
+
auto so they can be reviewed.
|
|
1425
|
+
stitch distill --review [--limit 20] Show recent auto-distilled
|
|
1426
|
+
memories for inspection.
|
|
1427
|
+
stitch distill --clear --yes Soft-delete every auto-distilled
|
|
1428
|
+
memory (recoverable 30 days).
|
|
1429
|
+
|
|
1133
1430
|
stitch sync [--watch] [--dry-run] Mirror ~/.claude/projects/*/memory/
|
|
1134
1431
|
files into Stitch as memories.
|
|
1135
1432
|
--watch keeps running; otherwise it's
|
|
@@ -1177,6 +1474,8 @@ async function main(argv) {
|
|
|
1177
1474
|
case 'thread': return cmdThread(rest);
|
|
1178
1475
|
case 'install': return cmdInstall(rest);
|
|
1179
1476
|
case 'sync': return cmdSync(rest);
|
|
1477
|
+
case 'link': return cmdLink(rest);
|
|
1478
|
+
case 'distill': return cmdDistill(rest);
|
|
1180
1479
|
case '_hook': return cmdHook(rest);
|
|
1181
1480
|
case 'update':
|
|
1182
1481
|
case 'upgrade': return cmdUpdate(rest);
|