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.
- package/README.md +333 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.js +29 -10
- package/dist/core/cleanup.d.ts +8 -0
- package/dist/core/cleanup.js +41 -0
- package/dist/core/doc-indexer.d.ts +13 -0
- package/dist/core/doc-indexer.js +76 -0
- package/dist/core/doc-searcher.d.ts +13 -0
- package/dist/core/doc-searcher.js +65 -0
- package/dist/core/file-category.d.ts +7 -0
- package/dist/core/file-category.js +75 -0
- package/dist/core/indexer.js +12 -4
- package/dist/core/preview.d.ts +1 -2
- package/dist/core/preview.js +2 -5
- package/dist/core/repo-map.d.ts +33 -0
- package/dist/core/repo-map.js +144 -0
- package/dist/core/searcher.d.ts +1 -13
- package/dist/core/searcher.js +20 -24
- package/dist/core/snapshot-io.js +2 -2
- package/dist/core/sync.d.ts +5 -25
- package/dist/core/sync.js +90 -65
- package/dist/core/targeted-indexer.d.ts +19 -0
- package/dist/core/targeted-indexer.js +127 -0
- package/dist/embedding/factory.d.ts +0 -13
- package/dist/embedding/factory.js +0 -17
- package/dist/embedding/openai.d.ts +2 -14
- package/dist/embedding/openai.js +7 -20
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +2 -0
- package/dist/format.d.ts +12 -0
- package/dist/format.js +160 -31
- package/dist/hooks/post-tool-use.d.ts +13 -0
- package/dist/hooks/post-tool-use.js +113 -0
- package/dist/hooks/stop-hook.d.ts +11 -0
- package/dist/hooks/stop-hook.js +121 -0
- package/dist/hooks/targeted-runner.d.ts +11 -0
- package/dist/hooks/targeted-runner.js +66 -0
- package/dist/index.js +68 -9
- package/dist/infra/qdrant-bootstrap.js +14 -12
- package/dist/memory/history.d.ts +19 -0
- package/dist/memory/history.js +40 -0
- package/dist/memory/llm.d.ts +2 -0
- package/dist/memory/llm.js +56 -0
- package/dist/memory/prompts.d.ts +5 -0
- package/dist/memory/prompts.js +36 -0
- package/dist/memory/reconciler.d.ts +12 -0
- package/dist/memory/reconciler.js +36 -0
- package/dist/memory/store.d.ts +20 -0
- package/dist/memory/store.js +206 -0
- package/dist/memory/types.d.ts +28 -0
- package/dist/memory/types.js +2 -0
- package/dist/paths.d.ts +3 -4
- package/dist/paths.js +14 -4
- package/dist/precompact/hook.d.ts +9 -0
- package/dist/precompact/hook.js +170 -0
- package/dist/precompact/index-runner.d.ts +9 -0
- package/dist/precompact/index-runner.js +52 -0
- package/dist/precompact/note-writer.d.ts +15 -0
- package/dist/precompact/note-writer.js +109 -0
- package/dist/precompact/session-indexer.d.ts +13 -0
- package/dist/precompact/session-indexer.js +31 -0
- package/dist/precompact/tier0-inject.d.ts +16 -0
- package/dist/precompact/tier0-inject.js +88 -0
- package/dist/precompact/tier0-writer.d.ts +16 -0
- package/dist/precompact/tier0-writer.js +74 -0
- package/dist/precompact/transcript-parser.d.ts +10 -0
- package/dist/precompact/transcript-parser.js +148 -0
- package/dist/precompact/types.d.ts +93 -0
- package/dist/precompact/types.js +5 -0
- package/dist/precompact/utils.d.ts +29 -0
- package/dist/precompact/utils.js +95 -0
- package/dist/setup-message.d.ts +2 -2
- package/dist/setup-message.js +39 -20
- package/dist/splitter/ast.js +84 -22
- package/dist/splitter/line.d.ts +0 -4
- package/dist/splitter/line.js +1 -7
- package/dist/splitter/symbol-extract.d.ts +16 -0
- package/dist/splitter/symbol-extract.js +61 -0
- package/dist/splitter/types.d.ts +5 -0
- package/dist/splitter/types.js +1 -1
- package/dist/state/doc-metadata.d.ts +18 -0
- package/dist/state/doc-metadata.js +59 -0
- package/dist/state/registry.d.ts +1 -3
- package/dist/state/snapshot.d.ts +0 -1
- package/dist/state/snapshot.js +3 -19
- package/dist/tool-schemas.d.ts +251 -1
- package/dist/tool-schemas.js +307 -0
- package/dist/tools.d.ts +69 -0
- package/dist/tools.js +286 -17
- package/dist/vectordb/milvus.d.ts +7 -5
- package/dist/vectordb/milvus.js +116 -19
- package/dist/vectordb/qdrant.d.ts +8 -10
- package/dist/vectordb/qdrant.js +105 -33
- package/dist/vectordb/types.d.ts +20 -0
- package/messages.yaml +50 -0
- 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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
lines.push('|-----------|-------|');
|
|
67
|
+
const maxExt = Math.max(...sorted.map(([ext]) => ext.length));
|
|
43
68
|
for (const [ext, count] of sorted) {
|
|
44
|
-
lines.push(
|
|
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 = [
|
|
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:
|
|
80
|
-
lines.push(
|
|
81
|
-
lines.push(
|
|
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(
|
|
105
|
+
lines.push(` Files: ${s.totalFiles}`);
|
|
84
106
|
if (s.totalChunks)
|
|
85
|
-
lines.push(
|
|
107
|
+
lines.push(` Chunks: ${s.totalChunks}`);
|
|
86
108
|
if (s.lastIndexed)
|
|
87
|
-
lines.push(
|
|
109
|
+
lines.push(` Last indexed: ${s.lastIndexed}`);
|
|
88
110
|
if (s.status === 'indexing' && s.progress !== undefined) {
|
|
89
|
-
lines.push(
|
|
111
|
+
lines.push(` Progress: ${s.progress}% — ${s.progressMessage ?? ''}`);
|
|
90
112
|
}
|
|
91
113
|
if (s.error)
|
|
92
|
-
lines.push(
|
|
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
|