@stitchdb/cli 0.6.1 → 0.7.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 +368 -12
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -211,8 +211,46 @@ async function cmdWorkspace(args) {
211
211
  saveConfig(cfg);
212
212
  console.log(`Default workspace set: ${id}`);
213
213
  }
214
+ else if (sub === 'rename') {
215
+ const positionals = positional(rest);
216
+ const id = positionals[0];
217
+ const name = positionals.slice(1).join(' ');
218
+ if (!id || !name) {
219
+ console.error('Usage: stitch workspace rename <id> <new-name>');
220
+ process.exit(2);
221
+ }
222
+ const w = await stitch.workspaces.update(id, { name });
223
+ console.log(`Renamed ${w.id} → ${w.name}`);
224
+ }
225
+ else if (sub === 'delete') {
226
+ const id = positional(rest)[0];
227
+ if (!id) {
228
+ console.error('Usage: stitch workspace delete <id> [--yes]');
229
+ process.exit(2);
230
+ }
231
+ if (!hasFlag(rest, ['--yes', '-y'])) {
232
+ // Show what will be lost so the user knows the blast radius.
233
+ try {
234
+ const w = await stitch.workspaces.get(id);
235
+ console.log(`This will permanently delete workspace "${w.name}" (${w.id})`);
236
+ console.log('and ALL its memories, threads, and conversation history.');
237
+ console.log('Re-run with --yes to confirm.');
238
+ }
239
+ catch (e) {
240
+ console.error(`Workspace ${id} not found: ${e?.message || e}`);
241
+ process.exit(1);
242
+ }
243
+ return;
244
+ }
245
+ const ok = await stitch.workspaces.delete(id);
246
+ console.log(ok ? `Deleted ${id}` : `Workspace ${id} not found`);
247
+ if (cfg.defaultWorkspace === id) {
248
+ delete cfg.defaultWorkspace;
249
+ saveConfig(cfg);
250
+ }
251
+ }
214
252
  else {
215
- console.error('Usage: stitch workspace [list|create <name>|use <id>]');
253
+ console.error('Usage: stitch workspace [list | create <name> | use <id> | rename <id> <name> | delete <id> --yes]');
216
254
  process.exit(2);
217
255
  }
218
256
  }
@@ -420,9 +458,10 @@ async function cmdHook(args) {
420
458
  const cfg = loadConfig();
421
459
  if (!cfg.apiKey)
422
460
  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.
461
+ // When the Stitch CLI spawns its own `claude -p` (e.g. for distillation
462
+ // or file summary extraction), we don't want THAT inner conversation
463
+ // logged or its tool calls cached it would create runaway recursion
464
+ // and pollute the thread with internal prompts.
426
465
  if (process.env.STITCH_HOOKS_DISABLED === '1')
427
466
  return;
428
467
  let raw = '';
@@ -446,6 +485,16 @@ async function cmdHook(args) {
446
485
  // (which is the directory `claude` was launched from).
447
486
  const cwd = event?.cwd || process.cwd();
448
487
  const threadName = inferThreadFor(cwd) || 'default';
488
+ // ── PreToolUse on Read: inject cached summary if hash matches ─────────
489
+ if (eventName === 'PreToolUse' && event?.tool_name === 'Read') {
490
+ await handlePreReadHook(cfg, event, cwd || process.cwd()).catch(() => { });
491
+ return;
492
+ }
493
+ // ── PostToolUse on Read: opportunistically save a fresh summary ───────
494
+ if (eventName === 'PostToolUse' && event?.tool_name === 'Read') {
495
+ await handlePostReadHook(cfg, event, cwd || process.cwd()).catch(() => { });
496
+ return;
497
+ }
449
498
  // ── SessionStart: inject prior context (token-efficient) ─────────────
450
499
  // Strategy: prefer distilled memories (dense facts) over raw turns. Only
451
500
  // include raw turns for the last 5 to give the agent immediate continuation.
@@ -453,16 +502,30 @@ async function cmdHook(args) {
453
502
  try {
454
503
  const stitch = client(cfg);
455
504
  const projectTag = (threadName.split('/')[0] || threadName).toLowerCase();
456
- const [thread, memHits] = await Promise.all([
505
+ const [thread, memHits, workspaces, fileSummaries, aboutMems] = await Promise.all([
457
506
  stitch.thread(threadName).recall({ last: 5 }).catch(() => ({ thread_id: '', recent: [], semantic: [] })),
458
507
  stitch.recall(projectTag, { k: 8 }).catch(() => []),
508
+ stitch.workspaces.list().catch(() => []),
509
+ stitch.list({ limit: 12 }).then((all) => all.filter((m) => m.tags.some((t) => t.startsWith('file:')))).catch(() => []),
510
+ stitch.list({ tag: 'workspace:about', limit: 1 }).catch(() => []),
459
511
  ]);
512
+ const currentWs = Array.isArray(workspaces) && workspaces.length > 0 ? workspaces[0] : null;
460
513
  const lines = [];
461
514
  lines.push('<stitch-context>');
462
- lines.push(`Project: ${threadName} · Stitch MCP server is available with tools: recall, remember, thread_recall, thread_append.`);
515
+ lines.push(`Project: ${threadName} · Workspace: ${currentWs?.name || '(unknown)'} · Stitch MCP tools: recall, remember, thread_recall, thread_append, workspace_setup, file_summary, file_summary_save.`);
463
516
  lines.push('');
517
+ // Nudge the AI to set a meaningful workspace name once.
518
+ if (currentWs?.name === 'default') {
519
+ lines.push('### ⚠ Workspace is still named "default"');
520
+ lines.push('Call the `workspace_setup` MCP tool with a slug-style name based on this project (e.g. the package.json name or repo dir).');
521
+ lines.push('');
522
+ }
523
+ if (Array.isArray(aboutMems) && aboutMems.length > 0) {
524
+ lines.push('### About this workspace');
525
+ lines.push(String(aboutMems[0].content || '').slice(0, 400));
526
+ lines.push('');
527
+ }
464
528
  if (Array.isArray(memHits) && memHits.length > 0) {
465
- // Prefer distilled (auto-tagged) facts and decisions; fall back to anything else
466
529
  const sortedMems = [...memHits].sort((a, b) => Number(b.score ?? 0) - Number(a.score ?? 0)).slice(0, 8);
467
530
  lines.push('### Durable memories for this project');
468
531
  for (const m of sortedMems) {
@@ -472,6 +535,16 @@ async function cmdHook(args) {
472
535
  }
473
536
  lines.push('');
474
537
  }
538
+ if (Array.isArray(fileSummaries) && fileSummaries.length > 0) {
539
+ lines.push('### File summaries (cached — call `file_summary` before reading)');
540
+ for (const m of fileSummaries.slice(0, 8)) {
541
+ const fileTag = m.tags.find((t) => t.startsWith('file:'));
542
+ const filePath = fileTag ? fileTag.slice(5) : '(unknown)';
543
+ const txt = String(m.content || '').replace(/\n+/g, ' ').slice(0, 250);
544
+ lines.push(`- **${filePath}** — ${txt}`);
545
+ }
546
+ lines.push('');
547
+ }
475
548
  if (thread.recent && thread.recent.length > 0) {
476
549
  lines.push('### Most recent turns (continue from here)');
477
550
  for (const t of thread.recent.slice(-5)) {
@@ -480,7 +553,7 @@ async function cmdHook(args) {
480
553
  }
481
554
  lines.push('');
482
555
  }
483
- lines.push('Call `recall` for deeper search or `thread_recall` for older turns when needed.');
556
+ lines.push('Call `recall` for deeper search, `thread_recall` for older turns, `file_summary` BEFORE reading any non-trivial file (saves tokens).');
484
557
  lines.push('</stitch-context>');
485
558
  process.stdout.write(lines.join('\n'));
486
559
  }
@@ -549,6 +622,38 @@ function inferThreadFor(cwd) {
549
622
  }
550
623
  return path.basename(dir) || 'default';
551
624
  }
625
+ /**
626
+ * Best-effort derive a workspace slug from a project directory:
627
+ * - package.json `name` (npm convention, slug-friendly already)
628
+ * - else folder basename, normalised to a lowercase slug
629
+ *
630
+ * The AI can later refine via the `workspace_setup` MCP tool — this is just
631
+ * the seed name so it isn't "default".
632
+ */
633
+ function deriveWorkspaceName(cwd) {
634
+ // 1. package.json `name`.
635
+ try {
636
+ const pkgPath = path.join(cwd, 'package.json');
637
+ if (fs.existsSync(pkgPath)) {
638
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
639
+ if (typeof pkg.name === 'string' && pkg.name.trim()) {
640
+ // Strip leading scope (@org/) if any — keeps the slug clean.
641
+ const stripped = pkg.name.replace(/^@[^/]+\//, '').trim();
642
+ if (stripped)
643
+ return slugify(stripped).slice(0, 60);
644
+ }
645
+ }
646
+ }
647
+ catch { /* fall through */ }
648
+ // 2. Folder basename.
649
+ return slugify(path.basename(cwd) || 'default').slice(0, 60) || 'default';
650
+ }
651
+ function slugify(s) {
652
+ return s
653
+ .toLowerCase()
654
+ .replace(/[^a-z0-9]+/g, '-')
655
+ .replace(/^-+|-+$/g, '') || 'default';
656
+ }
552
657
  function findProjectRoot(cwd) {
553
658
  let cur = cwd;
554
659
  for (let i = 0; i < 8; i++) {
@@ -604,6 +709,223 @@ function lastAssistantTextFromTranscript(transcriptPath) {
604
709
  catch { /* ignore */ }
605
710
  return text;
606
711
  }
712
+ // ─── File summary cache ────────────────────────────────────────────────────
713
+ //
714
+ // Goal: when Claude reads a non-trivial file we've seen before, surface our
715
+ // cached 2-3 sentence summary so the agent can decide whether to skim or
716
+ // re-read in full. When Claude reads a file we haven't summarized (or that's
717
+ // changed), kick off a background `claude -p` to extract a summary and save
718
+ // it as a memory tagged `file:<path>` + `hash:<sha-prefix>`.
719
+ //
720
+ // Trade-off: the cache is hash-bound, so any byte change invalidates the
721
+ // cached summary — fine for big stable files (docs, configs, third-party src),
722
+ // less useful for files Claude is actively editing. The PostToolUse hook
723
+ // re-summarizes after the next read, so the cache repopulates on its own.
724
+ const FILE_SUMMARY_MIN_BYTES = 500; // skip tiny files
725
+ const FILE_SUMMARY_MAX_BYTES = 200_000; // skip absolutely huge files
726
+ const FILE_SUMMARY_COOLDOWN_MS = 5 * 60_000; // per-file: don't re-summarize more than once per 5 min
727
+ const FILE_SUMMARY_PROMPT = `You are summarising a single source file for an AI coding assistant cache.
728
+
729
+ Return ONLY a 2-3 sentence summary describing:
730
+ • What the file does (purpose, the few exported symbols / commands / endpoints worth knowing).
731
+ • Any important pattern, convention, or non-obvious behaviour another agent would benefit from before reading the file in full.
732
+
733
+ Skip preamble. Skip "this file …". Be specific. No markdown fences. No prose around it.
734
+
735
+ File path: {{PATH}}
736
+
737
+ File content:
738
+ {{CONTENT}}
739
+ `;
740
+ function readFileContentFromHookEvent(event) {
741
+ // Claude Code hook 'tool_response' shape varies between versions; accept
742
+ // a few shapes. The Read tool's success path includes the file content
743
+ // somewhere in tool_response.
744
+ const r = event?.tool_response;
745
+ if (!r)
746
+ return '';
747
+ if (typeof r === 'string')
748
+ return r;
749
+ if (typeof r?.file?.content === 'string')
750
+ return r.file.content;
751
+ if (typeof r?.content === 'string')
752
+ return r.content;
753
+ if (Array.isArray(r?.content)) {
754
+ return r.content.map((c) => (typeof c === 'string' ? c : c?.text ?? '')).join('');
755
+ }
756
+ return '';
757
+ }
758
+ function relPathFor(cwd, abs) {
759
+ try {
760
+ return path.relative(cwd, abs) || abs;
761
+ }
762
+ catch {
763
+ return abs;
764
+ }
765
+ }
766
+ async function sha256Hex(s) {
767
+ const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(s));
768
+ return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, '0')).join('');
769
+ }
770
+ async function lookupFileSummary(stitch, relPath) {
771
+ try {
772
+ const list = await stitch.list({ tag: `file:${relPath}`, limit: 5 });
773
+ if (!list || list.length === 0)
774
+ return null;
775
+ const first = list[0];
776
+ const hashTag = first.tags.find((t) => t.startsWith('hash:'));
777
+ return {
778
+ id: first.id,
779
+ content: first.content,
780
+ hash: hashTag ? hashTag.slice(5) : null,
781
+ created_at: first.created_at,
782
+ };
783
+ }
784
+ catch {
785
+ return null;
786
+ }
787
+ }
788
+ // PreToolUse on Read: if we have a cached summary for this exact file path,
789
+ // emit Claude-Code-flavoured JSON to stdout that adds it as additional context.
790
+ async function handlePreReadHook(cfg, event, cwd) {
791
+ const filePath = String(event?.tool_input?.file_path || '');
792
+ if (!filePath)
793
+ return;
794
+ const rel = relPathFor(cwd, filePath);
795
+ const stitch = client(cfg);
796
+ const cached = await lookupFileSummary(stitch, rel);
797
+ if (!cached)
798
+ return;
799
+ const ageMin = Math.round((Date.now() - cached.created_at) / 60_000);
800
+ const note = [
801
+ `Stitch cache: a prior summary of ${rel} exists (${ageMin}m ago).`,
802
+ cached.hash ? `Cached hash prefix: ${cached.hash}.` : '',
803
+ `Summary: ${cached.content}`,
804
+ `If the file hasn't changed, you can act on this summary alone and skip a full read; otherwise the read will re-cache the summary.`,
805
+ ].filter(Boolean).join('\n');
806
+ // Claude Code accepts a JSON object on stdout for PreToolUse with
807
+ // hookSpecificOutput.additionalContext to inject text into the model's
808
+ // context for this tool call.
809
+ const payload = { hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: note } };
810
+ process.stdout.write(JSON.stringify(payload));
811
+ }
812
+ // PostToolUse on Read: kick off a background `claude -p` to summarize this
813
+ // file IF we don't already have a summary at the current hash. Fire-and-forget.
814
+ async function handlePostReadHook(cfg, event, cwd) {
815
+ const filePath = String(event?.tool_input?.file_path || '');
816
+ if (!filePath)
817
+ return;
818
+ const rel = relPathFor(cwd, filePath);
819
+ const content = readFileContentFromHookEvent(event);
820
+ // Fall back to reading from disk if hook payload didn't carry content.
821
+ let body = content;
822
+ if (!body) {
823
+ try {
824
+ body = fs.readFileSync(filePath, 'utf8');
825
+ }
826
+ catch {
827
+ return;
828
+ }
829
+ }
830
+ if (!body || body.length < FILE_SUMMARY_MIN_BYTES)
831
+ return;
832
+ if (body.length > FILE_SUMMARY_MAX_BYTES)
833
+ return;
834
+ const fullHash = await sha256Hex(body);
835
+ const hashPrefix = fullHash.slice(0, 16);
836
+ // Per-file cooldown so a hot-edited file doesn't re-summarize on every read.
837
+ const state = loadFileSummaryState();
838
+ const last = state.files[rel];
839
+ if (last && last.hash === hashPrefix && Date.now() - last.lastSummarizedAt < FILE_SUMMARY_COOLDOWN_MS)
840
+ return;
841
+ const stitch = client(cfg);
842
+ const cached = await lookupFileSummary(stitch, rel);
843
+ if (cached && cached.hash === hashPrefix) {
844
+ // Already summarized this exact version — refresh cooldown and bail.
845
+ state.files[rel] = { hash: hashPrefix, lastSummarizedAt: Date.now() };
846
+ saveFileSummaryState(state);
847
+ return;
848
+ }
849
+ // Mark BEFORE spawning so a flurry of Read events doesn't fire N spawns.
850
+ state.files[rel] = { hash: hashPrefix, lastSummarizedAt: Date.now() };
851
+ saveFileSummaryState(state);
852
+ // Spawn detached: stitch _summarize-file <abs-path> <hash> <rel>
853
+ try {
854
+ const cliPath = process.argv[1] || (await import('node:url')).fileURLToPath(import.meta.url);
855
+ const child = spawn(process.argv[0], [cliPath, '_summarize-file', filePath, hashPrefix, rel], {
856
+ detached: true,
857
+ stdio: 'ignore',
858
+ env: { ...process.env, STITCH_HOOKS_DISABLED: '1' },
859
+ });
860
+ child.unref();
861
+ }
862
+ catch { /* ignore */ }
863
+ }
864
+ // File summary state: per-file cooldown so we don't re-fire on every Read.
865
+ const FILE_SUMMARY_STATE_FILE = path.join(CONFIG_DIR, 'file-summary-state.json');
866
+ function loadFileSummaryState() {
867
+ try {
868
+ return JSON.parse(fs.readFileSync(FILE_SUMMARY_STATE_FILE, 'utf8'));
869
+ }
870
+ catch {
871
+ return { files: {} };
872
+ }
873
+ }
874
+ function saveFileSummaryState(s) {
875
+ if (!fs.existsSync(CONFIG_DIR))
876
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
877
+ fs.writeFileSync(FILE_SUMMARY_STATE_FILE, JSON.stringify(s, null, 2));
878
+ }
879
+ // `stitch _summarize-file <abs> <hashPrefix> <rel>` — internal entry called
880
+ // from the detached PostToolUse spawn. Reads the file, asks claude -p for a
881
+ // 2-3 sentence summary, saves as a Stitch memory.
882
+ async function cmdSummarizeFile(args) {
883
+ const [abs, hashPrefix, rel] = args;
884
+ if (!abs || !hashPrefix || !rel)
885
+ return;
886
+ const cfg = loadConfig();
887
+ if (!cfg.apiKey)
888
+ return;
889
+ let body = '';
890
+ try {
891
+ body = fs.readFileSync(abs, 'utf8');
892
+ }
893
+ catch {
894
+ return;
895
+ }
896
+ if (!body || body.length > FILE_SUMMARY_MAX_BYTES)
897
+ return;
898
+ const claudeBin = process.env.STITCH_CLAUDE_BIN || 'claude';
899
+ const prompt = FILE_SUMMARY_PROMPT
900
+ .replace('{{PATH}}', rel)
901
+ .replace('{{CONTENT}}', body);
902
+ const result = await runWithStdin(claudeBin, ['-p'], prompt, {
903
+ cwd: process.cwd(),
904
+ env: { STITCH_HOOKS_DISABLED: '1' },
905
+ });
906
+ if (result.exit_code !== 0)
907
+ return;
908
+ const summary = result.stdout.trim().slice(0, 2000);
909
+ if (!summary)
910
+ return;
911
+ try {
912
+ const stitch = client(cfg);
913
+ const projectTag = (inferThread() || 'default').split('/')[0];
914
+ // Replace any prior summary for this path.
915
+ const prior = await stitch.list({ tag: `file:${rel}`, limit: 50 }).catch(() => []);
916
+ for (const p of prior) {
917
+ try {
918
+ await stitch.forget(p.id);
919
+ }
920
+ catch { }
921
+ }
922
+ await stitch.remember(summary, {
923
+ kind: 'snippet',
924
+ tags: [`file:${rel}`, `hash:${hashPrefix}`, 'auto:file-summary', `project:${projectTag}`],
925
+ });
926
+ }
927
+ catch { /* silent — never break a session */ }
928
+ }
607
929
  // ── stitch link — pin a project's thread name so it's the same on every machine
608
930
  async function cmdLink(args) {
609
931
  const positionals = positional(args);
@@ -1179,6 +1501,26 @@ async function cmdInstall(args) {
1179
1501
  const ws = await stitch.resolveWorkspace();
1180
1502
  const baseUrl = cfg.baseUrl || 'https://db.stitchdb.com';
1181
1503
  const mcpUrl = `${baseUrl}/mcp/v1/${ws}`;
1504
+ // 0. If the workspace is still called "default", upgrade its name to a
1505
+ // project-derived slug so memories surface under a meaningful identity.
1506
+ // The AI can later refine it via the workspace_setup MCP tool.
1507
+ if (!hasFlag(args, ['--no-rename-workspace'])) {
1508
+ try {
1509
+ const current = await stitch.workspaces.get(ws);
1510
+ if (current.name === 'default') {
1511
+ const derived = deriveWorkspaceName(process.cwd());
1512
+ if (derived && derived !== 'default') {
1513
+ process.stdout.write(`• Naming workspace "${derived}" (was "default")… `);
1514
+ await stitch.workspaces.update(ws, { name: derived });
1515
+ console.log('ok');
1516
+ }
1517
+ }
1518
+ }
1519
+ catch (e) {
1520
+ // Older deployments may not have the PATCH endpoint yet.
1521
+ console.log(`• Workspace rename skipped (${e?.message || e}).`);
1522
+ }
1523
+ }
1182
1524
  const noMcp = hasFlag(args, ['--no-mcp']);
1183
1525
  const noHooks = hasFlag(args, ['--no-hooks']);
1184
1526
  const noClaudeMd = hasFlag(args, ['--no-claude-md']);
@@ -1197,9 +1539,10 @@ async function cmdInstall(args) {
1197
1539
  else
1198
1540
  console.log(`failed (${stderr.trim().slice(0, 120)})`);
1199
1541
  }
1200
- // 2. Hooks: auto-log every turn + auto-inject prior context at session start
1542
+ // 2. Hooks: auto-log every turn + auto-inject prior context at session start +
1543
+ // file-summary cache (Pre/PostToolUse on Read).
1201
1544
  if (!noHooks) {
1202
- process.stdout.write('• Wiring hooks (SessionStart + UserPromptSubmit + Stop)… ');
1545
+ process.stdout.write('• Wiring hooks (SessionStart + UserPromptSubmit + Stop + Pre/PostToolUse:Read)… ');
1203
1546
  try {
1204
1547
  const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
1205
1548
  const existing = fs.existsSync(settingsPath)
@@ -1209,6 +1552,8 @@ async function cmdInstall(args) {
1209
1552
  existing.hooks.SessionStart = mergeHook(existing.hooks.SessionStart, STITCH_SESSION_START_HOOK);
1210
1553
  existing.hooks.UserPromptSubmit = mergeHook(existing.hooks.UserPromptSubmit, STITCH_USER_HOOK);
1211
1554
  existing.hooks.Stop = mergeHook(existing.hooks.Stop, STITCH_STOP_HOOK);
1555
+ existing.hooks.PreToolUse = mergeHook(existing.hooks.PreToolUse, STITCH_PRE_READ_HOOK);
1556
+ existing.hooks.PostToolUse = mergeHook(existing.hooks.PostToolUse, STITCH_POST_READ_HOOK);
1212
1557
  fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
1213
1558
  fs.writeFileSync(settingsPath, JSON.stringify(existing, null, 2));
1214
1559
  console.log('ok');
@@ -1290,10 +1635,11 @@ async function cmdInstall(args) {
1290
1635
  }
1291
1636
  console.log();
1292
1637
  console.log('Done. Open a fresh `claude` session — Stitch is wired.');
1293
- console.log(' • remember/recall tools are available');
1638
+ console.log(' • workspace_setup, recall, remember, thread tools are available');
1294
1639
  console.log(' • every turn auto-logs to the thread for this repo');
1295
1640
  console.log(' • Claude auto-pulls past context at session start (per CLAUDE.md)');
1296
1641
  console.log(' • Claude memory files are mirrored to Stitch');
1642
+ console.log(' • file Reads auto-cache 2-3 sentence summaries; Pre-Read injects them');
1297
1643
  console.log();
1298
1644
  console.log('Run `stitch sync --watch` to keep them in sync as Claude updates them.');
1299
1645
  }
@@ -1313,6 +1659,15 @@ const STITCH_SESSION_START_HOOK = {
1313
1659
  matcher: '*',
1314
1660
  hooks: [{ type: 'command', command: 'stitch _hook SessionStart' }],
1315
1661
  };
1662
+ // File summary cache: only fires for the Read tool, both pre and post.
1663
+ const STITCH_PRE_READ_HOOK = {
1664
+ matcher: 'Read',
1665
+ hooks: [{ type: 'command', command: 'stitch _hook' }],
1666
+ };
1667
+ const STITCH_POST_READ_HOOK = {
1668
+ matcher: 'Read',
1669
+ hooks: [{ type: 'command', command: 'stitch _hook' }],
1670
+ };
1316
1671
  function mergeHook(existing, entry) {
1317
1672
  const arr = Array.isArray(existing) ? existing.slice() : [];
1318
1673
  // Replace any earlier Stitch entry; identify by the marker.
@@ -1543,7 +1898,7 @@ function help() {
1543
1898
  stitch thread current Print the auto-derived thread name
1544
1899
  for the current repo / cwd.
1545
1900
 
1546
- stitch workspace [list | create <name> | use <id>]
1901
+ stitch workspace [list | create <name> | use <id> | rename <id> <name> | delete <id> --yes]
1547
1902
 
1548
1903
  stitch agent register <name> Create an agent identity (id only).
1549
1904
  stitch agent list
@@ -1570,6 +1925,7 @@ async function main(argv) {
1570
1925
  case 'link': return cmdLink(rest);
1571
1926
  case 'distill': return cmdDistill(rest);
1572
1927
  case '_hook': return cmdHook(rest);
1928
+ case '_summarize-file': return cmdSummarizeFile(rest);
1573
1929
  case 'update':
1574
1930
  case 'upgrade': return cmdUpdate(rest);
1575
1931
  case 'version':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stitchdb/cli",
3
- "version": "0.6.1",
3
+ "version": "0.7.1",
4
4
  "description": "Stitch CLI — manage memory + run agents from your terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  "license": "MIT",
17
17
  "engines": { "node": ">=20" },
18
18
  "dependencies": {
19
- "@stitchdb/agent": "^0.1.1"
19
+ "@stitchdb/agent": "^0.2.0"
20
20
  },
21
21
  "devDependencies": {
22
22
  "typescript": "^5.4.0",