claude-eidetic 0.1.1 → 0.1.2

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 (96) hide show
  1. package/README.md +333 -0
  2. package/dist/config.d.ts +25 -0
  3. package/dist/config.js +29 -10
  4. package/dist/core/cleanup.d.ts +8 -0
  5. package/dist/core/cleanup.js +41 -0
  6. package/dist/core/doc-indexer.d.ts +13 -0
  7. package/dist/core/doc-indexer.js +76 -0
  8. package/dist/core/doc-searcher.d.ts +13 -0
  9. package/dist/core/doc-searcher.js +65 -0
  10. package/dist/core/file-category.d.ts +7 -0
  11. package/dist/core/file-category.js +75 -0
  12. package/dist/core/indexer.js +12 -4
  13. package/dist/core/preview.d.ts +1 -2
  14. package/dist/core/preview.js +2 -5
  15. package/dist/core/repo-map.d.ts +33 -0
  16. package/dist/core/repo-map.js +144 -0
  17. package/dist/core/searcher.d.ts +1 -13
  18. package/dist/core/searcher.js +20 -24
  19. package/dist/core/snapshot-io.js +2 -2
  20. package/dist/core/sync.d.ts +5 -25
  21. package/dist/core/sync.js +90 -65
  22. package/dist/core/targeted-indexer.d.ts +19 -0
  23. package/dist/core/targeted-indexer.js +127 -0
  24. package/dist/embedding/factory.d.ts +0 -13
  25. package/dist/embedding/factory.js +0 -17
  26. package/dist/embedding/openai.d.ts +2 -14
  27. package/dist/embedding/openai.js +7 -20
  28. package/dist/errors.d.ts +2 -0
  29. package/dist/errors.js +2 -0
  30. package/dist/format.d.ts +12 -0
  31. package/dist/format.js +160 -31
  32. package/dist/hooks/post-tool-use.d.ts +13 -0
  33. package/dist/hooks/post-tool-use.js +113 -0
  34. package/dist/hooks/stop-hook.d.ts +11 -0
  35. package/dist/hooks/stop-hook.js +121 -0
  36. package/dist/hooks/targeted-runner.d.ts +11 -0
  37. package/dist/hooks/targeted-runner.js +66 -0
  38. package/dist/index.js +68 -9
  39. package/dist/infra/qdrant-bootstrap.js +14 -12
  40. package/dist/memory/history.d.ts +19 -0
  41. package/dist/memory/history.js +40 -0
  42. package/dist/memory/llm.d.ts +2 -0
  43. package/dist/memory/llm.js +56 -0
  44. package/dist/memory/prompts.d.ts +5 -0
  45. package/dist/memory/prompts.js +36 -0
  46. package/dist/memory/reconciler.d.ts +12 -0
  47. package/dist/memory/reconciler.js +36 -0
  48. package/dist/memory/store.d.ts +20 -0
  49. package/dist/memory/store.js +206 -0
  50. package/dist/memory/types.d.ts +28 -0
  51. package/dist/memory/types.js +2 -0
  52. package/dist/paths.d.ts +3 -4
  53. package/dist/paths.js +14 -4
  54. package/dist/precompact/hook.d.ts +9 -0
  55. package/dist/precompact/hook.js +170 -0
  56. package/dist/precompact/index-runner.d.ts +9 -0
  57. package/dist/precompact/index-runner.js +52 -0
  58. package/dist/precompact/note-writer.d.ts +15 -0
  59. package/dist/precompact/note-writer.js +109 -0
  60. package/dist/precompact/session-indexer.d.ts +13 -0
  61. package/dist/precompact/session-indexer.js +31 -0
  62. package/dist/precompact/tier0-inject.d.ts +16 -0
  63. package/dist/precompact/tier0-inject.js +88 -0
  64. package/dist/precompact/tier0-writer.d.ts +16 -0
  65. package/dist/precompact/tier0-writer.js +74 -0
  66. package/dist/precompact/transcript-parser.d.ts +10 -0
  67. package/dist/precompact/transcript-parser.js +148 -0
  68. package/dist/precompact/types.d.ts +93 -0
  69. package/dist/precompact/types.js +5 -0
  70. package/dist/precompact/utils.d.ts +29 -0
  71. package/dist/precompact/utils.js +95 -0
  72. package/dist/setup-message.d.ts +2 -2
  73. package/dist/setup-message.js +39 -20
  74. package/dist/splitter/ast.js +84 -22
  75. package/dist/splitter/line.d.ts +0 -4
  76. package/dist/splitter/line.js +1 -7
  77. package/dist/splitter/symbol-extract.d.ts +16 -0
  78. package/dist/splitter/symbol-extract.js +61 -0
  79. package/dist/splitter/types.d.ts +5 -0
  80. package/dist/splitter/types.js +1 -1
  81. package/dist/state/doc-metadata.d.ts +18 -0
  82. package/dist/state/doc-metadata.js +59 -0
  83. package/dist/state/registry.d.ts +1 -3
  84. package/dist/state/snapshot.d.ts +0 -1
  85. package/dist/state/snapshot.js +3 -19
  86. package/dist/tool-schemas.d.ts +251 -1
  87. package/dist/tool-schemas.js +307 -0
  88. package/dist/tools.d.ts +69 -0
  89. package/dist/tools.js +286 -17
  90. package/dist/vectordb/milvus.d.ts +7 -5
  91. package/dist/vectordb/milvus.js +116 -19
  92. package/dist/vectordb/qdrant.d.ts +8 -10
  93. package/dist/vectordb/qdrant.js +105 -33
  94. package/dist/vectordb/types.d.ts +20 -0
  95. package/messages.yaml +50 -0
  96. package/package.json +31 -6
package/dist/format.js CHANGED
@@ -2,26 +2,54 @@ import { listProjects } from './state/registry.js';
2
2
  export function textResult(text) {
3
3
  return { content: [{ type: 'text', text }] };
4
4
  }
5
+ export function formatCleanupResult(result, normalizedPath, dryRun) {
6
+ if (dryRun) {
7
+ if (result.removedFiles.length === 0) {
8
+ return `Dry run for ${normalizedPath}: no orphaned vectors found.`;
9
+ }
10
+ const lines = [
11
+ `Dry run for ${normalizedPath}: ${result.removedFiles.length} file(s) would be cleaned:`,
12
+ '',
13
+ ];
14
+ for (const f of result.removedFiles) {
15
+ lines.push(` - ${f}`);
16
+ }
17
+ return lines.join('\n');
18
+ }
19
+ if (result.totalRemoved === 0) {
20
+ return `Cleanup complete for ${normalizedPath}: no orphaned vectors found.`;
21
+ }
22
+ const lines = [
23
+ `Cleanup complete for ${normalizedPath}`,
24
+ '',
25
+ ` Removed: ${result.totalRemoved} file(s)`,
26
+ ` Duration: ${(result.durationMs / 1000).toFixed(1)}s`,
27
+ '',
28
+ 'Cleaned files:',
29
+ ];
30
+ for (const f of result.removedFiles) {
31
+ lines.push(` - ${f}`);
32
+ }
33
+ return lines.join('\n');
34
+ }
5
35
  export function formatIndexResult(result, normalizedPath) {
6
36
  const lines = [
7
37
  `Indexing complete for ${normalizedPath}`,
8
38
  '',
9
- `| Metric | Value |`,
10
- `|--------|-------|`,
11
- `| Total files | ${result.totalFiles} |`,
12
- `| Total chunks | ${result.totalChunks} |`,
13
- `| Added files | ${result.addedFiles} |`,
14
- `| Modified files | ${result.modifiedFiles} |`,
15
- `| Removed files | ${result.removedFiles} |`,
16
- `| Skipped (unchanged) | ${result.skippedFiles} |`,
17
- `| Parse failures | ${result.parseFailures.length} |`,
18
- `| Estimated tokens | ~${(result.estimatedTokens / 1000).toFixed(0)}K |`,
19
- `| Estimated cost | $${result.estimatedCostUsd.toFixed(4)} |`,
20
- `| Duration | ${(result.durationMs / 1000).toFixed(1)}s |`,
39
+ ` Total files: ${result.totalFiles}`,
40
+ ` Total chunks: ${result.totalChunks}`,
41
+ ` Added: ${result.addedFiles}`,
42
+ ` Modified: ${result.modifiedFiles}`,
43
+ ` Removed: ${result.removedFiles}`,
44
+ ` Skipped: ${result.skippedFiles}`,
45
+ ` Parse failures: ${result.parseFailures.length}`,
46
+ ` Tokens: ~${(result.estimatedTokens / 1000).toFixed(0)}K`,
47
+ ` Cost: $${result.estimatedCostUsd.toFixed(4)}`,
48
+ ` Duration: ${(result.durationMs / 1000).toFixed(1)}s`,
21
49
  ];
22
50
  if (result.parseFailures.length > 0) {
23
51
  lines.push('');
24
- lines.push('**Parse Failures:**');
52
+ lines.push('Parse failures:');
25
53
  const toShow = result.parseFailures.slice(0, 10);
26
54
  for (const file of toShow) {
27
55
  lines.push(`- ${file}`);
@@ -34,18 +62,14 @@ export function formatIndexResult(result, normalizedPath) {
34
62
  }
35
63
  export function formatPreview(preview, rootPath) {
36
64
  const lines = [`Preview for ${rootPath}:`, ''];
37
- // Extension table
38
- const sorted = Object.entries(preview.byExtension)
39
- .sort((a, b) => b[1] - a[1]);
65
+ const sorted = Object.entries(preview.byExtension).sort((a, b) => b[1] - a[1]);
40
66
  if (sorted.length > 0) {
41
- lines.push('| Extension | Files |');
42
- lines.push('|-----------|-------|');
67
+ const maxExt = Math.max(...sorted.map(([ext]) => ext.length));
43
68
  for (const [ext, count] of sorted) {
44
- lines.push(`| ${ext} | ${count.toLocaleString()} |`);
69
+ lines.push(` ${ext.padEnd(maxExt)} ${count.toLocaleString()}`);
45
70
  }
46
71
  }
47
72
  lines.push(`Total: ${preview.totalFiles.toLocaleString()} files`, '');
48
- // Top directories
49
73
  if (preview.topDirectories.length > 0) {
50
74
  lines.push('Top directories:');
51
75
  for (const { dir, count } of preview.topDirectories) {
@@ -53,12 +77,10 @@ export function formatPreview(preview, rootPath) {
53
77
  }
54
78
  lines.push('');
55
79
  }
56
- // Cost estimate
57
80
  const tokenStr = preview.estimatedTokens >= 1_000_000
58
81
  ? `~${(preview.estimatedTokens / 1_000_000).toFixed(1)}M`
59
82
  : `~${(preview.estimatedTokens / 1000).toFixed(0)}K`;
60
83
  lines.push(`Estimated: ${tokenStr} tokens (~$${preview.estimatedCostUsd.toFixed(4)})`, '');
61
- // Warnings
62
84
  lines.push('Warnings:');
63
85
  if (preview.warnings.length === 0) {
64
86
  lines.push('- None');
@@ -73,23 +95,130 @@ export function formatPreview(preview, rootPath) {
73
95
  export function formatListIndexed(states) {
74
96
  const registry = listProjects();
75
97
  const pathToProject = new Map(Object.entries(registry).map(([name, p]) => [p, name]));
76
- const lines = [`## Indexed Codebases (${states.length})\n`];
98
+ const lines = [`Indexed Codebases (${states.length})`, ''];
77
99
  for (const s of states) {
78
100
  const projectName = pathToProject.get(s.path);
79
- const heading = projectName ? `${s.path} (project: \`${projectName}\`)` : s.path;
80
- lines.push(`### ${heading}`);
81
- lines.push(`- **Status:** ${s.status}`);
101
+ const heading = projectName ? `${s.path} (project: ${projectName})` : s.path;
102
+ lines.push(heading);
103
+ lines.push(` Status: ${s.status}`);
82
104
  if (s.totalFiles)
83
- lines.push(`- **Files:** ${s.totalFiles}`);
105
+ lines.push(` Files: ${s.totalFiles}`);
84
106
  if (s.totalChunks)
85
- lines.push(`- **Chunks:** ${s.totalChunks}`);
107
+ lines.push(` Chunks: ${s.totalChunks}`);
86
108
  if (s.lastIndexed)
87
- lines.push(`- **Last indexed:** ${s.lastIndexed}`);
109
+ lines.push(` Last indexed: ${s.lastIndexed}`);
88
110
  if (s.status === 'indexing' && s.progress !== undefined) {
89
- lines.push(`- **Progress:** ${s.progress}% — ${s.progressMessage ?? ''}`);
111
+ lines.push(` Progress: ${s.progress}% — ${s.progressMessage ?? ''}`);
90
112
  }
91
113
  if (s.error)
92
- lines.push(`- **Error:** ${s.error}`);
114
+ lines.push(` Error: ${s.error}`);
115
+ lines.push('');
116
+ }
117
+ return lines.join('\n');
118
+ }
119
+ export function formatDocIndexResult(result) {
120
+ const lines = [
121
+ `Documentation cached: ${result.library}/${result.topic}`,
122
+ '',
123
+ ` Source: ${result.source}`,
124
+ ` Chunks: ${result.totalChunks}`,
125
+ ` Tokens: ~${(result.estimatedTokens / 1000).toFixed(0)}K`,
126
+ ` Duration: ${(result.durationMs / 1000).toFixed(1)}s`,
127
+ ` Collection: ${result.collectionName}`,
128
+ '',
129
+ `Use \`search_documents(query="...", library="${result.library}")\` to search this documentation.`,
130
+ ];
131
+ return lines.join('\n');
132
+ }
133
+ export function formatDocSearchResults(results, query) {
134
+ if (results.length === 0) {
135
+ return `No cached documentation found for "${query}".`;
136
+ }
137
+ const lines = [`Found ${results.length} result(s) for "${query}" in cached docs:\n`];
138
+ for (let i = 0; i < results.length; i++) {
139
+ const r = results[i];
140
+ const staleTag = r.stale ? ' **[STALE]**' : '';
141
+ lines.push(`### Result ${i + 1}`);
142
+ lines.push(`**Library:** ${r.library}/${r.topic} | **Source:** ${r.source}${staleTag}`);
143
+ lines.push(`**Score:** ${r.score.toFixed(4)}`);
144
+ lines.push('```markdown');
145
+ lines.push(r.content);
146
+ lines.push('```');
147
+ lines.push('');
148
+ }
149
+ return lines.join('\n');
150
+ }
151
+ export function formatMemoryActions(actions) {
152
+ if (actions.length === 0) {
153
+ return 'No new facts extracted from the provided content.';
154
+ }
155
+ const lines = [`Processed ${actions.length} memory action(s):`, ''];
156
+ for (const action of actions) {
157
+ const icon = action.event === 'ADD' ? '+' : '~';
158
+ lines.push(` ${icon} [${action.event}] ${action.memory}`);
159
+ lines.push(` Category: ${action.category ?? 'unknown'} | ID: ${action.id}`);
160
+ if (action.previous) {
161
+ lines.push(` Previous: ${action.previous}`);
162
+ }
163
+ }
164
+ return lines.join('\n');
165
+ }
166
+ export function formatMemorySearchResults(items, query) {
167
+ if (items.length === 0) {
168
+ return `No memories found for "${query}".`;
169
+ }
170
+ const lines = [`Found ${items.length} memory(ies) for "${query}":\n`];
171
+ for (let i = 0; i < items.length; i++) {
172
+ const m = items[i];
173
+ lines.push(`${i + 1}. ${m.memory}`);
174
+ lines.push(` Category: ${m.category} | ID: ${m.id}`);
175
+ if (m.source)
176
+ lines.push(` Source: ${m.source}`);
177
+ if (m.created_at || m.updated_at) {
178
+ lines.push(` Created: ${m.created_at || 'unknown'} | Updated: ${m.updated_at || 'unknown'}`);
179
+ }
180
+ lines.push('');
181
+ }
182
+ return lines.join('\n');
183
+ }
184
+ export function formatMemoryList(items) {
185
+ if (items.length === 0) {
186
+ return 'No memories stored yet. Use `add_memory` to store developer knowledge.';
187
+ }
188
+ const lines = [`Stored Memories (${items.length}):\n`];
189
+ const grouped = new Map();
190
+ for (const m of items) {
191
+ const cat = m.category || 'uncategorized';
192
+ let catList = grouped.get(cat);
193
+ if (!catList) {
194
+ catList = [];
195
+ grouped.set(cat, catList);
196
+ }
197
+ catList.push(m);
198
+ }
199
+ for (const [category, memories] of grouped) {
200
+ lines.push(`### ${category} (${memories.length})`);
201
+ for (const m of memories) {
202
+ const updatedDate = m.updated_at ? ` (updated: ${m.updated_at.slice(0, 10)})` : '';
203
+ lines.push(` - ${m.memory} [${m.id.slice(0, 8)}...]${updatedDate}`);
204
+ }
205
+ lines.push('');
206
+ }
207
+ return lines.join('\n');
208
+ }
209
+ export function formatMemoryHistory(entries, memoryId) {
210
+ if (entries.length === 0) {
211
+ return `No history found for memory ${memoryId}.`;
212
+ }
213
+ const lines = [`History for memory ${memoryId} (${entries.length} event(s)):\n`];
214
+ for (const e of entries) {
215
+ lines.push(` [${e.event}] ${e.created_at}`);
216
+ if (e.new_value)
217
+ lines.push(` Value: ${e.new_value}`);
218
+ if (e.previous_value)
219
+ lines.push(` Previous: ${e.previous_value}`);
220
+ if (e.source)
221
+ lines.push(` Source: ${e.source}`);
93
222
  lines.push('');
94
223
  }
95
224
  return lines.join('\n');
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PostToolUse hook entry point.
4
+ *
5
+ * Receives hook data via stdin when Write or Edit tools are used.
6
+ * Maintains a shadow git index per session to track which files were
7
+ * modified, without touching HEAD or the working index.
8
+ *
9
+ * Shadow index: <git-dir>/claude/indexes/<session-id>/index
10
+ * Base commit: <git-dir>/claude/indexes/<session-id>/base_commit
11
+ */
12
+ export {};
13
+ //# sourceMappingURL=post-tool-use.d.ts.map
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PostToolUse hook entry point.
4
+ *
5
+ * Receives hook data via stdin when Write or Edit tools are used.
6
+ * Maintains a shadow git index per session to track which files were
7
+ * modified, without touching HEAD or the working index.
8
+ *
9
+ * Shadow index: <git-dir>/claude/indexes/<session-id>/index
10
+ * Base commit: <git-dir>/claude/indexes/<session-id>/base_commit
11
+ */
12
+ import { z } from 'zod';
13
+ import path from 'node:path';
14
+ import fs from 'node:fs';
15
+ import { execFileSync } from 'node:child_process';
16
+ const PostToolUseInputSchema = z.object({
17
+ session_id: z.string(),
18
+ cwd: z.string(),
19
+ hook_event_name: z.literal('PostToolUse'),
20
+ tool_name: z.string(),
21
+ tool_input: z
22
+ .object({
23
+ file_path: z.string().optional(),
24
+ })
25
+ .passthrough(),
26
+ });
27
+ async function readStdin() {
28
+ const chunks = [];
29
+ for await (const chunk of process.stdin) {
30
+ chunks.push(chunk);
31
+ }
32
+ return Buffer.concat(chunks).toString('utf-8');
33
+ }
34
+ function outputSuccess() {
35
+ process.stdout.write(JSON.stringify({ hookSpecificOutput: {} }));
36
+ }
37
+ function outputError(message) {
38
+ process.stderr.write(`[eidetic:post-tool-use] ${message}\n`);
39
+ process.stdout.write(JSON.stringify({ hookSpecificOutput: {} }));
40
+ }
41
+ async function main() {
42
+ try {
43
+ const input = await readStdin();
44
+ const parseResult = PostToolUseInputSchema.safeParse(JSON.parse(input));
45
+ if (!parseResult.success) {
46
+ outputError(`Invalid hook input: ${parseResult.error.message}`);
47
+ return;
48
+ }
49
+ const { session_id, cwd, tool_name, tool_input } = parseResult.data;
50
+ // Safety check beyond matcher — only process Write and Edit
51
+ if (tool_name !== 'Write' && tool_name !== 'Edit') {
52
+ outputSuccess();
53
+ return;
54
+ }
55
+ const filePath = tool_input.file_path;
56
+ if (!filePath) {
57
+ outputSuccess();
58
+ return;
59
+ }
60
+ // Resolve git dir
61
+ let gitDir;
62
+ try {
63
+ gitDir = execFileSync('git', ['-C', cwd, 'rev-parse', '--git-dir'], {
64
+ encoding: 'utf-8',
65
+ timeout: 5000,
66
+ }).trim();
67
+ }
68
+ catch {
69
+ // Not a git repo — silently skip
70
+ outputSuccess();
71
+ return;
72
+ }
73
+ if (!path.isAbsolute(gitDir)) {
74
+ gitDir = path.resolve(cwd, gitDir);
75
+ }
76
+ const shadowDir = path.join(gitDir, 'claude', 'indexes', session_id);
77
+ const shadowIndex = path.join(shadowDir, 'index');
78
+ const baseCommitFile = path.join(shadowDir, 'base_commit');
79
+ // First call for this session: seed shadow index from HEAD
80
+ if (!fs.existsSync(shadowIndex)) {
81
+ fs.mkdirSync(shadowDir, { recursive: true });
82
+ let headSha;
83
+ try {
84
+ headSha = execFileSync('git', ['-C', cwd, 'rev-parse', 'HEAD'], {
85
+ encoding: 'utf-8',
86
+ timeout: 5000,
87
+ }).trim();
88
+ }
89
+ catch {
90
+ // Empty repo / no commits — cannot seed from HEAD
91
+ outputSuccess();
92
+ return;
93
+ }
94
+ // Seed shadow index from HEAD tree
95
+ execFileSync('git', ['-C', cwd, 'read-tree', `--index-output=${shadowIndex}`, 'HEAD'], {
96
+ timeout: 5000,
97
+ });
98
+ fs.writeFileSync(baseCommitFile, headSha, 'utf-8');
99
+ }
100
+ // Stage the file into the shadow index
101
+ const absoluteFilePath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
102
+ execFileSync('git', ['-C', cwd, 'add', absoluteFilePath], {
103
+ env: { ...process.env, GIT_INDEX_FILE: shadowIndex },
104
+ timeout: 5000,
105
+ });
106
+ outputSuccess();
107
+ }
108
+ catch (err) {
109
+ outputError(err instanceof Error ? err.message : String(err));
110
+ }
111
+ }
112
+ void main();
113
+ //# sourceMappingURL=post-tool-use.js.map
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Stop hook entry point.
4
+ *
5
+ * Receives hook data via stdin when a Claude session ends.
6
+ * Commits the session's shadow git index to refs/heads/claude/<session-id>,
7
+ * diffs against base commit to find modified files, then spawns a
8
+ * detached background targeted re-indexer.
9
+ */
10
+ export {};
11
+ //# sourceMappingURL=stop-hook.d.ts.map
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Stop hook entry point.
4
+ *
5
+ * Receives hook data via stdin when a Claude session ends.
6
+ * Commits the session's shadow git index to refs/heads/claude/<session-id>,
7
+ * diffs against base commit to find modified files, then spawns a
8
+ * detached background targeted re-indexer.
9
+ */
10
+ import { z } from 'zod';
11
+ import path from 'node:path';
12
+ import fs from 'node:fs';
13
+ import os from 'node:os';
14
+ import { execFileSync, spawn } from 'node:child_process';
15
+ import { fileURLToPath } from 'node:url';
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = path.dirname(__filename);
18
+ const StopInputSchema = z.object({
19
+ session_id: z.string(),
20
+ cwd: z.string(),
21
+ hook_event_name: z.literal('Stop'),
22
+ stop_hook_active: z.boolean().optional(),
23
+ });
24
+ async function readStdin() {
25
+ const chunks = [];
26
+ for await (const chunk of process.stdin) {
27
+ chunks.push(chunk);
28
+ }
29
+ return Buffer.concat(chunks).toString('utf-8');
30
+ }
31
+ function outputSuccess() {
32
+ process.stdout.write(JSON.stringify({ hookSpecificOutput: {} }));
33
+ }
34
+ function outputError(message) {
35
+ process.stderr.write(`[eidetic:stop-hook] ${message}\n`);
36
+ process.stdout.write(JSON.stringify({ hookSpecificOutput: {} }));
37
+ }
38
+ async function main() {
39
+ try {
40
+ const input = await readStdin();
41
+ const parseResult = StopInputSchema.safeParse(JSON.parse(input));
42
+ if (!parseResult.success) {
43
+ outputError(`Invalid hook input: ${parseResult.error.message}`);
44
+ return;
45
+ }
46
+ const { session_id, cwd } = parseResult.data;
47
+ // Resolve git dir
48
+ let gitDir;
49
+ try {
50
+ gitDir = execFileSync('git', ['-C', cwd, 'rev-parse', '--git-dir'], {
51
+ encoding: 'utf-8',
52
+ timeout: 5000,
53
+ }).trim();
54
+ }
55
+ catch {
56
+ // Not a git repo — nothing to do
57
+ outputSuccess();
58
+ return;
59
+ }
60
+ if (!path.isAbsolute(gitDir)) {
61
+ gitDir = path.resolve(cwd, gitDir);
62
+ }
63
+ const shadowDir = path.join(gitDir, 'claude', 'indexes', session_id);
64
+ const shadowIndex = path.join(shadowDir, 'index');
65
+ const baseCommitFile = path.join(shadowDir, 'base_commit');
66
+ // No shadow index means no edits happened this session
67
+ if (!fs.existsSync(shadowIndex)) {
68
+ outputSuccess();
69
+ return;
70
+ }
71
+ const baseCommit = fs.readFileSync(baseCommitFile, 'utf-8').trim();
72
+ // Write tree from shadow index
73
+ const treeSha = execFileSync('git', ['-C', cwd, 'write-tree'], {
74
+ env: { ...process.env, GIT_INDEX_FILE: shadowIndex },
75
+ encoding: 'utf-8',
76
+ timeout: 10000,
77
+ }).trim();
78
+ // Create a commit object pointing to that tree
79
+ const commitSha = execFileSync('git', ['-C', cwd, 'commit-tree', treeSha, '-p', baseCommit, '-m', `eidetic: session ${session_id}`], { encoding: 'utf-8', timeout: 10000 }).trim();
80
+ // Store under refs/heads/claude/<session-id> for history
81
+ execFileSync('git', ['-C', cwd, 'update-ref', `refs/heads/claude/${session_id}`, commitSha], {
82
+ timeout: 5000,
83
+ });
84
+ // Find files that changed between base and new commit
85
+ const diffOutput = execFileSync('git', ['-C', cwd, 'diff-tree', '--no-commit-id', '--name-only', '-r', baseCommit, commitSha], { encoding: 'utf-8', timeout: 5000 }).trim();
86
+ const modifiedFiles = diffOutput
87
+ .split('\n')
88
+ .map((f) => f.trim())
89
+ .filter((f) => f.length > 0);
90
+ // Clean up shadow index directory
91
+ try {
92
+ fs.rmSync(shadowDir, { recursive: true, force: true });
93
+ }
94
+ catch (err) {
95
+ process.stderr.write(`[eidetic:stop-hook] Failed to clean shadow index: ${String(err)}\n`);
96
+ }
97
+ if (modifiedFiles.length === 0) {
98
+ outputSuccess();
99
+ return;
100
+ }
101
+ // Write manifest for targeted runner
102
+ const manifest = { projectPath: cwd, modifiedFiles };
103
+ const manifestFile = path.join(os.tmpdir(), `eidetic-reindex-${session_id}.json`);
104
+ fs.writeFileSync(manifestFile, JSON.stringify(manifest), 'utf-8');
105
+ // Spawn detached background targeted runner
106
+ const runnerPath = path.join(__dirname, 'targeted-runner.js');
107
+ const child = spawn(process.execPath, [runnerPath, manifestFile], {
108
+ detached: true,
109
+ stdio: 'ignore',
110
+ env: process.env,
111
+ windowsHide: true,
112
+ });
113
+ child.unref();
114
+ outputSuccess();
115
+ }
116
+ catch (err) {
117
+ outputError(err instanceof Error ? err.message : String(err));
118
+ }
119
+ }
120
+ void main();
121
+ //# sourceMappingURL=stop-hook.js.map
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Standalone CLI for background targeted re-indexing.
4
+ * Spawned as a detached child process by stop-hook.ts.
5
+ *
6
+ * Usage: node targeted-runner.js <manifest-json-path>
7
+ *
8
+ * Manifest JSON: { projectPath: string, modifiedFiles: string[] }
9
+ */
10
+ export {};
11
+ //# sourceMappingURL=targeted-runner.d.ts.map
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Standalone CLI for background targeted re-indexing.
4
+ * Spawned as a detached child process by stop-hook.ts.
5
+ *
6
+ * Usage: node targeted-runner.js <manifest-json-path>
7
+ *
8
+ * Manifest JSON: { projectPath: string, modifiedFiles: string[] }
9
+ */
10
+ import fs from 'node:fs';
11
+ import { indexFiles } from '../core/targeted-indexer.js';
12
+ import { createEmbedding } from '../embedding/factory.js';
13
+ import { QdrantVectorDB } from '../vectordb/qdrant.js';
14
+ import { bootstrapQdrant } from '../infra/qdrant-bootstrap.js';
15
+ import { loadConfig } from '../config.js';
16
+ async function main() {
17
+ const manifestPath = process.argv[2];
18
+ if (!manifestPath) {
19
+ process.stderr.write('Usage: targeted-runner.js <manifest-json-path>\n');
20
+ process.exit(1);
21
+ }
22
+ let manifest;
23
+ try {
24
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
25
+ }
26
+ catch (err) {
27
+ process.stderr.write(`[targeted-runner] Failed to read manifest: ${String(err)}\n`);
28
+ process.exit(1);
29
+ }
30
+ // Clean up manifest file
31
+ try {
32
+ fs.unlinkSync(manifestPath);
33
+ }
34
+ catch {
35
+ // Ignore — best effort
36
+ }
37
+ const { projectPath, modifiedFiles } = manifest;
38
+ if (!projectPath || !Array.isArray(modifiedFiles) || modifiedFiles.length === 0) {
39
+ process.stderr.write('[targeted-runner] Empty or invalid manifest, nothing to do.\n');
40
+ process.exit(0);
41
+ }
42
+ try {
43
+ const config = loadConfig();
44
+ const embedding = createEmbedding(config);
45
+ await embedding.initialize();
46
+ let vectordb;
47
+ if (config.vectordbProvider === 'milvus') {
48
+ const { MilvusVectorDB } = await import('../vectordb/milvus.js');
49
+ vectordb = new MilvusVectorDB();
50
+ }
51
+ else {
52
+ const qdrantUrl = await bootstrapQdrant();
53
+ vectordb = new QdrantVectorDB(qdrantUrl, config.qdrantApiKey);
54
+ }
55
+ const result = await indexFiles(projectPath, modifiedFiles, embedding, vectordb);
56
+ process.stderr.write(`[targeted-runner] Re-indexed ${result.processedFiles} files ` +
57
+ `(${result.totalChunks} chunks, ${result.skippedFiles} deleted) ` +
58
+ `in ${result.durationMs}ms\n`);
59
+ }
60
+ catch (err) {
61
+ process.stderr.write(`[targeted-runner] Failed: ${String(err)}\n`);
62
+ process.exit(1);
63
+ }
64
+ }
65
+ void main();
66
+ //# sourceMappingURL=targeted-runner.js.map