@stitchdb/cli 0.5.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 +314 -63
- 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();
|
|
@@ -469,42 +441,43 @@ async function cmdHook(args) {
|
|
|
469
441
|
// (which is the directory `claude` was launched from).
|
|
470
442
|
const cwd = event?.cwd || process.cwd();
|
|
471
443
|
const threadName = inferThreadFor(cwd) || 'default';
|
|
472
|
-
// ── SessionStart: inject prior context
|
|
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.
|
|
473
447
|
if (eventName === 'SessionStart') {
|
|
474
448
|
try {
|
|
475
449
|
const stitch = client(cfg);
|
|
476
|
-
const projectTag =
|
|
477
|
-
// Pull both: recent dialogue from the per-repo thread + relevant
|
|
478
|
-
// durable memories tagged with the project.
|
|
450
|
+
const projectTag = (threadName.split('/')[0] || threadName).toLowerCase();
|
|
479
451
|
const [thread, memHits] = await Promise.all([
|
|
480
|
-
stitch.thread(threadName).recall({ last:
|
|
481
|
-
stitch.recall(projectTag, { k:
|
|
452
|
+
stitch.thread(threadName).recall({ last: 5 }).catch(() => ({ thread_id: '', recent: [], semantic: [] })),
|
|
453
|
+
stitch.recall(projectTag, { k: 8 }).catch(() => []),
|
|
482
454
|
]);
|
|
483
455
|
const lines = [];
|
|
484
|
-
lines.push('
|
|
485
|
-
lines.push(
|
|
486
|
-
lines.push(`Thread: \`${threadName}\` · Workspace recall available via the \`stitch\` MCP server.`);
|
|
456
|
+
lines.push('<stitch-context>');
|
|
457
|
+
lines.push(`Project: ${threadName} · Stitch MCP server is available with tools: recall, remember, thread_recall, thread_append.`);
|
|
487
458
|
lines.push('');
|
|
488
|
-
if (
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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}`);
|
|
493
467
|
}
|
|
494
468
|
lines.push('');
|
|
495
469
|
}
|
|
496
|
-
if (
|
|
497
|
-
lines.push('###
|
|
498
|
-
for (const
|
|
499
|
-
const txt = String(
|
|
500
|
-
lines.push(`-
|
|
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}`);
|
|
501
475
|
}
|
|
502
476
|
lines.push('');
|
|
503
477
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
}
|
|
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'));
|
|
508
481
|
}
|
|
509
482
|
catch { /* silent — never break a session start */ }
|
|
510
483
|
return;
|
|
@@ -533,29 +506,55 @@ async function cmdHook(args) {
|
|
|
533
506
|
catch {
|
|
534
507
|
/* silent */
|
|
535
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
|
+
}
|
|
536
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
|
+
*/
|
|
537
527
|
function inferThreadFor(cwd) {
|
|
538
528
|
const dir = cwd && fs.existsSync(cwd) ? cwd : process.cwd();
|
|
539
529
|
let cur = dir;
|
|
540
530
|
for (let i = 0; i < 8; i++) {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
let branch = 'main';
|
|
531
|
+
const projectFile = path.join(cur, '.stitch', 'project.json');
|
|
532
|
+
if (fs.existsSync(projectFile)) {
|
|
544
533
|
try {
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
branch = m[1];
|
|
534
|
+
const cfg = JSON.parse(fs.readFileSync(projectFile, 'utf8'));
|
|
535
|
+
if (cfg.thread && typeof cfg.thread === 'string')
|
|
536
|
+
return cfg.thread;
|
|
549
537
|
}
|
|
550
|
-
catch { /*
|
|
551
|
-
return `${repoName}/${branch}`;
|
|
538
|
+
catch { /* malformed — fall through */ }
|
|
552
539
|
}
|
|
553
540
|
const next = path.dirname(cur);
|
|
554
541
|
if (next === cur)
|
|
555
542
|
break;
|
|
556
543
|
cur = next;
|
|
557
544
|
}
|
|
558
|
-
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;
|
|
559
558
|
}
|
|
560
559
|
function lastAssistantTextFromTranscript(transcriptPath) {
|
|
561
560
|
if (!transcriptPath || !fs.existsSync(transcriptPath))
|
|
@@ -600,6 +599,238 @@ function lastAssistantTextFromTranscript(transcriptPath) {
|
|
|
600
599
|
catch { /* ignore */ }
|
|
601
600
|
return text;
|
|
602
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
|
+
}
|
|
603
834
|
// ── Sync Claude's local memory dir into Stitch ────────────────────────────
|
|
604
835
|
// Claude Code keeps per-project memory at ~/.claude/projects/<encoded>/memory/
|
|
605
836
|
// Each file is a markdown memory with optional YAML frontmatter (name,
|
|
@@ -1178,6 +1409,24 @@ function help() {
|
|
|
1178
1409
|
stitch whoami Show the configured key.
|
|
1179
1410
|
stitch logout
|
|
1180
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
|
+
|
|
1181
1430
|
stitch sync [--watch] [--dry-run] Mirror ~/.claude/projects/*/memory/
|
|
1182
1431
|
files into Stitch as memories.
|
|
1183
1432
|
--watch keeps running; otherwise it's
|
|
@@ -1225,6 +1474,8 @@ async function main(argv) {
|
|
|
1225
1474
|
case 'thread': return cmdThread(rest);
|
|
1226
1475
|
case 'install': return cmdInstall(rest);
|
|
1227
1476
|
case 'sync': return cmdSync(rest);
|
|
1477
|
+
case 'link': return cmdLink(rest);
|
|
1478
|
+
case 'distill': return cmdDistill(rest);
|
|
1228
1479
|
case '_hook': return cmdHook(rest);
|
|
1229
1480
|
case 'update':
|
|
1230
1481
|
case 'upgrade': return cmdUpdate(rest);
|