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/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