@stitchdb/cli 0.6.1 → 0.7.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 +328 -10
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -420,9 +420,10 @@ async function cmdHook(args) {
|
|
|
420
420
|
const cfg = loadConfig();
|
|
421
421
|
if (!cfg.apiKey)
|
|
422
422
|
return; // not logged in, silently skip
|
|
423
|
-
// When the Stitch CLI spawns its own `claude -p` (e.g. for distillation
|
|
424
|
-
// we don't want THAT inner conversation
|
|
425
|
-
//
|
|
423
|
+
// When the Stitch CLI spawns its own `claude -p` (e.g. for distillation
|
|
424
|
+
// or file summary extraction), we don't want THAT inner conversation
|
|
425
|
+
// logged or its tool calls cached — it would create runaway recursion
|
|
426
|
+
// and pollute the thread with internal prompts.
|
|
426
427
|
if (process.env.STITCH_HOOKS_DISABLED === '1')
|
|
427
428
|
return;
|
|
428
429
|
let raw = '';
|
|
@@ -446,6 +447,16 @@ async function cmdHook(args) {
|
|
|
446
447
|
// (which is the directory `claude` was launched from).
|
|
447
448
|
const cwd = event?.cwd || process.cwd();
|
|
448
449
|
const threadName = inferThreadFor(cwd) || 'default';
|
|
450
|
+
// ── PreToolUse on Read: inject cached summary if hash matches ─────────
|
|
451
|
+
if (eventName === 'PreToolUse' && event?.tool_name === 'Read') {
|
|
452
|
+
await handlePreReadHook(cfg, event, cwd || process.cwd()).catch(() => { });
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
// ── PostToolUse on Read: opportunistically save a fresh summary ───────
|
|
456
|
+
if (eventName === 'PostToolUse' && event?.tool_name === 'Read') {
|
|
457
|
+
await handlePostReadHook(cfg, event, cwd || process.cwd()).catch(() => { });
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
449
460
|
// ── SessionStart: inject prior context (token-efficient) ─────────────
|
|
450
461
|
// Strategy: prefer distilled memories (dense facts) over raw turns. Only
|
|
451
462
|
// include raw turns for the last 5 to give the agent immediate continuation.
|
|
@@ -453,16 +464,30 @@ async function cmdHook(args) {
|
|
|
453
464
|
try {
|
|
454
465
|
const stitch = client(cfg);
|
|
455
466
|
const projectTag = (threadName.split('/')[0] || threadName).toLowerCase();
|
|
456
|
-
const [thread, memHits] = await Promise.all([
|
|
467
|
+
const [thread, memHits, workspaces, fileSummaries, aboutMems] = await Promise.all([
|
|
457
468
|
stitch.thread(threadName).recall({ last: 5 }).catch(() => ({ thread_id: '', recent: [], semantic: [] })),
|
|
458
469
|
stitch.recall(projectTag, { k: 8 }).catch(() => []),
|
|
470
|
+
stitch.workspaces.list().catch(() => []),
|
|
471
|
+
stitch.list({ limit: 12 }).then((all) => all.filter((m) => m.tags.some((t) => t.startsWith('file:')))).catch(() => []),
|
|
472
|
+
stitch.list({ tag: 'workspace:about', limit: 1 }).catch(() => []),
|
|
459
473
|
]);
|
|
474
|
+
const currentWs = Array.isArray(workspaces) && workspaces.length > 0 ? workspaces[0] : null;
|
|
460
475
|
const lines = [];
|
|
461
476
|
lines.push('<stitch-context>');
|
|
462
|
-
lines.push(`Project: ${threadName} ·
|
|
477
|
+
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
478
|
lines.push('');
|
|
479
|
+
// Nudge the AI to set a meaningful workspace name once.
|
|
480
|
+
if (currentWs?.name === 'default') {
|
|
481
|
+
lines.push('### ⚠ Workspace is still named "default"');
|
|
482
|
+
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).');
|
|
483
|
+
lines.push('');
|
|
484
|
+
}
|
|
485
|
+
if (Array.isArray(aboutMems) && aboutMems.length > 0) {
|
|
486
|
+
lines.push('### About this workspace');
|
|
487
|
+
lines.push(String(aboutMems[0].content || '').slice(0, 400));
|
|
488
|
+
lines.push('');
|
|
489
|
+
}
|
|
464
490
|
if (Array.isArray(memHits) && memHits.length > 0) {
|
|
465
|
-
// Prefer distilled (auto-tagged) facts and decisions; fall back to anything else
|
|
466
491
|
const sortedMems = [...memHits].sort((a, b) => Number(b.score ?? 0) - Number(a.score ?? 0)).slice(0, 8);
|
|
467
492
|
lines.push('### Durable memories for this project');
|
|
468
493
|
for (const m of sortedMems) {
|
|
@@ -472,6 +497,16 @@ async function cmdHook(args) {
|
|
|
472
497
|
}
|
|
473
498
|
lines.push('');
|
|
474
499
|
}
|
|
500
|
+
if (Array.isArray(fileSummaries) && fileSummaries.length > 0) {
|
|
501
|
+
lines.push('### File summaries (cached — call `file_summary` before reading)');
|
|
502
|
+
for (const m of fileSummaries.slice(0, 8)) {
|
|
503
|
+
const fileTag = m.tags.find((t) => t.startsWith('file:'));
|
|
504
|
+
const filePath = fileTag ? fileTag.slice(5) : '(unknown)';
|
|
505
|
+
const txt = String(m.content || '').replace(/\n+/g, ' ').slice(0, 250);
|
|
506
|
+
lines.push(`- **${filePath}** — ${txt}`);
|
|
507
|
+
}
|
|
508
|
+
lines.push('');
|
|
509
|
+
}
|
|
475
510
|
if (thread.recent && thread.recent.length > 0) {
|
|
476
511
|
lines.push('### Most recent turns (continue from here)');
|
|
477
512
|
for (const t of thread.recent.slice(-5)) {
|
|
@@ -480,7 +515,7 @@ async function cmdHook(args) {
|
|
|
480
515
|
}
|
|
481
516
|
lines.push('');
|
|
482
517
|
}
|
|
483
|
-
lines.push('Call `recall` for deeper search
|
|
518
|
+
lines.push('Call `recall` for deeper search, `thread_recall` for older turns, `file_summary` BEFORE reading any non-trivial file (saves tokens).');
|
|
484
519
|
lines.push('</stitch-context>');
|
|
485
520
|
process.stdout.write(lines.join('\n'));
|
|
486
521
|
}
|
|
@@ -549,6 +584,38 @@ function inferThreadFor(cwd) {
|
|
|
549
584
|
}
|
|
550
585
|
return path.basename(dir) || 'default';
|
|
551
586
|
}
|
|
587
|
+
/**
|
|
588
|
+
* Best-effort derive a workspace slug from a project directory:
|
|
589
|
+
* - package.json `name` (npm convention, slug-friendly already)
|
|
590
|
+
* - else folder basename, normalised to a lowercase slug
|
|
591
|
+
*
|
|
592
|
+
* The AI can later refine via the `workspace_setup` MCP tool — this is just
|
|
593
|
+
* the seed name so it isn't "default".
|
|
594
|
+
*/
|
|
595
|
+
function deriveWorkspaceName(cwd) {
|
|
596
|
+
// 1. package.json `name`.
|
|
597
|
+
try {
|
|
598
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
599
|
+
if (fs.existsSync(pkgPath)) {
|
|
600
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
601
|
+
if (typeof pkg.name === 'string' && pkg.name.trim()) {
|
|
602
|
+
// Strip leading scope (@org/) if any — keeps the slug clean.
|
|
603
|
+
const stripped = pkg.name.replace(/^@[^/]+\//, '').trim();
|
|
604
|
+
if (stripped)
|
|
605
|
+
return slugify(stripped).slice(0, 60);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
catch { /* fall through */ }
|
|
610
|
+
// 2. Folder basename.
|
|
611
|
+
return slugify(path.basename(cwd) || 'default').slice(0, 60) || 'default';
|
|
612
|
+
}
|
|
613
|
+
function slugify(s) {
|
|
614
|
+
return s
|
|
615
|
+
.toLowerCase()
|
|
616
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
617
|
+
.replace(/^-+|-+$/g, '') || 'default';
|
|
618
|
+
}
|
|
552
619
|
function findProjectRoot(cwd) {
|
|
553
620
|
let cur = cwd;
|
|
554
621
|
for (let i = 0; i < 8; i++) {
|
|
@@ -604,6 +671,223 @@ function lastAssistantTextFromTranscript(transcriptPath) {
|
|
|
604
671
|
catch { /* ignore */ }
|
|
605
672
|
return text;
|
|
606
673
|
}
|
|
674
|
+
// ─── File summary cache ────────────────────────────────────────────────────
|
|
675
|
+
//
|
|
676
|
+
// Goal: when Claude reads a non-trivial file we've seen before, surface our
|
|
677
|
+
// cached 2-3 sentence summary so the agent can decide whether to skim or
|
|
678
|
+
// re-read in full. When Claude reads a file we haven't summarized (or that's
|
|
679
|
+
// changed), kick off a background `claude -p` to extract a summary and save
|
|
680
|
+
// it as a memory tagged `file:<path>` + `hash:<sha-prefix>`.
|
|
681
|
+
//
|
|
682
|
+
// Trade-off: the cache is hash-bound, so any byte change invalidates the
|
|
683
|
+
// cached summary — fine for big stable files (docs, configs, third-party src),
|
|
684
|
+
// less useful for files Claude is actively editing. The PostToolUse hook
|
|
685
|
+
// re-summarizes after the next read, so the cache repopulates on its own.
|
|
686
|
+
const FILE_SUMMARY_MIN_BYTES = 500; // skip tiny files
|
|
687
|
+
const FILE_SUMMARY_MAX_BYTES = 200_000; // skip absolutely huge files
|
|
688
|
+
const FILE_SUMMARY_COOLDOWN_MS = 5 * 60_000; // per-file: don't re-summarize more than once per 5 min
|
|
689
|
+
const FILE_SUMMARY_PROMPT = `You are summarising a single source file for an AI coding assistant cache.
|
|
690
|
+
|
|
691
|
+
Return ONLY a 2-3 sentence summary describing:
|
|
692
|
+
• What the file does (purpose, the few exported symbols / commands / endpoints worth knowing).
|
|
693
|
+
• Any important pattern, convention, or non-obvious behaviour another agent would benefit from before reading the file in full.
|
|
694
|
+
|
|
695
|
+
Skip preamble. Skip "this file …". Be specific. No markdown fences. No prose around it.
|
|
696
|
+
|
|
697
|
+
File path: {{PATH}}
|
|
698
|
+
|
|
699
|
+
File content:
|
|
700
|
+
{{CONTENT}}
|
|
701
|
+
`;
|
|
702
|
+
function readFileContentFromHookEvent(event) {
|
|
703
|
+
// Claude Code hook 'tool_response' shape varies between versions; accept
|
|
704
|
+
// a few shapes. The Read tool's success path includes the file content
|
|
705
|
+
// somewhere in tool_response.
|
|
706
|
+
const r = event?.tool_response;
|
|
707
|
+
if (!r)
|
|
708
|
+
return '';
|
|
709
|
+
if (typeof r === 'string')
|
|
710
|
+
return r;
|
|
711
|
+
if (typeof r?.file?.content === 'string')
|
|
712
|
+
return r.file.content;
|
|
713
|
+
if (typeof r?.content === 'string')
|
|
714
|
+
return r.content;
|
|
715
|
+
if (Array.isArray(r?.content)) {
|
|
716
|
+
return r.content.map((c) => (typeof c === 'string' ? c : c?.text ?? '')).join('');
|
|
717
|
+
}
|
|
718
|
+
return '';
|
|
719
|
+
}
|
|
720
|
+
function relPathFor(cwd, abs) {
|
|
721
|
+
try {
|
|
722
|
+
return path.relative(cwd, abs) || abs;
|
|
723
|
+
}
|
|
724
|
+
catch {
|
|
725
|
+
return abs;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
async function sha256Hex(s) {
|
|
729
|
+
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(s));
|
|
730
|
+
return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
731
|
+
}
|
|
732
|
+
async function lookupFileSummary(stitch, relPath) {
|
|
733
|
+
try {
|
|
734
|
+
const list = await stitch.list({ tag: `file:${relPath}`, limit: 5 });
|
|
735
|
+
if (!list || list.length === 0)
|
|
736
|
+
return null;
|
|
737
|
+
const first = list[0];
|
|
738
|
+
const hashTag = first.tags.find((t) => t.startsWith('hash:'));
|
|
739
|
+
return {
|
|
740
|
+
id: first.id,
|
|
741
|
+
content: first.content,
|
|
742
|
+
hash: hashTag ? hashTag.slice(5) : null,
|
|
743
|
+
created_at: first.created_at,
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
catch {
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
// PreToolUse on Read: if we have a cached summary for this exact file path,
|
|
751
|
+
// emit Claude-Code-flavoured JSON to stdout that adds it as additional context.
|
|
752
|
+
async function handlePreReadHook(cfg, event, cwd) {
|
|
753
|
+
const filePath = String(event?.tool_input?.file_path || '');
|
|
754
|
+
if (!filePath)
|
|
755
|
+
return;
|
|
756
|
+
const rel = relPathFor(cwd, filePath);
|
|
757
|
+
const stitch = client(cfg);
|
|
758
|
+
const cached = await lookupFileSummary(stitch, rel);
|
|
759
|
+
if (!cached)
|
|
760
|
+
return;
|
|
761
|
+
const ageMin = Math.round((Date.now() - cached.created_at) / 60_000);
|
|
762
|
+
const note = [
|
|
763
|
+
`Stitch cache: a prior summary of ${rel} exists (${ageMin}m ago).`,
|
|
764
|
+
cached.hash ? `Cached hash prefix: ${cached.hash}.` : '',
|
|
765
|
+
`Summary: ${cached.content}`,
|
|
766
|
+
`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.`,
|
|
767
|
+
].filter(Boolean).join('\n');
|
|
768
|
+
// Claude Code accepts a JSON object on stdout for PreToolUse with
|
|
769
|
+
// hookSpecificOutput.additionalContext to inject text into the model's
|
|
770
|
+
// context for this tool call.
|
|
771
|
+
const payload = { hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: note } };
|
|
772
|
+
process.stdout.write(JSON.stringify(payload));
|
|
773
|
+
}
|
|
774
|
+
// PostToolUse on Read: kick off a background `claude -p` to summarize this
|
|
775
|
+
// file IF we don't already have a summary at the current hash. Fire-and-forget.
|
|
776
|
+
async function handlePostReadHook(cfg, event, cwd) {
|
|
777
|
+
const filePath = String(event?.tool_input?.file_path || '');
|
|
778
|
+
if (!filePath)
|
|
779
|
+
return;
|
|
780
|
+
const rel = relPathFor(cwd, filePath);
|
|
781
|
+
const content = readFileContentFromHookEvent(event);
|
|
782
|
+
// Fall back to reading from disk if hook payload didn't carry content.
|
|
783
|
+
let body = content;
|
|
784
|
+
if (!body) {
|
|
785
|
+
try {
|
|
786
|
+
body = fs.readFileSync(filePath, 'utf8');
|
|
787
|
+
}
|
|
788
|
+
catch {
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if (!body || body.length < FILE_SUMMARY_MIN_BYTES)
|
|
793
|
+
return;
|
|
794
|
+
if (body.length > FILE_SUMMARY_MAX_BYTES)
|
|
795
|
+
return;
|
|
796
|
+
const fullHash = await sha256Hex(body);
|
|
797
|
+
const hashPrefix = fullHash.slice(0, 16);
|
|
798
|
+
// Per-file cooldown so a hot-edited file doesn't re-summarize on every read.
|
|
799
|
+
const state = loadFileSummaryState();
|
|
800
|
+
const last = state.files[rel];
|
|
801
|
+
if (last && last.hash === hashPrefix && Date.now() - last.lastSummarizedAt < FILE_SUMMARY_COOLDOWN_MS)
|
|
802
|
+
return;
|
|
803
|
+
const stitch = client(cfg);
|
|
804
|
+
const cached = await lookupFileSummary(stitch, rel);
|
|
805
|
+
if (cached && cached.hash === hashPrefix) {
|
|
806
|
+
// Already summarized this exact version — refresh cooldown and bail.
|
|
807
|
+
state.files[rel] = { hash: hashPrefix, lastSummarizedAt: Date.now() };
|
|
808
|
+
saveFileSummaryState(state);
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
// Mark BEFORE spawning so a flurry of Read events doesn't fire N spawns.
|
|
812
|
+
state.files[rel] = { hash: hashPrefix, lastSummarizedAt: Date.now() };
|
|
813
|
+
saveFileSummaryState(state);
|
|
814
|
+
// Spawn detached: stitch _summarize-file <abs-path> <hash> <rel>
|
|
815
|
+
try {
|
|
816
|
+
const cliPath = process.argv[1] || (await import('node:url')).fileURLToPath(import.meta.url);
|
|
817
|
+
const child = spawn(process.argv[0], [cliPath, '_summarize-file', filePath, hashPrefix, rel], {
|
|
818
|
+
detached: true,
|
|
819
|
+
stdio: 'ignore',
|
|
820
|
+
env: { ...process.env, STITCH_HOOKS_DISABLED: '1' },
|
|
821
|
+
});
|
|
822
|
+
child.unref();
|
|
823
|
+
}
|
|
824
|
+
catch { /* ignore */ }
|
|
825
|
+
}
|
|
826
|
+
// File summary state: per-file cooldown so we don't re-fire on every Read.
|
|
827
|
+
const FILE_SUMMARY_STATE_FILE = path.join(CONFIG_DIR, 'file-summary-state.json');
|
|
828
|
+
function loadFileSummaryState() {
|
|
829
|
+
try {
|
|
830
|
+
return JSON.parse(fs.readFileSync(FILE_SUMMARY_STATE_FILE, 'utf8'));
|
|
831
|
+
}
|
|
832
|
+
catch {
|
|
833
|
+
return { files: {} };
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
function saveFileSummaryState(s) {
|
|
837
|
+
if (!fs.existsSync(CONFIG_DIR))
|
|
838
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
839
|
+
fs.writeFileSync(FILE_SUMMARY_STATE_FILE, JSON.stringify(s, null, 2));
|
|
840
|
+
}
|
|
841
|
+
// `stitch _summarize-file <abs> <hashPrefix> <rel>` — internal entry called
|
|
842
|
+
// from the detached PostToolUse spawn. Reads the file, asks claude -p for a
|
|
843
|
+
// 2-3 sentence summary, saves as a Stitch memory.
|
|
844
|
+
async function cmdSummarizeFile(args) {
|
|
845
|
+
const [abs, hashPrefix, rel] = args;
|
|
846
|
+
if (!abs || !hashPrefix || !rel)
|
|
847
|
+
return;
|
|
848
|
+
const cfg = loadConfig();
|
|
849
|
+
if (!cfg.apiKey)
|
|
850
|
+
return;
|
|
851
|
+
let body = '';
|
|
852
|
+
try {
|
|
853
|
+
body = fs.readFileSync(abs, 'utf8');
|
|
854
|
+
}
|
|
855
|
+
catch {
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
if (!body || body.length > FILE_SUMMARY_MAX_BYTES)
|
|
859
|
+
return;
|
|
860
|
+
const claudeBin = process.env.STITCH_CLAUDE_BIN || 'claude';
|
|
861
|
+
const prompt = FILE_SUMMARY_PROMPT
|
|
862
|
+
.replace('{{PATH}}', rel)
|
|
863
|
+
.replace('{{CONTENT}}', body);
|
|
864
|
+
const result = await runWithStdin(claudeBin, ['-p'], prompt, {
|
|
865
|
+
cwd: process.cwd(),
|
|
866
|
+
env: { STITCH_HOOKS_DISABLED: '1' },
|
|
867
|
+
});
|
|
868
|
+
if (result.exit_code !== 0)
|
|
869
|
+
return;
|
|
870
|
+
const summary = result.stdout.trim().slice(0, 2000);
|
|
871
|
+
if (!summary)
|
|
872
|
+
return;
|
|
873
|
+
try {
|
|
874
|
+
const stitch = client(cfg);
|
|
875
|
+
const projectTag = (inferThread() || 'default').split('/')[0];
|
|
876
|
+
// Replace any prior summary for this path.
|
|
877
|
+
const prior = await stitch.list({ tag: `file:${rel}`, limit: 50 }).catch(() => []);
|
|
878
|
+
for (const p of prior) {
|
|
879
|
+
try {
|
|
880
|
+
await stitch.forget(p.id);
|
|
881
|
+
}
|
|
882
|
+
catch { }
|
|
883
|
+
}
|
|
884
|
+
await stitch.remember(summary, {
|
|
885
|
+
kind: 'snippet',
|
|
886
|
+
tags: [`file:${rel}`, `hash:${hashPrefix}`, 'auto:file-summary', `project:${projectTag}`],
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
catch { /* silent — never break a session */ }
|
|
890
|
+
}
|
|
607
891
|
// ── stitch link — pin a project's thread name so it's the same on every machine
|
|
608
892
|
async function cmdLink(args) {
|
|
609
893
|
const positionals = positional(args);
|
|
@@ -1179,6 +1463,26 @@ async function cmdInstall(args) {
|
|
|
1179
1463
|
const ws = await stitch.resolveWorkspace();
|
|
1180
1464
|
const baseUrl = cfg.baseUrl || 'https://db.stitchdb.com';
|
|
1181
1465
|
const mcpUrl = `${baseUrl}/mcp/v1/${ws}`;
|
|
1466
|
+
// 0. If the workspace is still called "default", upgrade its name to a
|
|
1467
|
+
// project-derived slug so memories surface under a meaningful identity.
|
|
1468
|
+
// The AI can later refine it via the workspace_setup MCP tool.
|
|
1469
|
+
if (!hasFlag(args, ['--no-rename-workspace'])) {
|
|
1470
|
+
try {
|
|
1471
|
+
const current = await stitch.workspaces.get(ws);
|
|
1472
|
+
if (current.name === 'default') {
|
|
1473
|
+
const derived = deriveWorkspaceName(process.cwd());
|
|
1474
|
+
if (derived && derived !== 'default') {
|
|
1475
|
+
process.stdout.write(`• Naming workspace "${derived}" (was "default")… `);
|
|
1476
|
+
await stitch.workspaces.update(ws, { name: derived });
|
|
1477
|
+
console.log('ok');
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
catch (e) {
|
|
1482
|
+
// Older deployments may not have the PATCH endpoint yet.
|
|
1483
|
+
console.log(`• Workspace rename skipped (${e?.message || e}).`);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1182
1486
|
const noMcp = hasFlag(args, ['--no-mcp']);
|
|
1183
1487
|
const noHooks = hasFlag(args, ['--no-hooks']);
|
|
1184
1488
|
const noClaudeMd = hasFlag(args, ['--no-claude-md']);
|
|
@@ -1197,9 +1501,10 @@ async function cmdInstall(args) {
|
|
|
1197
1501
|
else
|
|
1198
1502
|
console.log(`failed (${stderr.trim().slice(0, 120)})`);
|
|
1199
1503
|
}
|
|
1200
|
-
// 2. Hooks: auto-log every turn + auto-inject prior context at session start
|
|
1504
|
+
// 2. Hooks: auto-log every turn + auto-inject prior context at session start +
|
|
1505
|
+
// file-summary cache (Pre/PostToolUse on Read).
|
|
1201
1506
|
if (!noHooks) {
|
|
1202
|
-
process.stdout.write('• Wiring hooks (SessionStart + UserPromptSubmit + Stop)… ');
|
|
1507
|
+
process.stdout.write('• Wiring hooks (SessionStart + UserPromptSubmit + Stop + Pre/PostToolUse:Read)… ');
|
|
1203
1508
|
try {
|
|
1204
1509
|
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
1205
1510
|
const existing = fs.existsSync(settingsPath)
|
|
@@ -1209,6 +1514,8 @@ async function cmdInstall(args) {
|
|
|
1209
1514
|
existing.hooks.SessionStart = mergeHook(existing.hooks.SessionStart, STITCH_SESSION_START_HOOK);
|
|
1210
1515
|
existing.hooks.UserPromptSubmit = mergeHook(existing.hooks.UserPromptSubmit, STITCH_USER_HOOK);
|
|
1211
1516
|
existing.hooks.Stop = mergeHook(existing.hooks.Stop, STITCH_STOP_HOOK);
|
|
1517
|
+
existing.hooks.PreToolUse = mergeHook(existing.hooks.PreToolUse, STITCH_PRE_READ_HOOK);
|
|
1518
|
+
existing.hooks.PostToolUse = mergeHook(existing.hooks.PostToolUse, STITCH_POST_READ_HOOK);
|
|
1212
1519
|
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
1213
1520
|
fs.writeFileSync(settingsPath, JSON.stringify(existing, null, 2));
|
|
1214
1521
|
console.log('ok');
|
|
@@ -1290,10 +1597,11 @@ async function cmdInstall(args) {
|
|
|
1290
1597
|
}
|
|
1291
1598
|
console.log();
|
|
1292
1599
|
console.log('Done. Open a fresh `claude` session — Stitch is wired.');
|
|
1293
|
-
console.log(' •
|
|
1600
|
+
console.log(' • workspace_setup, recall, remember, thread tools are available');
|
|
1294
1601
|
console.log(' • every turn auto-logs to the thread for this repo');
|
|
1295
1602
|
console.log(' • Claude auto-pulls past context at session start (per CLAUDE.md)');
|
|
1296
1603
|
console.log(' • Claude memory files are mirrored to Stitch');
|
|
1604
|
+
console.log(' • file Reads auto-cache 2-3 sentence summaries; Pre-Read injects them');
|
|
1297
1605
|
console.log();
|
|
1298
1606
|
console.log('Run `stitch sync --watch` to keep them in sync as Claude updates them.');
|
|
1299
1607
|
}
|
|
@@ -1313,6 +1621,15 @@ const STITCH_SESSION_START_HOOK = {
|
|
|
1313
1621
|
matcher: '*',
|
|
1314
1622
|
hooks: [{ type: 'command', command: 'stitch _hook SessionStart' }],
|
|
1315
1623
|
};
|
|
1624
|
+
// File summary cache: only fires for the Read tool, both pre and post.
|
|
1625
|
+
const STITCH_PRE_READ_HOOK = {
|
|
1626
|
+
matcher: 'Read',
|
|
1627
|
+
hooks: [{ type: 'command', command: 'stitch _hook' }],
|
|
1628
|
+
};
|
|
1629
|
+
const STITCH_POST_READ_HOOK = {
|
|
1630
|
+
matcher: 'Read',
|
|
1631
|
+
hooks: [{ type: 'command', command: 'stitch _hook' }],
|
|
1632
|
+
};
|
|
1316
1633
|
function mergeHook(existing, entry) {
|
|
1317
1634
|
const arr = Array.isArray(existing) ? existing.slice() : [];
|
|
1318
1635
|
// Replace any earlier Stitch entry; identify by the marker.
|
|
@@ -1570,6 +1887,7 @@ async function main(argv) {
|
|
|
1570
1887
|
case 'link': return cmdLink(rest);
|
|
1571
1888
|
case 'distill': return cmdDistill(rest);
|
|
1572
1889
|
case '_hook': return cmdHook(rest);
|
|
1890
|
+
case '_summarize-file': return cmdSummarizeFile(rest);
|
|
1573
1891
|
case 'update':
|
|
1574
1892
|
case 'upgrade': return cmdUpdate(rest);
|
|
1575
1893
|
case 'version':
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stitchdb/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
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",
|