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/index.js CHANGED
@@ -9,15 +9,18 @@ console.warn = (...args) => {
9
9
  };
10
10
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
11
11
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
12
- import { ListToolsRequestSchema, CallToolRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
12
+ import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
13
13
  import { loadConfig } from './config.js';
14
14
  import { createEmbedding } from './embedding/factory.js';
15
15
  import { QdrantVectorDB } from './vectordb/qdrant.js';
16
16
  import { bootstrapQdrant } from './infra/qdrant-bootstrap.js';
17
17
  import { StateManager, cleanupOrphanedSnapshots } from './state/snapshot.js';
18
- import { ToolHandlers } from './tools.js';
18
+ import { ToolHandlers, handleReadFile } from './tools.js';
19
19
  import { TOOL_DEFINITIONS } from './tool-schemas.js';
20
20
  import { getSetupErrorMessage } from './setup-message.js';
21
+ import { MemoryStore } from './memory/store.js';
22
+ import { MemoryHistory } from './memory/history.js';
23
+ import { getMemoryDbPath } from './paths.js';
21
24
  const WORKFLOW_GUIDANCE = `# Eidetic Code Search Workflow
22
25
 
23
26
  **Before searching:** Ensure the codebase is indexed.
@@ -33,14 +36,34 @@ const WORKFLOW_GUIDANCE = `# Eidetic Code Search Workflow
33
36
  - Use \`project\` param instead of \`path\` for convenience
34
37
  - Start with specific queries, broaden if no results
35
38
 
39
+ **Reading files efficiently:**
40
+ - \`read_file(path="...")\` → raw content without line-number overhead (~15-20% fewer tokens for code, more for short-line files)
41
+ - Use \`offset\` and \`limit\` to page through large files
42
+ - Add \`lineNumbers=true\` only when you need line references for editing
43
+
36
44
  **After first index:**
37
45
  - Re-indexing is incremental (only changed files re-embedded)
38
46
  - Use \`project\` param instead of \`path\` for convenience
39
47
  - Use \`get_indexing_status\` to check progress during long indexes
48
+ - Use \`cleanup_vectors(path="...", dryRun=true)\` to preview stale vectors, then without dryRun to remove them (no embedding cost)
40
49
 
41
50
  **Cross-project search:**
42
51
  - Index multiple projects, each with its own path
43
- - Search across any indexed project regardless of current working directory`;
52
+ - Search across any indexed project regardless of current working directory
53
+
54
+ **Documentation caching (saves ~5K tokens per repeated doc fetch):**
55
+ - After fetching docs via query-docs or WebFetch, cache them: \`index_document(content="...", source="<url>", library="<name>", topic="<topic>")\`
56
+ - Next time you need the same docs: \`search_documents(query="...", library="<name>")\` (~20 tokens/result)
57
+ - Docs are grouped by library — one collection per library, searchable across topics
58
+ - Stale docs (past TTL) still return results but are flagged \`[STALE]\`
59
+
60
+ **Persistent memory (cross-session developer knowledge):**
61
+ - \`add_memory(content="...")\` → extracts facts about coding style, tools, architecture, etc.
62
+ - \`search_memory(query="...")\` → find relevant memories by semantic search
63
+ - \`list_memories()\` → see all stored memories grouped by category
64
+ - \`delete_memory(id="...")\` → remove a specific memory
65
+ - \`memory_history(id="...")\` → view change log for a memory
66
+ - Memories are automatically deduplicated — adding similar facts updates existing ones`;
44
67
  async function main() {
45
68
  const config = loadConfig();
46
69
  console.log(`Config loaded. Provider: ${config.vectordbProvider}, Model: ${config.embeddingModel}`);
@@ -66,24 +89,44 @@ async function main() {
66
89
  }
67
90
  const state = new StateManager();
68
91
  handlers = new ToolHandlers(embedding, vectordb, state);
92
+ // Initialize memory subsystem
93
+ try {
94
+ const memoryHistory = new MemoryHistory(getMemoryDbPath());
95
+ const memoryStore = new MemoryStore(embedding, vectordb, memoryHistory);
96
+ handlers.setMemoryStore(memoryStore);
97
+ console.log('Memory system initialized.');
98
+ }
99
+ catch (memErr) {
100
+ console.warn(`Memory system initialization failed: ${memErr instanceof Error ? memErr.message : String(memErr)}`);
101
+ console.warn('Memory tools will return errors. Other tools work normally.');
102
+ }
69
103
  }
70
104
  catch (err) {
71
105
  setupError = err instanceof Error ? err.message : String(err);
72
106
  console.warn(`Eidetic initialization failed: ${setupError}`);
73
107
  console.warn('Server will start in setup-required mode. All tool calls will return setup instructions.');
74
108
  }
109
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
75
110
  const server = new Server({ name: 'claude-eidetic', version: '0.1.0' }, { capabilities: { tools: {} } });
111
+ // eslint-disable-next-line @typescript-eslint/require-await
76
112
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
77
113
  tools: [...TOOL_DEFINITIONS],
78
114
  }));
79
115
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
80
116
  const { name, arguments: args } = request.params;
117
+ // Tools that work without initialization (no embedding/vectordb needed)
118
+ if (name === '__IMPORTANT')
119
+ return { content: [{ type: 'text', text: WORKFLOW_GUIDANCE }] };
120
+ if (name === 'read_file')
121
+ return handleReadFile(args ?? {});
81
122
  if (!handlers) {
82
123
  return {
83
- content: [{
124
+ content: [
125
+ {
84
126
  type: 'text',
85
127
  text: getSetupErrorMessage(setupError ?? 'Unknown error'),
86
- }],
128
+ },
129
+ ],
87
130
  isError: true,
88
131
  };
89
132
  }
@@ -98,10 +141,26 @@ async function main() {
98
141
  return handlers.handleGetIndexingStatus(args ?? {});
99
142
  case 'list_indexed':
100
143
  return handlers.handleListIndexed();
101
- case '__IMPORTANT':
102
- return {
103
- content: [{ type: 'text', text: WORKFLOW_GUIDANCE }],
104
- };
144
+ case 'index_document':
145
+ return handlers.handleIndexDocument(args ?? {});
146
+ case 'search_documents':
147
+ return handlers.handleSearchDocuments(args ?? {});
148
+ case 'add_memory':
149
+ return handlers.handleAddMemory(args ?? {});
150
+ case 'search_memory':
151
+ return handlers.handleSearchMemory(args ?? {});
152
+ case 'list_memories':
153
+ return handlers.handleListMemories(args ?? {});
154
+ case 'delete_memory':
155
+ return handlers.handleDeleteMemory(args ?? {});
156
+ case 'memory_history':
157
+ return handlers.handleMemoryHistory(args ?? {});
158
+ case 'cleanup_vectors':
159
+ return handlers.handleCleanupVectors(args ?? {});
160
+ case 'browse_structure':
161
+ return handlers.handleBrowseStructure(args ?? {});
162
+ case 'list_symbols':
163
+ return handlers.handleListSymbols(args ?? {});
105
164
  default:
106
165
  return {
107
166
  content: [{ type: 'text', text: `Unknown tool: ${name}` }],
@@ -30,12 +30,18 @@ export async function bootstrapQdrant() {
30
30
  const dataDir = path.join(getDataDir(), 'qdrant-data').replace(/\\/g, '/');
31
31
  console.log(`Creating new Qdrant container "${CONTAINER_NAME}"...`);
32
32
  execFileSync('docker', [
33
- 'run', '-d',
34
- '--name', CONTAINER_NAME,
35
- '--restart', 'unless-stopped',
36
- '-p', '6333:6333',
37
- '-p', '6334:6334',
38
- '-v', `${dataDir}:/qdrant/storage`,
33
+ 'run',
34
+ '-d',
35
+ '--name',
36
+ CONTAINER_NAME,
37
+ '--restart',
38
+ 'unless-stopped',
39
+ '-p',
40
+ '6333:6333',
41
+ '-p',
42
+ '6334:6334',
43
+ '-v',
44
+ `${dataDir}:/qdrant/storage`,
39
45
  'qdrant/qdrant',
40
46
  ], { stdio: 'pipe' });
41
47
  }
@@ -67,11 +73,7 @@ function isDockerAvailable() {
67
73
  }
68
74
  function getContainerState() {
69
75
  try {
70
- const output = execFileSync('docker', [
71
- 'ps', '-a',
72
- '--filter', `name=^/${CONTAINER_NAME}$`,
73
- '--format', '{{.State}}',
74
- ], { encoding: 'utf-8', stdio: 'pipe' }).trim();
76
+ const output = execFileSync('docker', ['ps', '-a', '--filter', `name=^/${CONTAINER_NAME}$`, '--format', '{{.State}}'], { encoding: 'utf-8', stdio: 'pipe' }).trim();
75
77
  if (!output)
76
78
  return 'none';
77
79
  if (output === 'running')
@@ -87,7 +89,7 @@ async function waitForHealth(url, timeoutMs) {
87
89
  while (Date.now() - start < timeoutMs) {
88
90
  if (await isQdrantHealthy(url))
89
91
  return true;
90
- await new Promise(r => setTimeout(r, HEALTH_POLL_MS));
92
+ await new Promise((r) => setTimeout(r, HEALTH_POLL_MS));
91
93
  }
92
94
  return false;
93
95
  }
@@ -0,0 +1,19 @@
1
+ import type { MemoryEvent } from './types.js';
2
+ export interface HistoryEntry {
3
+ id: number;
4
+ memory_id: string;
5
+ previous_value: string | null;
6
+ new_value: string | null;
7
+ event: MemoryEvent;
8
+ created_at: string;
9
+ updated_at: string | null;
10
+ source: string | null;
11
+ }
12
+ export declare class MemoryHistory {
13
+ private db;
14
+ constructor(dbPath: string);
15
+ log(memoryId: string, event: MemoryEvent, newValue: string | null, previousValue?: string | null, source?: string | null, updatedAt?: string | null): void;
16
+ getHistory(memoryId: string): HistoryEntry[];
17
+ close(): void;
18
+ }
19
+ //# sourceMappingURL=history.d.ts.map
@@ -0,0 +1,40 @@
1
+ import Database from 'better-sqlite3';
2
+ import { mkdirSync } from 'node:fs';
3
+ import { dirname } from 'node:path';
4
+ export class MemoryHistory {
5
+ db;
6
+ constructor(dbPath) {
7
+ mkdirSync(dirname(dbPath), { recursive: true });
8
+ this.db = new Database(dbPath);
9
+ this.db.pragma('journal_mode = WAL');
10
+ this.db.exec(`
11
+ CREATE TABLE IF NOT EXISTS memory_history (
12
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
13
+ memory_id TEXT NOT NULL,
14
+ previous_value TEXT,
15
+ new_value TEXT,
16
+ event TEXT NOT NULL,
17
+ created_at TEXT NOT NULL,
18
+ updated_at TEXT,
19
+ source TEXT
20
+ )
21
+ `);
22
+ }
23
+ log(memoryId, event, newValue, previousValue = null, source = null, updatedAt = null) {
24
+ this.db
25
+ .prepare(`
26
+ INSERT INTO memory_history (memory_id, previous_value, new_value, event, created_at, updated_at, source)
27
+ VALUES (?, ?, ?, ?, ?, ?, ?)
28
+ `)
29
+ .run(memoryId, previousValue, newValue, event, new Date().toISOString(), updatedAt, source);
30
+ }
31
+ getHistory(memoryId) {
32
+ return this.db
33
+ .prepare('SELECT * FROM memory_history WHERE memory_id = ? ORDER BY created_at ASC')
34
+ .all(memoryId);
35
+ }
36
+ close() {
37
+ this.db.close();
38
+ }
39
+ }
40
+ //# sourceMappingURL=history.js.map
@@ -0,0 +1,2 @@
1
+ export declare function chatCompletion(systemPrompt: string, userMessage: string): Promise<string>;
2
+ //# sourceMappingURL=llm.d.ts.map
@@ -0,0 +1,56 @@
1
+ import OpenAI from 'openai';
2
+ import Anthropic from '@anthropic-ai/sdk';
3
+ import { getConfig } from '../config.js';
4
+ import { MemoryError } from '../errors.js';
5
+ export async function chatCompletion(systemPrompt, userMessage) {
6
+ const config = getConfig();
7
+ if (config.memoryLlmProvider === 'anthropic') {
8
+ const apiKey = config.memoryLlmApiKey ?? config.anthropicApiKey ?? config.openaiApiKey;
9
+ if (!apiKey) {
10
+ throw new MemoryError('No API key configured for memory LLM. Set MEMORY_LLM_API_KEY, ANTHROPIC_API_KEY, or OPENAI_API_KEY.');
11
+ }
12
+ const client = new Anthropic({ apiKey });
13
+ try {
14
+ const response = await client.messages.create({
15
+ model: config.memoryLlmModel,
16
+ max_tokens: 2048,
17
+ system: systemPrompt,
18
+ messages: [{ role: 'user', content: userMessage }],
19
+ });
20
+ const block = response.content[0];
21
+ return block?.type === 'text' ? block.text : '{}';
22
+ }
23
+ catch (err) {
24
+ throw new MemoryError('Memory LLM call failed', err);
25
+ }
26
+ }
27
+ // OpenAI / Ollama path
28
+ const apiKey = config.memoryLlmApiKey ?? config.openaiApiKey;
29
+ if (!apiKey) {
30
+ throw new MemoryError('No API key configured for memory LLM. Set MEMORY_LLM_API_KEY or OPENAI_API_KEY.');
31
+ }
32
+ let baseURL;
33
+ if (config.memoryLlmBaseUrl) {
34
+ baseURL = config.memoryLlmBaseUrl;
35
+ }
36
+ else if (config.memoryLlmProvider === 'ollama') {
37
+ baseURL = config.ollamaBaseUrl;
38
+ }
39
+ const client = new OpenAI({ apiKey, ...(baseURL ? { baseURL } : {}) });
40
+ try {
41
+ const response = await client.chat.completions.create({
42
+ model: config.memoryLlmModel,
43
+ messages: [
44
+ { role: 'system', content: systemPrompt },
45
+ { role: 'user', content: userMessage },
46
+ ],
47
+ temperature: 0,
48
+ response_format: { type: 'json_object' },
49
+ });
50
+ return response.choices[0]?.message?.content ?? '{}';
51
+ }
52
+ catch (err) {
53
+ throw new MemoryError('Memory LLM call failed', err);
54
+ }
55
+ }
56
+ //# sourceMappingURL=llm.js.map
@@ -0,0 +1,5 @@
1
+ export declare const FACT_EXTRACTION_SYSTEM_PROMPT = "You are a developer knowledge extractor. Your job is to extract discrete, factual statements from conversations about software development.\n\nExtract facts about:\n- **coding_style**: Formatting preferences (tabs/spaces, naming conventions, line length), code style rules\n- **tools**: Preferred tools, frameworks, libraries, test runners, bundlers, linters, editors\n- **architecture**: Design patterns, architectural decisions, system design preferences\n- **conventions**: Project conventions, commit message formats, branch naming, PR workflows\n- **debugging**: Solutions to specific bugs, debugging techniques, known issues\n- **workflow**: Development habits, deployment processes, review preferences\n- **preferences**: General preferences, opinions, requirements that don't fit other categories\n\nRules:\n1. Extract only factual, concrete statements \u2014 not vague observations\n2. Each fact should be a single, self-contained statement\n3. Use third person (\"The user prefers...\" or state the fact directly)\n4. If the input contains no extractable facts, return an empty array\n5. Do NOT extract facts about the conversation itself (e.g., \"the user asked about...\")\n6. Do NOT extract temporary or session-specific information\n7. Prefer specific facts over general ones\n\nRespond with JSON: { \"facts\": [{ \"fact\": \"...\", \"category\": \"...\" }] }\n\nCategories: coding_style, tools, architecture, conventions, debugging, workflow, preferences";
2
+ export declare const FACT_EXTRACTION_USER_TEMPLATE = "Extract developer knowledge facts from this text:\n\n<text>\n{content}\n</text>";
3
+ export declare function buildSystemPrompt(): string;
4
+ export declare function buildExtractionPrompt(content: string): string;
5
+ //# sourceMappingURL=prompts.d.ts.map
@@ -0,0 +1,36 @@
1
+ export const FACT_EXTRACTION_SYSTEM_PROMPT = `You are a developer knowledge extractor. Your job is to extract discrete, factual statements from conversations about software development.
2
+
3
+ Extract facts about:
4
+ - **coding_style**: Formatting preferences (tabs/spaces, naming conventions, line length), code style rules
5
+ - **tools**: Preferred tools, frameworks, libraries, test runners, bundlers, linters, editors
6
+ - **architecture**: Design patterns, architectural decisions, system design preferences
7
+ - **conventions**: Project conventions, commit message formats, branch naming, PR workflows
8
+ - **debugging**: Solutions to specific bugs, debugging techniques, known issues
9
+ - **workflow**: Development habits, deployment processes, review preferences
10
+ - **preferences**: General preferences, opinions, requirements that don't fit other categories
11
+
12
+ Rules:
13
+ 1. Extract only factual, concrete statements — not vague observations
14
+ 2. Each fact should be a single, self-contained statement
15
+ 3. Use third person ("The user prefers..." or state the fact directly)
16
+ 4. If the input contains no extractable facts, return an empty array
17
+ 5. Do NOT extract facts about the conversation itself (e.g., "the user asked about...")
18
+ 6. Do NOT extract temporary or session-specific information
19
+ 7. Prefer specific facts over general ones
20
+
21
+ Respond with JSON: { "facts": [{ "fact": "...", "category": "..." }] }
22
+
23
+ Categories: coding_style, tools, architecture, conventions, debugging, workflow, preferences`;
24
+ export const FACT_EXTRACTION_USER_TEMPLATE = `Extract developer knowledge facts from this text:
25
+
26
+ <text>
27
+ {content}
28
+ </text>`;
29
+ export function buildSystemPrompt() {
30
+ const today = new Date().toISOString().slice(0, 10);
31
+ return `${FACT_EXTRACTION_SYSTEM_PROMPT}\n- Today's date is ${today}.`;
32
+ }
33
+ export function buildExtractionPrompt(content) {
34
+ return FACT_EXTRACTION_USER_TEMPLATE.replace('{content}', content);
35
+ }
36
+ //# sourceMappingURL=prompts.js.map
@@ -0,0 +1,12 @@
1
+ import type { ReconcileResult } from './types.js';
2
+ export declare function hashMemory(text: string): string;
3
+ export declare function cosineSimilarity(a: number[], b: number[]): number;
4
+ export interface ExistingMatch {
5
+ id: string;
6
+ memory: string;
7
+ hash: string;
8
+ vector: number[];
9
+ score: number;
10
+ }
11
+ export declare function reconcile(newHash: string, newVector: number[], candidates: ExistingMatch[]): ReconcileResult;
12
+ //# sourceMappingURL=reconciler.d.ts.map
@@ -0,0 +1,36 @@
1
+ import { createHash } from 'node:crypto';
2
+ const SIMILARITY_THRESHOLD = 0.92;
3
+ export function hashMemory(text) {
4
+ return createHash('md5').update(text.trim().toLowerCase()).digest('hex');
5
+ }
6
+ export function cosineSimilarity(a, b) {
7
+ if (a.length !== b.length || a.length === 0)
8
+ return 0;
9
+ let dot = 0;
10
+ let normA = 0;
11
+ let normB = 0;
12
+ for (let i = 0; i < a.length; i++) {
13
+ dot += a[i] * b[i];
14
+ normA += a[i] * a[i];
15
+ normB += b[i] * b[i];
16
+ }
17
+ const denom = Math.sqrt(normA) * Math.sqrt(normB);
18
+ return denom === 0 ? 0 : dot / denom;
19
+ }
20
+ export function reconcile(newHash, newVector, candidates) {
21
+ // Check for exact hash match first
22
+ for (const candidate of candidates) {
23
+ if (candidate.hash === newHash) {
24
+ return { action: 'NONE', existingId: candidate.id, existingMemory: candidate.memory };
25
+ }
26
+ }
27
+ // Check cosine similarity for semantic near-duplicates
28
+ for (const candidate of candidates) {
29
+ const sim = cosineSimilarity(newVector, candidate.vector);
30
+ if (sim >= SIMILARITY_THRESHOLD) {
31
+ return { action: 'UPDATE', existingId: candidate.id, existingMemory: candidate.memory };
32
+ }
33
+ }
34
+ return { action: 'ADD' };
35
+ }
36
+ //# sourceMappingURL=reconciler.js.map
@@ -0,0 +1,20 @@
1
+ import type { Embedding } from '../embedding/types.js';
2
+ import type { VectorDB } from '../vectordb/types.js';
3
+ import type { MemoryItem, MemoryAction } from './types.js';
4
+ import { MemoryHistory } from './history.js';
5
+ export declare class MemoryStore {
6
+ private embedding;
7
+ private vectordb;
8
+ private history;
9
+ private initialized;
10
+ constructor(embedding: Embedding, vectordb: VectorDB, history: MemoryHistory);
11
+ private ensureCollection;
12
+ addMemory(content: string, source?: string): Promise<MemoryAction[]>;
13
+ searchMemory(query: string, limit?: number, category?: string): Promise<MemoryItem[]>;
14
+ listMemories(category?: string, limit?: number): Promise<MemoryItem[]>;
15
+ deleteMemory(id: string): Promise<boolean>;
16
+ getHistory(memoryId: string): import("./history.js").HistoryEntry[];
17
+ private extractFacts;
18
+ private processFact;
19
+ }
20
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1,206 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { chatCompletion } from './llm.js';
3
+ import { buildSystemPrompt, buildExtractionPrompt } from './prompts.js';
4
+ import { hashMemory, reconcile } from './reconciler.js';
5
+ const COLLECTION_NAME = 'eidetic_memory';
6
+ const SEARCH_CANDIDATES = 5;
7
+ // Data model mapping (reuses existing VectorDB/SearchResult fields):
8
+ // content → memory text (full-text search)
9
+ // relativePath → memory UUID (enables deleteByPath for single-memory deletion)
10
+ // fileExtension→ category (enables extensionFilter for category filtering)
11
+ // language → source
12
+ // Additional payload: hash, memory, category, source, created_at, updated_at
13
+ export class MemoryStore {
14
+ embedding;
15
+ vectordb;
16
+ history;
17
+ initialized = false;
18
+ constructor(embedding, vectordb, history) {
19
+ this.embedding = embedding;
20
+ this.vectordb = vectordb;
21
+ this.history = history;
22
+ }
23
+ async ensureCollection() {
24
+ if (this.initialized)
25
+ return;
26
+ const exists = await this.vectordb.hasCollection(COLLECTION_NAME);
27
+ if (!exists) {
28
+ await this.vectordb.createCollection(COLLECTION_NAME, this.embedding.dimension);
29
+ }
30
+ this.initialized = true;
31
+ }
32
+ async addMemory(content, source) {
33
+ await this.ensureCollection();
34
+ const facts = await this.extractFacts(content);
35
+ if (facts.length === 0)
36
+ return [];
37
+ const actions = [];
38
+ for (const fact of facts) {
39
+ const action = await this.processFact(fact, source);
40
+ if (action)
41
+ actions.push(action);
42
+ }
43
+ return actions;
44
+ }
45
+ async searchMemory(query, limit = 10, category) {
46
+ await this.ensureCollection();
47
+ const queryVector = await this.embedding.embed(query);
48
+ const results = await this.vectordb.search(COLLECTION_NAME, {
49
+ queryVector,
50
+ queryText: query,
51
+ limit,
52
+ ...(category ? { extensionFilter: [category] } : {}),
53
+ });
54
+ // Enrich with full payload from getById
55
+ const items = [];
56
+ for (const r of results) {
57
+ const id = r.relativePath; // memory UUID stored in relativePath
58
+ const point = await this.vectordb.getById(COLLECTION_NAME, id);
59
+ if (!point)
60
+ continue;
61
+ items.push(payloadToMemoryItem(id, point.payload));
62
+ }
63
+ return items;
64
+ }
65
+ async listMemories(category, limit = 50) {
66
+ await this.ensureCollection();
67
+ const queryVector = await this.embedding.embed('developer knowledge');
68
+ const results = await this.vectordb.search(COLLECTION_NAME, {
69
+ queryVector,
70
+ queryText: '',
71
+ limit,
72
+ ...(category ? { extensionFilter: [category] } : {}),
73
+ });
74
+ const items = [];
75
+ for (const r of results) {
76
+ const id = r.relativePath;
77
+ const point = await this.vectordb.getById(COLLECTION_NAME, id);
78
+ if (!point)
79
+ continue;
80
+ items.push(payloadToMemoryItem(id, point.payload));
81
+ }
82
+ return items;
83
+ }
84
+ async deleteMemory(id) {
85
+ await this.ensureCollection();
86
+ const existing = await this.vectordb.getById(COLLECTION_NAME, id);
87
+ if (!existing)
88
+ return false;
89
+ const memory = String(existing.payload.memory ?? existing.payload.content ?? '');
90
+ await this.vectordb.deleteByPath(COLLECTION_NAME, id);
91
+ this.history.log(id, 'DELETE', null, memory);
92
+ return true;
93
+ }
94
+ getHistory(memoryId) {
95
+ return this.history.getHistory(memoryId);
96
+ }
97
+ async extractFacts(content) {
98
+ const userMessage = buildExtractionPrompt(content);
99
+ const response = await chatCompletion(buildSystemPrompt(), userMessage);
100
+ try {
101
+ const parsed = JSON.parse(response);
102
+ const facts = parsed.facts;
103
+ if (!Array.isArray(facts))
104
+ return [];
105
+ return facts.filter((f) => typeof f === 'object' &&
106
+ f !== null &&
107
+ typeof f.fact === 'string' &&
108
+ typeof f.category === 'string');
109
+ }
110
+ catch {
111
+ return [];
112
+ }
113
+ }
114
+ async processFact(fact, source) {
115
+ const hash = hashMemory(fact.fact);
116
+ const vector = await this.embedding.embed(fact.fact);
117
+ const searchResults = await this.vectordb.search(COLLECTION_NAME, {
118
+ queryVector: vector,
119
+ queryText: fact.fact,
120
+ limit: SEARCH_CANDIDATES,
121
+ });
122
+ const candidates = [];
123
+ for (const result of searchResults) {
124
+ const id = result.relativePath;
125
+ if (!id)
126
+ continue;
127
+ const point = await this.vectordb.getById(COLLECTION_NAME, id);
128
+ if (!point)
129
+ continue;
130
+ candidates.push({
131
+ id,
132
+ memory: result.content,
133
+ hash: String(point.payload.hash ?? ''),
134
+ vector: point.vector,
135
+ score: result.score,
136
+ });
137
+ }
138
+ const decision = reconcile(hash, vector, candidates);
139
+ if (decision.action === 'NONE')
140
+ return null;
141
+ const now = new Date().toISOString();
142
+ if (decision.action === 'UPDATE' && decision.existingId) {
143
+ const existingPoint = await this.vectordb.getById(COLLECTION_NAME, decision.existingId);
144
+ const createdAt = String(existingPoint?.payload.created_at ?? now);
145
+ await this.vectordb.updatePoint(COLLECTION_NAME, decision.existingId, vector, {
146
+ content: fact.fact,
147
+ relativePath: decision.existingId,
148
+ fileExtension: fact.category,
149
+ language: source ?? '',
150
+ startLine: 0,
151
+ endLine: 0,
152
+ hash,
153
+ memory: fact.fact,
154
+ category: fact.category,
155
+ source: source ?? '',
156
+ created_at: createdAt,
157
+ updated_at: now,
158
+ });
159
+ this.history.log(decision.existingId, 'UPDATE', fact.fact, decision.existingMemory, source, now);
160
+ return {
161
+ event: 'UPDATE',
162
+ id: decision.existingId,
163
+ memory: fact.fact,
164
+ previous: decision.existingMemory,
165
+ category: fact.category,
166
+ source,
167
+ };
168
+ }
169
+ // ADD
170
+ const id = randomUUID();
171
+ await this.vectordb.updatePoint(COLLECTION_NAME, id, vector, {
172
+ content: fact.fact,
173
+ relativePath: id,
174
+ fileExtension: fact.category,
175
+ language: source ?? '',
176
+ startLine: 0,
177
+ endLine: 0,
178
+ hash,
179
+ memory: fact.fact,
180
+ category: fact.category,
181
+ source: source ?? '',
182
+ created_at: now,
183
+ updated_at: now,
184
+ });
185
+ this.history.log(id, 'ADD', fact.fact, null, source, now);
186
+ return {
187
+ event: 'ADD',
188
+ id,
189
+ memory: fact.fact,
190
+ category: fact.category,
191
+ source,
192
+ };
193
+ }
194
+ }
195
+ function payloadToMemoryItem(id, payload) {
196
+ return {
197
+ id,
198
+ memory: String(payload.memory ?? payload.content ?? ''),
199
+ hash: String(payload.hash ?? ''),
200
+ category: String(payload.category ?? payload.fileExtension ?? ''),
201
+ source: String(payload.source ?? payload.language ?? ''),
202
+ created_at: String(payload.created_at ?? ''),
203
+ updated_at: String(payload.updated_at ?? ''),
204
+ };
205
+ }
206
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1,28 @@
1
+ export interface MemoryItem {
2
+ id: string;
3
+ memory: string;
4
+ hash: string;
5
+ category: string;
6
+ source: string;
7
+ created_at: string;
8
+ updated_at: string;
9
+ }
10
+ export type MemoryEvent = 'ADD' | 'UPDATE' | 'DELETE';
11
+ export interface MemoryAction {
12
+ event: MemoryEvent;
13
+ id: string;
14
+ memory: string;
15
+ previous?: string;
16
+ category?: string;
17
+ source?: string;
18
+ }
19
+ export interface ReconcileResult {
20
+ action: 'ADD' | 'UPDATE' | 'NONE';
21
+ existingId?: string;
22
+ existingMemory?: string;
23
+ }
24
+ export interface ExtractedFact {
25
+ fact: string;
26
+ category: string;
27
+ }
28
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map