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/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
|
|
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 '
|
|
102
|
-
return {
|
|
103
|
-
|
|
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',
|
|
34
|
-
'
|
|
35
|
-
'--
|
|
36
|
-
|
|
37
|
-
'
|
|
38
|
-
'-
|
|
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,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
|