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/paths.d.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Normalize a path to forward slashes, resolve to absolute, remove trailing slash.
|
|
3
|
-
* This is the single source of truth for path handling — called at every boundary.
|
|
4
|
-
*/
|
|
5
1
|
export declare function normalizePath(inputPath: string): string;
|
|
6
2
|
export declare function getDataDir(): string;
|
|
7
3
|
export declare function getSnapshotDir(): string;
|
|
8
4
|
export declare function getCacheDir(): string;
|
|
9
5
|
export declare function getRegistryPath(): string;
|
|
10
6
|
export declare function pathToCollectionName(absolutePath: string): string;
|
|
7
|
+
export declare function getDocMetadataPath(): string;
|
|
8
|
+
export declare function getMemoryDbPath(): string;
|
|
9
|
+
export declare function docCollectionName(library: string): string;
|
|
11
10
|
//# sourceMappingURL=paths.d.ts.map
|
package/dist/paths.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import { getConfig } from './config.js';
|
|
4
|
-
/**
|
|
5
|
-
* Normalize a path to forward slashes, resolve to absolute, remove trailing slash.
|
|
6
|
-
* This is the single source of truth for path handling — called at every boundary.
|
|
7
|
-
*/
|
|
8
4
|
export function normalizePath(inputPath) {
|
|
9
5
|
let resolved = inputPath;
|
|
10
6
|
if (resolved.startsWith('~')) {
|
|
@@ -38,4 +34,18 @@ export function pathToCollectionName(absolutePath) {
|
|
|
38
34
|
.replace(/^_|_$/g, '');
|
|
39
35
|
return `eidetic_${safe}`;
|
|
40
36
|
}
|
|
37
|
+
export function getDocMetadataPath() {
|
|
38
|
+
return `${getDataDir()}/doc-metadata.json`;
|
|
39
|
+
}
|
|
40
|
+
export function getMemoryDbPath() {
|
|
41
|
+
return `${getDataDir()}/memory-history.db`;
|
|
42
|
+
}
|
|
43
|
+
export function docCollectionName(library) {
|
|
44
|
+
const safe = library
|
|
45
|
+
.toLowerCase()
|
|
46
|
+
.replace(/[^a-z0-9]/g, '_')
|
|
47
|
+
.replace(/_+/g, '_')
|
|
48
|
+
.replace(/^_|_$/g, '');
|
|
49
|
+
return `doc_${safe}`;
|
|
50
|
+
}
|
|
41
51
|
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Hook entry point for PreCompact and SessionEnd events.
|
|
4
|
+
*
|
|
5
|
+
* PreCompact: Parses transcript, writes session note, updates index, spawns background indexer.
|
|
6
|
+
* SessionEnd: Same as PreCompact + runs memory extraction pipeline (semantic facts → Qdrant).
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=hook.d.ts.map
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Hook entry point for PreCompact and SessionEnd events.
|
|
4
|
+
*
|
|
5
|
+
* PreCompact: Parses transcript, writes session note, updates index, spawns background indexer.
|
|
6
|
+
* SessionEnd: Same as PreCompact + runs memory extraction pipeline (semantic facts → Qdrant).
|
|
7
|
+
*/
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { parseTranscript } from './transcript-parser.js';
|
|
12
|
+
import { writeSessionNote } from './note-writer.js';
|
|
13
|
+
import { updateSessionIndex, readSessionIndex } from './tier0-writer.js';
|
|
14
|
+
import { spawnBackgroundIndexer } from './session-indexer.js';
|
|
15
|
+
import { getNotesDir, getProjectId } from './utils.js';
|
|
16
|
+
// Resolve index-runner path at module boundary (follows project convention)
|
|
17
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
+
const __dirname = path.dirname(__filename);
|
|
19
|
+
const INDEX_RUNNER_PATH = path.join(__dirname, 'index-runner.js');
|
|
20
|
+
// Zod schema — handles both PreCompact and SessionEnd hook events
|
|
21
|
+
const HookInputSchema = z.discriminatedUnion('hook_event_name', [
|
|
22
|
+
z.object({
|
|
23
|
+
session_id: z.string(),
|
|
24
|
+
transcript_path: z.string(),
|
|
25
|
+
cwd: z.string(),
|
|
26
|
+
trigger: z.enum(['auto', 'manual']),
|
|
27
|
+
hook_event_name: z.literal('PreCompact'),
|
|
28
|
+
}),
|
|
29
|
+
z.object({
|
|
30
|
+
session_id: z.string(),
|
|
31
|
+
transcript_path: z.string(),
|
|
32
|
+
cwd: z.string(),
|
|
33
|
+
hook_event_name: z.literal('SessionEnd'),
|
|
34
|
+
reason: z.string().optional(),
|
|
35
|
+
}),
|
|
36
|
+
]);
|
|
37
|
+
async function main() {
|
|
38
|
+
try {
|
|
39
|
+
const input = await readStdin();
|
|
40
|
+
const parseResult = HookInputSchema.safeParse(JSON.parse(input));
|
|
41
|
+
if (!parseResult.success) {
|
|
42
|
+
outputError(`Invalid hook input: ${parseResult.error.message}`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const hookInput = parseResult.data;
|
|
46
|
+
const projectId = getProjectId(hookInput.cwd);
|
|
47
|
+
const notesDir = getNotesDir(projectId);
|
|
48
|
+
const trigger = hookInput.hook_event_name === 'PreCompact' ? hookInput.trigger : 'session_end';
|
|
49
|
+
// Parse transcript
|
|
50
|
+
const session = await parseTranscript(hookInput.transcript_path, hookInput.session_id, projectId, hookInput.cwd, trigger);
|
|
51
|
+
let noteFile;
|
|
52
|
+
let skippedNote = false;
|
|
53
|
+
if (hookInput.hook_event_name === 'SessionEnd') {
|
|
54
|
+
// Dedup check: skip note if already captured by PreCompact
|
|
55
|
+
const existingIndex = readSessionIndex(notesDir);
|
|
56
|
+
const alreadyCaptured = existingIndex?.sessions.some((s) => s.sessionId === hookInput.session_id) ?? false;
|
|
57
|
+
if (alreadyCaptured) {
|
|
58
|
+
skippedNote = true;
|
|
59
|
+
// Use placeholder path — note already exists
|
|
60
|
+
// existingIndex is non-null here: alreadyCaptured implies existingIndex?.sessions.some() returned true
|
|
61
|
+
const sessions = existingIndex?.sessions ?? [];
|
|
62
|
+
const existing = sessions.find((s) => s.sessionId === hookInput.session_id);
|
|
63
|
+
noteFile = existing?.noteFile ?? writeSessionNote(notesDir, session);
|
|
64
|
+
process.stderr.write(`[eidetic] SessionEnd: session ${hookInput.session_id} already captured by PreCompact, skipping note\n`);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
noteFile = writeSessionNote(notesDir, session);
|
|
68
|
+
updateSessionIndex(notesDir, session, noteFile);
|
|
69
|
+
spawnBackgroundIndexer(notesDir, INDEX_RUNNER_PATH);
|
|
70
|
+
}
|
|
71
|
+
// Run memory extraction (best-effort — graceful failure if Qdrant unavailable)
|
|
72
|
+
const memoryActions = await extractMemories(session);
|
|
73
|
+
outputSuccess({
|
|
74
|
+
noteFile,
|
|
75
|
+
skippedNote,
|
|
76
|
+
filesModified: session.filesModified.length,
|
|
77
|
+
tasksCreated: session.tasksCreated.length,
|
|
78
|
+
memoriesExtracted: memoryActions,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
// PreCompact: original flow
|
|
83
|
+
noteFile = writeSessionNote(notesDir, session);
|
|
84
|
+
updateSessionIndex(notesDir, session, noteFile);
|
|
85
|
+
spawnBackgroundIndexer(notesDir, INDEX_RUNNER_PATH);
|
|
86
|
+
outputSuccess({
|
|
87
|
+
noteFile,
|
|
88
|
+
filesModified: session.filesModified.length,
|
|
89
|
+
tasksCreated: session.tasksCreated.length,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
outputError(err instanceof Error ? err.message : String(err));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Build content string for memory extraction from an ExtractedSession.
|
|
99
|
+
*/
|
|
100
|
+
function buildMemoryContent(session) {
|
|
101
|
+
const parts = [];
|
|
102
|
+
if (session.userMessages.length > 0) {
|
|
103
|
+
parts.push('User messages:');
|
|
104
|
+
session.userMessages.forEach((msg, i) => {
|
|
105
|
+
parts.push(`${i + 1}. ${msg}`);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (session.filesModified.length > 0) {
|
|
109
|
+
parts.push(`\nFiles modified: ${session.filesModified.join(', ')}`);
|
|
110
|
+
}
|
|
111
|
+
if (session.tasksCreated.length > 0) {
|
|
112
|
+
parts.push(`Tasks: ${session.tasksCreated.join(', ')}`);
|
|
113
|
+
}
|
|
114
|
+
if (session.branch) {
|
|
115
|
+
parts.push(`Branch: ${session.branch}`);
|
|
116
|
+
}
|
|
117
|
+
return parts.join('\n');
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Run memory extraction pipeline. Returns count of actions taken.
|
|
121
|
+
* Fails gracefully — logs to stderr if Qdrant or LLM unavailable.
|
|
122
|
+
*/
|
|
123
|
+
async function extractMemories(session) {
|
|
124
|
+
const content = buildMemoryContent(session);
|
|
125
|
+
if (!content.trim())
|
|
126
|
+
return 0;
|
|
127
|
+
try {
|
|
128
|
+
// Dynamic imports to avoid loading heavy deps on every hook invocation
|
|
129
|
+
const [{ loadConfig }, { createEmbedding }, { QdrantVectorDB }, { MemoryHistory }, { MemoryStore }, { getMemoryDbPath },] = await Promise.all([
|
|
130
|
+
import('../config.js'),
|
|
131
|
+
import('../embedding/factory.js'),
|
|
132
|
+
import('../vectordb/qdrant.js'),
|
|
133
|
+
import('../memory/history.js'),
|
|
134
|
+
import('../memory/store.js'),
|
|
135
|
+
import('../paths.js'),
|
|
136
|
+
]);
|
|
137
|
+
const config = loadConfig();
|
|
138
|
+
const embedding = createEmbedding(config);
|
|
139
|
+
await embedding.initialize();
|
|
140
|
+
const vectordb = new QdrantVectorDB();
|
|
141
|
+
const history = new MemoryHistory(getMemoryDbPath());
|
|
142
|
+
const memoryStore = new MemoryStore(embedding, vectordb, history);
|
|
143
|
+
const actions = await memoryStore.addMemory(content, 'session-end-hook');
|
|
144
|
+
process.stderr.write(`[eidetic] Memory extraction: ${actions.length} action(s) (${actions.map((a) => a.event).join(', ') || 'none'})\n`);
|
|
145
|
+
return actions.length;
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
process.stderr.write(`[eidetic] Memory extraction failed (non-fatal): ${err instanceof Error ? err.message : String(err)}\n`);
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function readStdin() {
|
|
153
|
+
const chunks = [];
|
|
154
|
+
for await (const chunk of process.stdin) {
|
|
155
|
+
chunks.push(chunk);
|
|
156
|
+
}
|
|
157
|
+
return Buffer.concat(chunks).toString('utf-8');
|
|
158
|
+
}
|
|
159
|
+
function outputSuccess(result) {
|
|
160
|
+
const output = {
|
|
161
|
+
hookSpecificOutput: { success: true, ...result },
|
|
162
|
+
};
|
|
163
|
+
process.stdout.write(JSON.stringify(output));
|
|
164
|
+
}
|
|
165
|
+
function outputError(message) {
|
|
166
|
+
process.stderr.write(JSON.stringify({ error: message }) + '\n');
|
|
167
|
+
process.stdout.write(JSON.stringify({ hookSpecificOutput: {} }));
|
|
168
|
+
}
|
|
169
|
+
void main();
|
|
170
|
+
//# sourceMappingURL=hook.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Standalone CLI for background indexing of session notes.
|
|
4
|
+
* Called by session-indexer.ts as a detached child process.
|
|
5
|
+
*
|
|
6
|
+
* Usage: node index-runner.js <notes-directory>
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=index-runner.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Standalone CLI for background indexing of session notes.
|
|
4
|
+
* Called by session-indexer.ts as a detached child process.
|
|
5
|
+
*
|
|
6
|
+
* Usage: node index-runner.js <notes-directory>
|
|
7
|
+
*/
|
|
8
|
+
import { indexCodebase } from '../core/indexer.js';
|
|
9
|
+
import { createEmbedding } from '../embedding/factory.js';
|
|
10
|
+
import { QdrantVectorDB } from '../vectordb/qdrant.js';
|
|
11
|
+
import { bootstrapQdrant } from '../infra/qdrant-bootstrap.js';
|
|
12
|
+
import { loadConfig } from '../config.js';
|
|
13
|
+
import { registerProject } from '../state/registry.js';
|
|
14
|
+
async function main() {
|
|
15
|
+
const notesDir = process.argv[2];
|
|
16
|
+
if (!notesDir) {
|
|
17
|
+
process.stderr.write('Usage: index-runner.js <notes-directory>\n');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
// Load config (requires OPENAI_API_KEY in env)
|
|
22
|
+
const config = loadConfig();
|
|
23
|
+
// Create embedding provider
|
|
24
|
+
const embedding = createEmbedding(config);
|
|
25
|
+
await embedding.initialize();
|
|
26
|
+
// Create vectordb using factory pattern (respects VECTORDB_PROVIDER)
|
|
27
|
+
let vectordb;
|
|
28
|
+
if (config.vectordbProvider === 'milvus') {
|
|
29
|
+
const { MilvusVectorDB } = await import('../vectordb/milvus.js');
|
|
30
|
+
vectordb = new MilvusVectorDB();
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
const qdrantUrl = await bootstrapQdrant();
|
|
34
|
+
vectordb = new QdrantVectorDB(qdrantUrl, config.qdrantApiKey);
|
|
35
|
+
}
|
|
36
|
+
// Index the notes directory
|
|
37
|
+
const result = await indexCodebase(notesDir, embedding, vectordb, false, // don't force re-index
|
|
38
|
+
undefined, // no progress callback
|
|
39
|
+
['.md'], // only markdown files
|
|
40
|
+
[]);
|
|
41
|
+
// Register project in registry for discovery
|
|
42
|
+
registerProject(notesDir);
|
|
43
|
+
process.stderr.write(`Indexed ${result.totalFiles} files (${result.totalChunks} chunks) in ${result.durationMs}ms\n`);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
// Best effort - log error but don't crash loudly
|
|
47
|
+
process.stderr.write(`Background indexing failed: ${String(err)}\n`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
void main();
|
|
52
|
+
//# sourceMappingURL=index-runner.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format and write session notes compatible with /wrapup and /catchup.
|
|
3
|
+
*/
|
|
4
|
+
import type { ExtractedSession } from './types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Format an extracted session as a markdown note.
|
|
7
|
+
* Output format matches /wrapup template for /catchup compatibility.
|
|
8
|
+
*/
|
|
9
|
+
export declare function formatSessionNote(session: ExtractedSession): string;
|
|
10
|
+
/**
|
|
11
|
+
* Write a session note to disk.
|
|
12
|
+
* Returns the full path to the written file.
|
|
13
|
+
*/
|
|
14
|
+
export declare function writeSessionNote(notesDir: string, session: ExtractedSession): string;
|
|
15
|
+
//# sourceMappingURL=note-writer.d.ts.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format and write session notes compatible with /wrapup and /catchup.
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { extractDate, writeFileAtomic } from './utils.js';
|
|
7
|
+
/**
|
|
8
|
+
* Format an extracted session as a markdown note.
|
|
9
|
+
* Output format matches /wrapup template for /catchup compatibility.
|
|
10
|
+
*/
|
|
11
|
+
export function formatSessionNote(session) {
|
|
12
|
+
const date = extractDate(session.startTime);
|
|
13
|
+
const branch = session.branch ?? 'unknown';
|
|
14
|
+
const lines = [];
|
|
15
|
+
// YAML frontmatter
|
|
16
|
+
lines.push('---');
|
|
17
|
+
lines.push(`project: ${session.projectName}`);
|
|
18
|
+
lines.push(`date: ${date}`);
|
|
19
|
+
lines.push(`branch: ${branch}`);
|
|
20
|
+
lines.push(`session_id: ${session.sessionId}`);
|
|
21
|
+
lines.push(`trigger: ${session.trigger}`);
|
|
22
|
+
lines.push('---');
|
|
23
|
+
lines.push('');
|
|
24
|
+
// Header
|
|
25
|
+
lines.push(`# ${session.projectName} — ${date}: Auto-captured session`);
|
|
26
|
+
lines.push('');
|
|
27
|
+
lines.push(`**Date:** ${date}`);
|
|
28
|
+
lines.push(`**Project:** ${session.projectName}`);
|
|
29
|
+
lines.push(`**Branch:** ${branch}`);
|
|
30
|
+
lines.push('');
|
|
31
|
+
// User Requests
|
|
32
|
+
if (session.userMessages.length > 0) {
|
|
33
|
+
lines.push('## User Requests');
|
|
34
|
+
session.userMessages.forEach((msg, i) => {
|
|
35
|
+
lines.push(`${i + 1}. ${msg}`);
|
|
36
|
+
});
|
|
37
|
+
lines.push('');
|
|
38
|
+
}
|
|
39
|
+
// Changes
|
|
40
|
+
lines.push('## Changes');
|
|
41
|
+
if (session.filesModified.length > 0) {
|
|
42
|
+
session.filesModified.forEach((file) => {
|
|
43
|
+
lines.push(`- \`${file}\``);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
lines.push('*No file modifications detected.*');
|
|
48
|
+
}
|
|
49
|
+
lines.push('');
|
|
50
|
+
// Tasks
|
|
51
|
+
lines.push('## Tasks');
|
|
52
|
+
if (session.tasksCreated.length > 0 || session.tasksUpdated.length > 0) {
|
|
53
|
+
session.tasksCreated.forEach((task) => {
|
|
54
|
+
lines.push(`- Created: ${task}`);
|
|
55
|
+
});
|
|
56
|
+
session.tasksUpdated.forEach((task) => {
|
|
57
|
+
lines.push(`- Updated: ${task}`);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
lines.push('*No tasks created or updated.*');
|
|
62
|
+
}
|
|
63
|
+
lines.push('');
|
|
64
|
+
// Commands
|
|
65
|
+
lines.push('## Commands');
|
|
66
|
+
if (session.bashCommands.length > 0) {
|
|
67
|
+
session.bashCommands.forEach((cmd) => {
|
|
68
|
+
lines.push(`- \`${cmd}\``);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
lines.push('*No commands recorded.*');
|
|
73
|
+
}
|
|
74
|
+
lines.push('');
|
|
75
|
+
// MCP Tools
|
|
76
|
+
if (session.mcpToolsCalled.length > 0) {
|
|
77
|
+
lines.push('## MCP Tools');
|
|
78
|
+
session.mcpToolsCalled.forEach((tool) => {
|
|
79
|
+
lines.push(`- ${tool}`);
|
|
80
|
+
});
|
|
81
|
+
lines.push('');
|
|
82
|
+
}
|
|
83
|
+
// Decisions (placeholder)
|
|
84
|
+
lines.push('## Decisions');
|
|
85
|
+
lines.push('*Auto-captured. Run /wrapup for decisions with rationale.*');
|
|
86
|
+
lines.push('');
|
|
87
|
+
// Open Questions (placeholder)
|
|
88
|
+
lines.push('## Open Questions');
|
|
89
|
+
lines.push('*Run /wrapup to capture open questions.*');
|
|
90
|
+
lines.push('');
|
|
91
|
+
return lines.join('\n');
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Write a session note to disk.
|
|
95
|
+
* Returns the full path to the written file.
|
|
96
|
+
*/
|
|
97
|
+
export function writeSessionNote(notesDir, session) {
|
|
98
|
+
// Ensure directory exists
|
|
99
|
+
fs.mkdirSync(notesDir, { recursive: true });
|
|
100
|
+
const date = extractDate(session.startTime);
|
|
101
|
+
const shortId = session.sessionId.slice(0, 8);
|
|
102
|
+
const filename = `${date}-auto-${shortId}.md`;
|
|
103
|
+
const filePath = path.join(notesDir, filename);
|
|
104
|
+
const content = formatSessionNote(session);
|
|
105
|
+
// Use atomic write for note files (less critical than index, but still good)
|
|
106
|
+
writeFileAtomic(filePath, content);
|
|
107
|
+
return filePath;
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=note-writer.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawn background process to index session notes into Qdrant.
|
|
3
|
+
* Non-blocking: hook returns immediately while indexing continues.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Spawn a detached background process to index the notes directory.
|
|
7
|
+
* The process runs independently and won't block the hook.
|
|
8
|
+
*
|
|
9
|
+
* @param notesDir - Directory containing session notes to index
|
|
10
|
+
* @param indexRunnerPath - Full path to the index-runner.js script
|
|
11
|
+
*/
|
|
12
|
+
export declare function spawnBackgroundIndexer(notesDir: string, indexRunnerPath: string): void;
|
|
13
|
+
//# sourceMappingURL=session-indexer.d.ts.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawn background process to index session notes into Qdrant.
|
|
3
|
+
* Non-blocking: hook returns immediately while indexing continues.
|
|
4
|
+
*/
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
/**
|
|
7
|
+
* Spawn a detached background process to index the notes directory.
|
|
8
|
+
* The process runs independently and won't block the hook.
|
|
9
|
+
*
|
|
10
|
+
* @param notesDir - Directory containing session notes to index
|
|
11
|
+
* @param indexRunnerPath - Full path to the index-runner.js script
|
|
12
|
+
*/
|
|
13
|
+
export function spawnBackgroundIndexer(notesDir, indexRunnerPath) {
|
|
14
|
+
try {
|
|
15
|
+
const child = spawn(process.execPath, [indexRunnerPath, notesDir], {
|
|
16
|
+
detached: true,
|
|
17
|
+
stdio: 'ignore',
|
|
18
|
+
// Inherit environment for OPENAI_API_KEY, QDRANT_URL, etc.
|
|
19
|
+
env: process.env,
|
|
20
|
+
// Prevent the parent process from waiting for this child
|
|
21
|
+
windowsHide: true,
|
|
22
|
+
});
|
|
23
|
+
// Unref so the parent can exit independently
|
|
24
|
+
child.unref();
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Best effort - if spawn fails, just skip indexing
|
|
28
|
+
// The notes are already saved, indexing can happen later
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=session-indexer.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Inject Tier-0 context at SessionStart.
|
|
4
|
+
* Called by session-start hook to output compact session summary.
|
|
5
|
+
*
|
|
6
|
+
* Outputs to stdout for hook to capture and inject into session.
|
|
7
|
+
*/
|
|
8
|
+
declare function detectProjectRoot(cwd: string): string | null;
|
|
9
|
+
declare function formatTier0Context(session: {
|
|
10
|
+
date: string;
|
|
11
|
+
branch: string | null;
|
|
12
|
+
filesModified: string[];
|
|
13
|
+
tasksCreated: string[];
|
|
14
|
+
}, totalSessions: number): string;
|
|
15
|
+
export { formatTier0Context, detectProjectRoot };
|
|
16
|
+
//# sourceMappingURL=tier0-inject.d.ts.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Inject Tier-0 context at SessionStart.
|
|
4
|
+
* Called by session-start hook to output compact session summary.
|
|
5
|
+
*
|
|
6
|
+
* Outputs to stdout for hook to capture and inject into session.
|
|
7
|
+
*/
|
|
8
|
+
import { execSync } from 'node:child_process';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { readSessionIndex } from './tier0-writer.js';
|
|
11
|
+
import { getNotesDir, getProjectId } from './utils.js';
|
|
12
|
+
const MAX_FILES_SHOWN = 5;
|
|
13
|
+
function main() {
|
|
14
|
+
try {
|
|
15
|
+
// Get cwd from environment (set by Claude Code) or detect from git
|
|
16
|
+
const cwd = process.env.CLAUDE_CWD || process.cwd();
|
|
17
|
+
// Detect project root from git
|
|
18
|
+
const projectPath = detectProjectRoot(cwd);
|
|
19
|
+
if (!projectPath) {
|
|
20
|
+
// Not in a git repo, nothing to inject
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
// Use consistent project ID (handles name collisions)
|
|
24
|
+
const projectId = getProjectId(projectPath);
|
|
25
|
+
const notesDir = getNotesDir(projectId);
|
|
26
|
+
// Read session index
|
|
27
|
+
const index = readSessionIndex(notesDir);
|
|
28
|
+
if (!index || index.sessions.length === 0) {
|
|
29
|
+
// No previous sessions
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// Get most recent session
|
|
33
|
+
const latest = index.sessions[0];
|
|
34
|
+
// Format compact output (~50 tokens)
|
|
35
|
+
const output = formatTier0Context(latest, index.sessions.length);
|
|
36
|
+
process.stdout.write(output);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
// Write to stderr for debugging, but don't break session start
|
|
40
|
+
process.stderr.write(`Tier-0 inject failed: ${String(err)}\n`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function detectProjectRoot(cwd) {
|
|
44
|
+
try {
|
|
45
|
+
const result = execSync('git rev-parse --show-toplevel', {
|
|
46
|
+
cwd,
|
|
47
|
+
encoding: 'utf-8',
|
|
48
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
49
|
+
});
|
|
50
|
+
return result.trim();
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function formatTier0Context(session, totalSessions) {
|
|
57
|
+
const lines = [];
|
|
58
|
+
lines.push(`## Eidetic: Last session (${session.date}, branch: ${session.branch ?? 'unknown'})`);
|
|
59
|
+
// Files modified
|
|
60
|
+
if (session.filesModified.length > 0) {
|
|
61
|
+
const shown = session.filesModified.slice(0, MAX_FILES_SHOWN);
|
|
62
|
+
const remaining = session.filesModified.length - shown.length;
|
|
63
|
+
const fileList = shown.map((f) => path.basename(f)).join(', ');
|
|
64
|
+
if (remaining > 0) {
|
|
65
|
+
lines.push(`- Files modified: ${fileList} (+${remaining} more)`);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
lines.push(`- Files modified: ${fileList}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Tasks
|
|
72
|
+
if (session.tasksCreated.length > 0) {
|
|
73
|
+
lines.push(`- Tasks: ${session.tasksCreated.join(', ')}`);
|
|
74
|
+
}
|
|
75
|
+
// Prompt for more
|
|
76
|
+
if (totalSessions > 1) {
|
|
77
|
+
lines.push(`- Run /catchup for full context (${totalSessions} sessions available).`);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
lines.push('- Run /catchup for full context.');
|
|
81
|
+
}
|
|
82
|
+
lines.push('');
|
|
83
|
+
return lines.join('\n');
|
|
84
|
+
}
|
|
85
|
+
// Export for testing
|
|
86
|
+
export { formatTier0Context, detectProjectRoot };
|
|
87
|
+
main();
|
|
88
|
+
//# sourceMappingURL=tier0-inject.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maintain .session-index.json for Tier-0 fast SessionStart context injection.
|
|
3
|
+
*/
|
|
4
|
+
import type { ExtractedSession, SessionIndex } from './types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Update the session index with a new session record.
|
|
7
|
+
* Prepends the new session and keeps only the last 10.
|
|
8
|
+
* Uses atomic write to prevent corruption from concurrent access.
|
|
9
|
+
*/
|
|
10
|
+
export declare function updateSessionIndex(notesDir: string, session: ExtractedSession, noteFile: string): void;
|
|
11
|
+
/**
|
|
12
|
+
* Read the session index for a project.
|
|
13
|
+
* Returns null if not found or corrupted.
|
|
14
|
+
*/
|
|
15
|
+
export declare function readSessionIndex(notesDir: string): SessionIndex | null;
|
|
16
|
+
//# sourceMappingURL=tier0-writer.d.ts.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maintain .session-index.json for Tier-0 fast SessionStart context injection.
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { extractDate, writeFileAtomic } from './utils.js';
|
|
7
|
+
const MAX_SESSIONS = 10;
|
|
8
|
+
const INDEX_FILENAME = '.session-index.json';
|
|
9
|
+
/**
|
|
10
|
+
* Update the session index with a new session record.
|
|
11
|
+
* Prepends the new session and keeps only the last 10.
|
|
12
|
+
* Uses atomic write to prevent corruption from concurrent access.
|
|
13
|
+
*/
|
|
14
|
+
export function updateSessionIndex(notesDir, session, noteFile) {
|
|
15
|
+
const indexPath = path.join(notesDir, INDEX_FILENAME);
|
|
16
|
+
// Load existing index or create new
|
|
17
|
+
let index;
|
|
18
|
+
if (fs.existsSync(indexPath)) {
|
|
19
|
+
try {
|
|
20
|
+
const content = fs.readFileSync(indexPath, 'utf-8');
|
|
21
|
+
index = JSON.parse(content);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Corrupted index, start fresh
|
|
25
|
+
index = createEmptyIndex(session.projectName);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
index = createEmptyIndex(session.projectName);
|
|
30
|
+
}
|
|
31
|
+
// Create new record
|
|
32
|
+
const record = {
|
|
33
|
+
sessionId: session.sessionId,
|
|
34
|
+
date: extractDate(session.startTime),
|
|
35
|
+
branch: session.branch,
|
|
36
|
+
filesModified: session.filesModified,
|
|
37
|
+
tasksCreated: session.tasksCreated,
|
|
38
|
+
trigger: session.trigger,
|
|
39
|
+
noteFile,
|
|
40
|
+
};
|
|
41
|
+
// Prepend new session and trim to max
|
|
42
|
+
index.sessions = [record, ...index.sessions].slice(0, MAX_SESSIONS);
|
|
43
|
+
index.project = session.projectName;
|
|
44
|
+
index.lastUpdated = new Date().toISOString();
|
|
45
|
+
// Ensure directory exists
|
|
46
|
+
fs.mkdirSync(notesDir, { recursive: true });
|
|
47
|
+
// Write atomically to prevent corruption from concurrent hooks
|
|
48
|
+
writeFileAtomic(indexPath, JSON.stringify(index, null, 2));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Read the session index for a project.
|
|
52
|
+
* Returns null if not found or corrupted.
|
|
53
|
+
*/
|
|
54
|
+
export function readSessionIndex(notesDir) {
|
|
55
|
+
const indexPath = path.join(notesDir, INDEX_FILENAME);
|
|
56
|
+
if (!fs.existsSync(indexPath)) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const content = fs.readFileSync(indexPath, 'utf-8');
|
|
61
|
+
return JSON.parse(content);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function createEmptyIndex(project) {
|
|
68
|
+
return {
|
|
69
|
+
project,
|
|
70
|
+
sessions: [],
|
|
71
|
+
lastUpdated: new Date().toISOString(),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=tier0-writer.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse Claude Code transcript JSONL to extract session data.
|
|
3
|
+
* Extracts deterministic data from tool calls - no LLM needed.
|
|
4
|
+
*/
|
|
5
|
+
import type { ExtractedSession } from './types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Parse a transcript JSONL file and extract session data.
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseTranscript(transcriptPath: string, sessionId: string, projectName: string, projectPath: string, trigger?: 'auto' | 'manual' | 'session_end'): Promise<ExtractedSession>;
|
|
10
|
+
//# sourceMappingURL=transcript-parser.d.ts.map
|