@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.
- package/dist/cli.js +368 -12
- 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
|
|
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
|
|
425
|
-
//
|
|
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} ·
|
|
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
|
|
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(' •
|
|
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.
|
|
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.
|
|
19
|
+
"@stitchdb/agent": "^0.2.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"typescript": "^5.4.0",
|