claude-eidetic 0.1.3 → 0.1.5

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 CHANGED
@@ -16,8 +16,7 @@ claude plugin install eidetics/claude-eidetic
16
16
  ```
17
17
 
18
18
  ```bash
19
- export OPENAI_API_KEY=sk-... # for embeddings (default)
20
- export ANTHROPIC_API_KEY=sk-ant-... # for memory extraction (default)
19
+ export OPENAI_API_KEY=sk-... # for embeddings (default)
21
20
  ```
22
21
 
23
22
  Index your codebase once, then search by meaning:
@@ -157,16 +156,13 @@ Add to your `.mcp.json`:
157
156
  "command": "npx",
158
157
  "args": ["-y", "claude-eidetic"],
159
158
  "env": {
160
- "OPENAI_API_KEY": "sk-...",
161
- "ANTHROPIC_API_KEY": "sk-ant-..."
159
+ "OPENAI_API_KEY": "sk-..."
162
160
  }
163
161
  }
164
162
  }
165
163
  }
166
164
  ```
167
165
 
168
- `ANTHROPIC_API_KEY` is needed for the memory LLM (default provider). Omit it if using `MEMORY_LLM_PROVIDER=openai` or `ollama`.
169
-
170
166
  ### Global install
171
167
 
172
168
  ```bash
@@ -184,7 +180,7 @@ npm install && npx tsc && npm start
184
180
  ### Requirements
185
181
 
186
182
  - Node.js >= 20.0.0
187
- - An API key (OpenAI for embeddings, Anthropic for memory extraction, or Ollama for both free)
183
+ - An API key (OpenAI for embeddings, or Ollama for free local embeddings)
188
184
  - Docker (optional): Qdrant auto-provisions via Docker if not already running
189
185
  - C/C++ build tools: required by tree-sitter native bindings (`node-gyp`)
190
186
 
@@ -207,8 +203,7 @@ export MEMORY_LLM_PROVIDER=ollama
207
203
 
208
204
  | Variable | Default | Description |
209
205
  |---|---|---|
210
- | `OPENAI_API_KEY` | _(required for openai)_ | OpenAI API key for embeddings and/or memory |
211
- | `ANTHROPIC_API_KEY` | _(required for anthropic memory)_ | Anthropic API key for memory LLM |
206
+ | `OPENAI_API_KEY` | _(required for openai)_ | OpenAI API key for embeddings |
212
207
  | `EMBEDDING_PROVIDER` | `openai` | `openai`, `ollama`, or `local` |
213
208
  | `EMBEDDING_MODEL` | `text-embedding-3-small` (openai) / `nomic-embed-text` (ollama) | Embedding model name |
214
209
  | `EMBEDDING_BATCH_SIZE` | `100` | Batch size for embedding requests (1-2048) |
@@ -223,10 +218,6 @@ export MEMORY_LLM_PROVIDER=ollama
223
218
  | `EIDETIC_DATA_DIR` | `~/.eidetic/` | Data root for snapshots, memory DB, registry |
224
219
  | `CUSTOM_EXTENSIONS` | `[]` | JSON array of extra file extensions to index (e.g., `[".dart",".arb"]`) |
225
220
  | `CUSTOM_IGNORE_PATTERNS` | `[]` | JSON array of glob patterns to exclude |
226
- | `MEMORY_LLM_PROVIDER` | `anthropic` | `anthropic`, `openai`, or `ollama` |
227
- | `MEMORY_LLM_MODEL` | `claude-haiku-4-5-20251001` (anthropic) / `gpt-4o-mini` (openai) / `llama3.2` (ollama) | Model for memory extraction |
228
- | `MEMORY_LLM_BASE_URL` | _(none)_ | Custom base URL for memory LLM |
229
- | `MEMORY_LLM_API_KEY` | _(none)_ | API key override for memory LLM |
230
221
 
231
222
  </details>
232
223
 
package/dist/config.d.ts CHANGED
@@ -15,11 +15,6 @@ declare const configSchema: z.ZodEffects<z.ZodObject<{
15
15
  eideticDataDir: z.ZodDefault<z.ZodString>;
16
16
  customExtensions: z.ZodEffects<z.ZodDefault<z.ZodArray<z.ZodString, "many">>, string[], unknown>;
17
17
  customIgnorePatterns: z.ZodEffects<z.ZodDefault<z.ZodArray<z.ZodString, "many">>, string[], unknown>;
18
- memoryLlmProvider: z.ZodDefault<z.ZodEnum<["openai", "ollama", "anthropic"]>>;
19
- memoryLlmModel: z.ZodOptional<z.ZodString>;
20
- memoryLlmBaseUrl: z.ZodOptional<z.ZodString>;
21
- memoryLlmApiKey: z.ZodOptional<z.ZodString>;
22
- anthropicApiKey: z.ZodDefault<z.ZodString>;
23
18
  }, "strip", z.ZodTypeAny, {
24
19
  embeddingProvider: "openai" | "ollama" | "local";
25
20
  openaiApiKey: string;
@@ -32,15 +27,10 @@ declare const configSchema: z.ZodEffects<z.ZodObject<{
32
27
  eideticDataDir: string;
33
28
  customExtensions: string[];
34
29
  customIgnorePatterns: string[];
35
- memoryLlmProvider: "openai" | "ollama" | "anthropic";
36
- anthropicApiKey: string;
37
30
  openaiBaseUrl?: string | undefined;
38
31
  embeddingModel?: string | undefined;
39
32
  qdrantApiKey?: string | undefined;
40
33
  milvusToken?: string | undefined;
41
- memoryLlmModel?: string | undefined;
42
- memoryLlmBaseUrl?: string | undefined;
43
- memoryLlmApiKey?: string | undefined;
44
34
  }, {
45
35
  embeddingProvider?: "openai" | "ollama" | "local" | undefined;
46
36
  openaiApiKey?: string | undefined;
@@ -57,14 +47,8 @@ declare const configSchema: z.ZodEffects<z.ZodObject<{
57
47
  eideticDataDir?: string | undefined;
58
48
  customExtensions?: unknown;
59
49
  customIgnorePatterns?: unknown;
60
- memoryLlmProvider?: "openai" | "ollama" | "anthropic" | undefined;
61
- memoryLlmModel?: string | undefined;
62
- memoryLlmBaseUrl?: string | undefined;
63
- memoryLlmApiKey?: string | undefined;
64
- anthropicApiKey?: string | undefined;
65
50
  }>, {
66
51
  embeddingModel: string;
67
- memoryLlmModel: string;
68
52
  embeddingProvider: "openai" | "ollama" | "local";
69
53
  openaiApiKey: string;
70
54
  ollamaBaseUrl: string;
@@ -76,13 +60,9 @@ declare const configSchema: z.ZodEffects<z.ZodObject<{
76
60
  eideticDataDir: string;
77
61
  customExtensions: string[];
78
62
  customIgnorePatterns: string[];
79
- memoryLlmProvider: "openai" | "ollama" | "anthropic";
80
- anthropicApiKey: string;
81
63
  openaiBaseUrl?: string | undefined;
82
64
  qdrantApiKey?: string | undefined;
83
65
  milvusToken?: string | undefined;
84
- memoryLlmBaseUrl?: string | undefined;
85
- memoryLlmApiKey?: string | undefined;
86
66
  }, {
87
67
  embeddingProvider?: "openai" | "ollama" | "local" | undefined;
88
68
  openaiApiKey?: string | undefined;
@@ -99,11 +79,6 @@ declare const configSchema: z.ZodEffects<z.ZodObject<{
99
79
  eideticDataDir?: string | undefined;
100
80
  customExtensions?: unknown;
101
81
  customIgnorePatterns?: unknown;
102
- memoryLlmProvider?: "openai" | "ollama" | "anthropic" | undefined;
103
- memoryLlmModel?: string | undefined;
104
- memoryLlmBaseUrl?: string | undefined;
105
- memoryLlmApiKey?: string | undefined;
106
- anthropicApiKey?: string | undefined;
107
82
  }>;
108
83
  export type Config = z.infer<typeof configSchema>;
109
84
  export declare function loadConfig(): Config;
package/dist/config.js CHANGED
@@ -19,24 +19,12 @@ const configSchema = z
19
19
  eideticDataDir: z.string().default(path.join(os.homedir(), '.eidetic')),
20
20
  customExtensions: z.preprocess((val) => (typeof val === 'string' ? JSON.parse(val) : val), z.array(z.string()).default([])),
21
21
  customIgnorePatterns: z.preprocess((val) => (typeof val === 'string' ? JSON.parse(val) : val), z.array(z.string()).default([])),
22
- memoryLlmProvider: z.enum(['openai', 'ollama', 'anthropic']).default('anthropic'),
23
- memoryLlmModel: z.string().optional(),
24
- memoryLlmBaseUrl: z.string().optional(),
25
- memoryLlmApiKey: z.string().optional(),
26
- anthropicApiKey: z.string().default(''),
27
22
  })
28
23
  .transform((cfg) => ({
29
24
  ...cfg,
30
25
  // Default embedding model depends on provider
31
26
  embeddingModel: cfg.embeddingModel ??
32
27
  (cfg.embeddingProvider === 'ollama' ? 'nomic-embed-text' : 'text-embedding-3-small'),
33
- // Default memory LLM model depends on provider
34
- memoryLlmModel: cfg.memoryLlmModel ??
35
- (cfg.memoryLlmProvider === 'ollama'
36
- ? 'llama3.2'
37
- : cfg.memoryLlmProvider === 'anthropic'
38
- ? 'claude-haiku-4-5-20251001'
39
- : 'gpt-4o-mini'),
40
28
  }));
41
29
  let cachedConfig = null;
42
30
  export function loadConfig() {
@@ -56,11 +44,6 @@ export function loadConfig() {
56
44
  eideticDataDir: process.env.EIDETIC_DATA_DIR,
57
45
  customExtensions: process.env.CUSTOM_EXTENSIONS,
58
46
  customIgnorePatterns: process.env.CUSTOM_IGNORE_PATTERNS,
59
- memoryLlmProvider: process.env.MEMORY_LLM_PROVIDER,
60
- memoryLlmModel: process.env.MEMORY_LLM_MODEL || undefined,
61
- memoryLlmBaseUrl: process.env.MEMORY_LLM_BASE_URL || undefined,
62
- memoryLlmApiKey: process.env.MEMORY_LLM_API_KEY?.trim().replace(/^["']|["']$/g, '') || undefined,
63
- anthropicApiKey: (process.env.ANTHROPIC_API_KEY ?? '').trim().replace(/^["']|["']$/g, ''),
64
47
  };
65
48
  const result = configSchema.safeParse(raw);
66
49
  if (!result.success) {
@@ -148,7 +148,7 @@ export class OpenAIEmbedding {
148
148
  }
149
149
  async callWithRetry(texts) {
150
150
  let currentBatchSize = texts.length;
151
- for (let attempt = 0; attempt <= RETRY_DELAYS.length; attempt++) {
151
+ for (let attempt = 0; attempt < RETRY_DELAYS.length + 1; attempt++) {
152
152
  try {
153
153
  const allResults = [];
154
154
  for (let offset = 0; offset < texts.length; offset += currentBatchSize) {
package/dist/format.js CHANGED
@@ -156,7 +156,8 @@ export function formatMemoryActions(actions) {
156
156
  for (const action of actions) {
157
157
  const icon = action.event === 'ADD' ? '+' : '~';
158
158
  lines.push(` ${icon} [${action.event}] ${action.memory}`);
159
- lines.push(` Category: ${action.category ?? 'unknown'} | ID: ${action.id}`);
159
+ const projectTag = action.project && action.project !== 'global' ? ` | Project: ${action.project}` : '';
160
+ lines.push(` Category: ${action.category ?? 'unknown'} | ID: ${action.id}${projectTag}`);
160
161
  if (action.previous) {
161
162
  lines.push(` Previous: ${action.previous}`);
162
163
  }
@@ -171,9 +172,13 @@ export function formatMemorySearchResults(items, query) {
171
172
  for (let i = 0; i < items.length; i++) {
172
173
  const m = items[i];
173
174
  lines.push(`${i + 1}. ${m.memory}`);
174
- lines.push(` Category: ${m.category} | ID: ${m.id}`);
175
+ const projectTag = m.project && m.project !== 'global' ? ` | Project: ${m.project}` : '';
176
+ lines.push(` Category: ${m.category} | ID: ${m.id}${projectTag}`);
175
177
  if (m.source)
176
178
  lines.push(` Source: ${m.source}`);
179
+ if (m.access_count > 0) {
180
+ lines.push(` Accessed: ${m.access_count}x | Last: ${m.last_accessed.slice(0, 10)}`);
181
+ }
177
182
  if (m.created_at || m.updated_at) {
178
183
  lines.push(` Created: ${m.created_at || 'unknown'} | Updated: ${m.updated_at || 'unknown'}`);
179
184
  }
@@ -200,7 +205,9 @@ export function formatMemoryList(items) {
200
205
  lines.push(`### ${category} (${memories.length})`);
201
206
  for (const m of memories) {
202
207
  const updatedDate = m.updated_at ? ` (updated: ${m.updated_at.slice(0, 10)})` : '';
203
- lines.push(` - ${m.memory} [${m.id.slice(0, 8)}...]${updatedDate}`);
208
+ const projectTag = m.project && m.project !== 'global' ? ` [${m.project}]` : '';
209
+ const accessTag = m.access_count > 0 ? ` (accessed: ${m.access_count}x)` : '';
210
+ lines.push(` - ${m.memory} [${m.id.slice(0, 8)}...]${updatedDate}${projectTag}${accessTag}`);
204
211
  }
205
212
  lines.push('');
206
213
  }
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PostToolUse automatic memory extraction.
4
+ * Replaces the nudge-based memory-nudge.sh approach.
5
+ *
6
+ * Reads PostToolUse stdin JSON (includes tool_response), pattern-matches on outcomes,
7
+ * and stores facts directly via MemoryStore — no Claude involvement required.
8
+ *
9
+ * Matches: WebFetch | Bash
10
+ */
11
+ export {};
12
+ //# sourceMappingURL=post-tool-extract.d.ts.map
@@ -0,0 +1,210 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PostToolUse automatic memory extraction.
4
+ * Replaces the nudge-based memory-nudge.sh approach.
5
+ *
6
+ * Reads PostToolUse stdin JSON (includes tool_response), pattern-matches on outcomes,
7
+ * and stores facts directly via MemoryStore — no Claude involvement required.
8
+ *
9
+ * Matches: WebFetch | Bash
10
+ */
11
+ import { execSync } from 'node:child_process';
12
+ import path from 'node:path';
13
+ const MAX_RESPONSE_SIZE = 10_000; // Skip extraction for large responses (data dumps)
14
+ async function main() {
15
+ let input;
16
+ try {
17
+ const raw = await readStdin();
18
+ input = JSON.parse(raw);
19
+ }
20
+ catch {
21
+ // Can't parse stdin — exit silently
22
+ writeOutput();
23
+ return;
24
+ }
25
+ const toolName = input.tool_name ?? '';
26
+ if (toolName !== 'WebFetch' && toolName !== 'Bash') {
27
+ writeOutput();
28
+ return;
29
+ }
30
+ const responseStr = stringifyResponse(input.tool_response);
31
+ // Skip if response is too large (likely a successful data dump, not an error)
32
+ if (responseStr.length > MAX_RESPONSE_SIZE) {
33
+ writeOutput();
34
+ return;
35
+ }
36
+ const facts = extractFacts(toolName, input.tool_input ?? {}, responseStr);
37
+ if (facts.length === 0) {
38
+ writeOutput();
39
+ return;
40
+ }
41
+ try {
42
+ const cwd = input.cwd ?? process.cwd();
43
+ const project = detectProject(cwd);
44
+ const [{ loadConfig }, { createEmbedding }, { MemoryHistory }, { MemoryStore }] = await Promise.all([
45
+ import('../config.js'),
46
+ import('../embedding/factory.js'),
47
+ import('../memory/history.js'),
48
+ import('../memory/store.js'),
49
+ ]);
50
+ const config = loadConfig();
51
+ const embedding = createEmbedding(config);
52
+ await embedding.initialize();
53
+ let vectordb;
54
+ if (config.vectordbProvider === 'milvus') {
55
+ const { MilvusVectorDB } = await import('../vectordb/milvus.js');
56
+ vectordb = new MilvusVectorDB();
57
+ }
58
+ else {
59
+ const { QdrantVectorDB } = await import('../vectordb/qdrant.js');
60
+ vectordb = new QdrantVectorDB(config.qdrantUrl, config.qdrantApiKey);
61
+ }
62
+ // Quick-exit if memory collection doesn't exist yet
63
+ const exists = await vectordb.hasCollection('eidetic_memory');
64
+ if (!exists) {
65
+ writeOutput();
66
+ return;
67
+ }
68
+ const { getMemoryDbPath } = await import('../paths.js');
69
+ const history = new MemoryHistory(getMemoryDbPath());
70
+ const store = new MemoryStore(embedding, vectordb, history);
71
+ await store.addMemory(facts, 'post-tool-extract', project);
72
+ }
73
+ catch (err) {
74
+ process.stderr.write(`post-tool-extract failed: ${String(err)}\n`);
75
+ }
76
+ writeOutput();
77
+ }
78
+ function extractFacts(toolName, toolInput, responseStr) {
79
+ const facts = [];
80
+ if (toolName === 'WebFetch') {
81
+ const url = String(toolInput.url ?? '');
82
+ facts.push(...extractWebFetchFacts(url, responseStr));
83
+ }
84
+ else if (toolName === 'Bash') {
85
+ const command = String(toolInput.command ?? '');
86
+ facts.push(...extractBashFacts(command, responseStr));
87
+ }
88
+ return facts;
89
+ }
90
+ function extractWebFetchFacts(url, responseStr) {
91
+ const facts = [];
92
+ const lower = responseStr.toLowerCase();
93
+ // Detect 404 / not found
94
+ if (/\b404\b/.test(responseStr) ||
95
+ lower.includes('page not found') ||
96
+ lower.includes('not found')) {
97
+ if (url) {
98
+ facts.push({
99
+ fact: `URL ${url} returned 404 / not found`,
100
+ category: 'debugging',
101
+ });
102
+ }
103
+ return facts;
104
+ }
105
+ // Detect redirect notice
106
+ const redirectMatch = /redirected?\s+to\s+(https?:\/\/\S+)/i.exec(responseStr);
107
+ if (redirectMatch) {
108
+ facts.push({
109
+ fact: `URL ${url} redirects to ${redirectMatch[1]}`,
110
+ category: 'tools',
111
+ });
112
+ return facts;
113
+ }
114
+ // Detect other errors
115
+ if (/(?:error|fail|403|ENOENT|EACCES)/i.test(responseStr)) {
116
+ const snippet = responseStr.slice(0, 150).replace(/\n/g, ' ').trim();
117
+ facts.push({
118
+ fact: `Fetching ${url} failed: ${snippet}`,
119
+ category: 'debugging',
120
+ });
121
+ return facts;
122
+ }
123
+ // Successful fetch — extract URL + key finding (first 200 chars)
124
+ if (url) {
125
+ const snippet = responseStr.slice(0, 200).replace(/\n/g, ' ').trim();
126
+ if (snippet.length > 20) {
127
+ facts.push({
128
+ fact: `Docs at ${url}: ${snippet}`,
129
+ category: 'tools',
130
+ });
131
+ }
132
+ }
133
+ return facts;
134
+ }
135
+ function extractBashFacts(command, responseStr) {
136
+ const facts = [];
137
+ const lower = responseStr.toLowerCase();
138
+ const isError = /(?:error|fail|not.found|command not found|ENOENT|EACCES|no such file)/i.test(responseStr) ||
139
+ lower.includes('exit code') ||
140
+ lower.includes('permission denied');
141
+ if (isError) {
142
+ const snippet = responseStr.slice(0, 200).replace(/\n/g, ' ').trim();
143
+ const shortCmd = command.slice(0, 100);
144
+ facts.push({
145
+ fact: `Command '${shortCmd}' failed: ${snippet}`,
146
+ category: 'debugging',
147
+ });
148
+ return facts;
149
+ }
150
+ // Detect successful installs
151
+ const installMatch = /(?:npm|yarn|pnpm|pip|pip3|brew|apt|apt-get|cargo)\s+(?:install|add|i)\s+(.+)/.exec(command);
152
+ if (installMatch) {
153
+ const pkg = installMatch[1].trim().slice(0, 80);
154
+ const manager = command.split(' ')[0];
155
+ facts.push({
156
+ fact: `Installed ${pkg} via ${manager}`,
157
+ category: 'tools',
158
+ });
159
+ return facts;
160
+ }
161
+ // Detect config commands (git config, npm config, etc.)
162
+ if (/\bconfig\b/.test(command) && !isError) {
163
+ const shortCmd = command.slice(0, 120).replace(/\n/g, ' ').trim();
164
+ facts.push({
165
+ fact: `Configured: ${shortCmd}`,
166
+ category: 'workflow',
167
+ });
168
+ }
169
+ return facts;
170
+ }
171
+ function detectProject(cwd) {
172
+ try {
173
+ const result = execSync('git rev-parse --show-toplevel', {
174
+ cwd,
175
+ encoding: 'utf-8',
176
+ stdio: ['pipe', 'pipe', 'pipe'],
177
+ });
178
+ return path.basename(result.trim());
179
+ }
180
+ catch {
181
+ return 'global';
182
+ }
183
+ }
184
+ function stringifyResponse(response) {
185
+ if (typeof response === 'string')
186
+ return response;
187
+ if (response === null || response === undefined)
188
+ return '';
189
+ try {
190
+ return JSON.stringify(response);
191
+ }
192
+ catch {
193
+ return String(response);
194
+ }
195
+ }
196
+ function readStdin() {
197
+ return new Promise((resolve, reject) => {
198
+ const chunks = [];
199
+ process.stdin.on('data', (chunk) => chunks.push(chunk));
200
+ process.stdin.on('end', () => {
201
+ resolve(Buffer.concat(chunks).toString('utf-8'));
202
+ });
203
+ process.stdin.on('error', reject);
204
+ });
205
+ }
206
+ function writeOutput() {
207
+ process.stdout.write(JSON.stringify({ hookSpecificOutput: {} }) + '\n');
208
+ }
209
+ void main();
210
+ //# sourceMappingURL=post-tool-extract.js.map
package/dist/index.js CHANGED
@@ -58,7 +58,7 @@ const WORKFLOW_GUIDANCE = `# Eidetic Code Search Workflow
58
58
  - Stale docs (past TTL) still return results but are flagged \`[STALE]\`
59
59
 
60
60
  **Persistent memory (cross-session developer knowledge):**
61
- - \`add_memory(content="...")\` → extracts facts about coding style, tools, architecture, etc.
61
+ - \`add_memory(facts=[{fact:"...", category:"..."}])\` → stores pre-extracted facts about coding style, tools, architecture, etc.
62
62
  - \`search_memory(query="...")\` → find relevant memories by semantic search
63
63
  - \`list_memories()\` → see all stored memories grouped by category
64
64
  - \`delete_memory(id="...")\` → remove a specific memory
@@ -1,6 +1,6 @@
1
1
  import type { Embedding } from '../embedding/types.js';
2
2
  import type { VectorDB } from '../vectordb/types.js';
3
- import type { MemoryItem, MemoryAction } from './types.js';
3
+ import type { MemoryItem, MemoryAction, ExtractedFact } from './types.js';
4
4
  import { MemoryHistory } from './history.js';
5
5
  export declare class MemoryStore {
6
6
  private embedding;
@@ -9,12 +9,12 @@ export declare class MemoryStore {
9
9
  private initialized;
10
10
  constructor(embedding: Embedding, vectordb: VectorDB, history: MemoryHistory);
11
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[]>;
12
+ addMemory(facts: ExtractedFact[], source?: string, project?: string): Promise<MemoryAction[]>;
13
+ searchMemory(query: string, limit?: number, category?: string, project?: string): Promise<MemoryItem[]>;
14
+ listMemories(category?: string, limit?: number, project?: string): Promise<MemoryItem[]>;
15
15
  deleteMemory(id: string): Promise<boolean>;
16
16
  getHistory(memoryId: string): import("./history.js").HistoryEntry[];
17
- private extractFacts;
17
+ private bumpAccessCounts;
18
18
  private processFact;
19
19
  }
20
20
  //# sourceMappingURL=store.d.ts.map
@@ -1,15 +1,15 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { chatCompletion } from './llm.js';
3
- import { buildSystemPrompt, buildExtractionPrompt } from './prompts.js';
4
2
  import { hashMemory, reconcile } from './reconciler.js';
5
3
  const COLLECTION_NAME = 'eidetic_memory';
6
4
  const SEARCH_CANDIDATES = 5;
5
+ const ACCESS_BUMP_COUNT = 5;
7
6
  // Data model mapping (reuses existing VectorDB/SearchResult fields):
8
7
  // content → memory text (full-text search)
9
8
  // relativePath → memory UUID (enables deleteByPath for single-memory deletion)
10
9
  // fileExtension→ category (enables extensionFilter for category filtering)
11
10
  // language → source
12
- // Additional payload: hash, memory, category, source, created_at, updated_at
11
+ // Additional payload: hash, memory, category, source, project,
12
+ // access_count, last_accessed, created_at, updated_at
13
13
  export class MemoryStore {
14
14
  embedding;
15
15
  vectordb;
@@ -29,26 +29,27 @@ export class MemoryStore {
29
29
  }
30
30
  this.initialized = true;
31
31
  }
32
- async addMemory(content, source) {
32
+ async addMemory(facts, source, project = 'global') {
33
33
  await this.ensureCollection();
34
- const facts = await this.extractFacts(content);
35
34
  if (facts.length === 0)
36
35
  return [];
37
36
  const actions = [];
38
37
  for (const fact of facts) {
39
- const action = await this.processFact(fact, source);
38
+ const action = await this.processFact(fact, source, project);
40
39
  if (action)
41
40
  actions.push(action);
42
41
  }
43
42
  return actions;
44
43
  }
45
- async searchMemory(query, limit = 10, category) {
44
+ async searchMemory(query, limit = 10, category, project) {
46
45
  await this.ensureCollection();
47
46
  const queryVector = await this.embedding.embed(query);
47
+ // Fetch extra candidates for project re-ranking when project is specified
48
+ const fetchLimit = project ? limit * 2 : limit;
48
49
  const results = await this.vectordb.search(COLLECTION_NAME, {
49
50
  queryVector,
50
51
  queryText: query,
51
- limit,
52
+ limit: fetchLimit,
52
53
  ...(category ? { extensionFilter: [category] } : {}),
53
54
  });
54
55
  // Enrich with full payload from getById
@@ -60,9 +61,19 @@ export class MemoryStore {
60
61
  continue;
61
62
  items.push(payloadToMemoryItem(id, point.payload));
62
63
  }
63
- return items;
64
+ // Project re-ranking: boost project-matching items to the front
65
+ let ranked = items;
66
+ if (project) {
67
+ const projectItems = items.filter((m) => m.project === project);
68
+ const otherItems = items.filter((m) => m.project !== project);
69
+ ranked = [...projectItems, ...otherItems].slice(0, limit);
70
+ }
71
+ // Fire-and-forget: bump access_count and last_accessed for top results
72
+ const topIds = ranked.slice(0, ACCESS_BUMP_COUNT).map((m) => m.id);
73
+ void this.bumpAccessCounts(topIds);
74
+ return ranked;
64
75
  }
65
- async listMemories(category, limit = 50) {
76
+ async listMemories(category, limit = 50, project) {
66
77
  await this.ensureCollection();
67
78
  const queryVector = await this.embedding.embed('developer knowledge');
68
79
  const results = await this.vectordb.search(COLLECTION_NAME, {
@@ -79,6 +90,10 @@ export class MemoryStore {
79
90
  continue;
80
91
  items.push(payloadToMemoryItem(id, point.payload));
81
92
  }
93
+ // Filter by project if specified
94
+ if (project) {
95
+ return items.filter((m) => m.project === project || m.project === 'global');
96
+ }
82
97
  return items;
83
98
  }
84
99
  async deleteMemory(id) {
@@ -94,24 +109,26 @@ export class MemoryStore {
94
109
  getHistory(memoryId) {
95
110
  return this.history.getHistory(memoryId);
96
111
  }
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
+ async bumpAccessCounts(ids) {
113
+ const now = new Date().toISOString();
114
+ for (const id of ids) {
115
+ try {
116
+ const point = await this.vectordb.getById(COLLECTION_NAME, id);
117
+ if (!point)
118
+ continue;
119
+ const currentCount = Number(point.payload.access_count ?? 0);
120
+ await this.vectordb.updatePoint(COLLECTION_NAME, id, point.vector, {
121
+ ...point.payload,
122
+ access_count: currentCount + 1,
123
+ last_accessed: now,
124
+ });
125
+ }
126
+ catch {
127
+ // Silently ignore — access tracking is a best-effort utility signal
128
+ }
112
129
  }
113
130
  }
114
- async processFact(fact, source) {
131
+ async processFact(fact, source, project = 'global') {
115
132
  const hash = hashMemory(fact.fact);
116
133
  const vector = await this.embedding.embed(fact.fact);
117
134
  const searchResults = await this.vectordb.search(COLLECTION_NAME, {
@@ -139,9 +156,13 @@ export class MemoryStore {
139
156
  if (decision.action === 'NONE')
140
157
  return null;
141
158
  const now = new Date().toISOString();
159
+ const effectiveProject = fact.project ?? project;
142
160
  if (decision.action === 'UPDATE' && decision.existingId) {
143
161
  const existingPoint = await this.vectordb.getById(COLLECTION_NAME, decision.existingId);
144
162
  const createdAt = String(existingPoint?.payload.created_at ?? now);
163
+ // Preserve existing access tracking
164
+ const existingAccessCount = Number(existingPoint?.payload.access_count ?? 0);
165
+ const existingLastAccessed = String(existingPoint?.payload.last_accessed ?? '');
145
166
  await this.vectordb.updatePoint(COLLECTION_NAME, decision.existingId, vector, {
146
167
  content: fact.fact,
147
168
  relativePath: decision.existingId,
@@ -153,6 +174,9 @@ export class MemoryStore {
153
174
  memory: fact.fact,
154
175
  category: fact.category,
155
176
  source: source ?? '',
177
+ project: effectiveProject,
178
+ access_count: existingAccessCount,
179
+ last_accessed: existingLastAccessed,
156
180
  created_at: createdAt,
157
181
  updated_at: now,
158
182
  });
@@ -164,6 +188,7 @@ export class MemoryStore {
164
188
  previous: decision.existingMemory,
165
189
  category: fact.category,
166
190
  source,
191
+ project: effectiveProject,
167
192
  };
168
193
  }
169
194
  // ADD
@@ -179,6 +204,9 @@ export class MemoryStore {
179
204
  memory: fact.fact,
180
205
  category: fact.category,
181
206
  source: source ?? '',
207
+ project: effectiveProject,
208
+ access_count: 0,
209
+ last_accessed: '',
182
210
  created_at: now,
183
211
  updated_at: now,
184
212
  });
@@ -189,6 +217,7 @@ export class MemoryStore {
189
217
  memory: fact.fact,
190
218
  category: fact.category,
191
219
  source,
220
+ project: effectiveProject,
192
221
  };
193
222
  }
194
223
  }
@@ -199,6 +228,9 @@ function payloadToMemoryItem(id, payload) {
199
228
  hash: String(payload.hash ?? ''),
200
229
  category: String(payload.category ?? payload.fileExtension ?? ''),
201
230
  source: String(payload.source ?? payload.language ?? ''),
231
+ project: String(payload.project ?? 'global'),
232
+ access_count: Number(payload.access_count ?? 0),
233
+ last_accessed: String(payload.last_accessed ?? ''),
202
234
  created_at: String(payload.created_at ?? ''),
203
235
  updated_at: String(payload.updated_at ?? ''),
204
236
  };
@@ -4,6 +4,9 @@ export interface MemoryItem {
4
4
  hash: string;
5
5
  category: string;
6
6
  source: string;
7
+ project: string;
8
+ access_count: number;
9
+ last_accessed: string;
7
10
  created_at: string;
8
11
  updated_at: string;
9
12
  }
@@ -15,6 +18,7 @@ export interface MemoryAction {
15
18
  previous?: string;
16
19
  category?: string;
17
20
  source?: string;
21
+ project?: string;
18
22
  }
19
23
  export interface ReconcileResult {
20
24
  action: 'ADD' | 'UPDATE' | 'NONE';
@@ -24,5 +28,6 @@ export interface ReconcileResult {
24
28
  export interface ExtractedFact {
25
29
  fact: string;
26
30
  category: string;
31
+ project?: string;
27
32
  }
28
33
  //# sourceMappingURL=types.d.ts.map
@@ -3,7 +3,7 @@
3
3
  * Hook entry point for PreCompact and SessionEnd events.
4
4
  *
5
5
  * PreCompact: Parses transcript, writes session note, updates index, spawns background indexer.
6
- * SessionEnd: Same as PreCompact + runs memory extraction pipeline (semantic facts Qdrant).
6
+ * SessionEnd: Same as PreCompact (writes session note if not already captured by PreCompact).
7
7
  */
8
8
  export {};
9
9
  //# sourceMappingURL=hook.d.ts.map
@@ -3,7 +3,7 @@
3
3
  * Hook entry point for PreCompact and SessionEnd events.
4
4
  *
5
5
  * PreCompact: Parses transcript, writes session note, updates index, spawns background indexer.
6
- * SessionEnd: Same as PreCompact + runs memory extraction pipeline (semantic facts Qdrant).
6
+ * SessionEnd: Same as PreCompact (writes session note if not already captured by PreCompact).
7
7
  */
8
8
  import { z } from 'zod';
9
9
  import path from 'node:path';
@@ -68,14 +68,11 @@ async function main() {
68
68
  updateSessionIndex(notesDir, session, noteFile);
69
69
  spawnBackgroundIndexer(notesDir, INDEX_RUNNER_PATH);
70
70
  }
71
- // Run memory extraction (best-effort — graceful failure if Qdrant unavailable)
72
- const memoryActions = await extractMemories(session);
73
71
  outputSuccess({
74
72
  noteFile,
75
73
  skippedNote,
76
74
  filesModified: session.filesModified.length,
77
75
  tasksCreated: session.tasksCreated.length,
78
- memoriesExtracted: memoryActions,
79
76
  });
80
77
  }
81
78
  else {
@@ -94,61 +91,6 @@ async function main() {
94
91
  outputError(err instanceof Error ? err.message : String(err));
95
92
  }
96
93
  }
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
94
  async function readStdin() {
153
95
  const chunks = [];
154
96
  for await (const chunk of process.stdin) {
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Inject stored memories at SessionStart.
4
+ * Called by session-start hook to surface previously learned knowledge.
5
+ *
6
+ * Outputs markdown to stdout for hook to capture and inject into session.
7
+ */
8
+ import type { MemoryItem } from '../memory/types.js';
9
+ export declare function formatMemoryContext(memories: MemoryItem[]): string;
10
+ //# sourceMappingURL=memory-inject.d.ts.map
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Inject stored memories at SessionStart.
4
+ * Called by session-start hook to surface previously learned knowledge.
5
+ *
6
+ * Outputs markdown 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
+ async function main() {
11
+ try {
12
+ // Get cwd from environment (set by Claude Code) or detect from git
13
+ const cwd = process.env.CLAUDE_CWD || process.cwd();
14
+ // Detect project root from git
15
+ const projectPath = detectProjectRoot(cwd);
16
+ if (!projectPath) {
17
+ // Not in a git repo, nothing to inject
18
+ return;
19
+ }
20
+ const projectName = path.basename(projectPath);
21
+ // Dynamic imports — avoid loading heavy deps if not needed
22
+ const [{ loadConfig }, { createEmbedding }, { MemoryHistory }, { MemoryStore }] = await Promise.all([
23
+ import('../config.js'),
24
+ import('../embedding/factory.js'),
25
+ import('../memory/history.js'),
26
+ import('../memory/store.js'),
27
+ ]);
28
+ const config = loadConfig();
29
+ const embedding = createEmbedding(config);
30
+ await embedding.initialize();
31
+ // Respect VECTORDB_PROVIDER — connect without bootstrapping (hook assumes DB is already running)
32
+ let vectordb;
33
+ if (config.vectordbProvider === 'milvus') {
34
+ const { MilvusVectorDB } = await import('../vectordb/milvus.js');
35
+ vectordb = new MilvusVectorDB();
36
+ }
37
+ else {
38
+ const { QdrantVectorDB } = await import('../vectordb/qdrant.js');
39
+ vectordb = new QdrantVectorDB(config.qdrantUrl, config.qdrantApiKey);
40
+ }
41
+ // Quick-exit if no memory collection exists
42
+ const exists = await vectordb.hasCollection('eidetic_memory');
43
+ if (!exists) {
44
+ return;
45
+ }
46
+ const { getMemoryDbPath } = await import('../paths.js');
47
+ const history = new MemoryHistory(getMemoryDbPath());
48
+ const store = new MemoryStore(embedding, vectordb, history);
49
+ const memories = await store.searchMemory(`${projectName} development knowledge`, 7, undefined, projectName);
50
+ if (memories.length === 0) {
51
+ return;
52
+ }
53
+ process.stdout.write(formatMemoryContext(memories));
54
+ }
55
+ catch (err) {
56
+ // Write to stderr for debugging, but don't break session start
57
+ process.stderr.write(`Memory inject failed: ${String(err)}\n`);
58
+ }
59
+ }
60
+ function detectProjectRoot(cwd) {
61
+ try {
62
+ const result = execSync('git rev-parse --show-toplevel', {
63
+ cwd,
64
+ encoding: 'utf-8',
65
+ stdio: ['pipe', 'pipe', 'pipe'],
66
+ });
67
+ return result.trim();
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ }
73
+ export function formatMemoryContext(memories) {
74
+ const lines = [];
75
+ lines.push('## Remembered Knowledge');
76
+ for (const m of memories) {
77
+ const category = m.category ? `[${m.category}] ` : '';
78
+ lines.push(`- ${category}${m.memory}`);
79
+ }
80
+ lines.push('');
81
+ lines.push('_search_memory(query) for more. add_memory(facts) to save new findings._');
82
+ lines.push('');
83
+ return lines.join('\n');
84
+ }
85
+ void main();
86
+ //# sourceMappingURL=memory-inject.js.map
@@ -229,24 +229,43 @@ export declare const TOOL_DEFINITIONS: readonly [{
229
229
  };
230
230
  }, {
231
231
  readonly name: "add_memory";
232
- readonly description: "Extract and store developer knowledge from text. Uses LLM to identify facts about coding style, tools, architecture, conventions, debugging insights, and workflow preferences. Automatically deduplicates against existing memories.";
232
+ readonly description: "Store pre-extracted developer knowledge facts. Before calling, extract facts yourself from the relevant content. Each fact should be a concise, self-contained statement about coding style, tools, architecture, conventions, debugging insights, or workflow preferences. Automatically deduplicates against existing memories.";
233
233
  readonly inputSchema: {
234
234
  readonly type: "object";
235
235
  readonly properties: {
236
- readonly content: {
237
- readonly type: "string";
238
- readonly description: "Text containing developer knowledge to extract and store (conversation snippets, notes, preferences).";
236
+ readonly facts: {
237
+ readonly type: "array";
238
+ readonly description: "Array of facts to store. Extract these yourself before calling. Each fact must be a concise, self-contained statement.";
239
+ readonly items: {
240
+ readonly type: "object";
241
+ readonly properties: {
242
+ readonly fact: {
243
+ readonly type: "string";
244
+ readonly description: "A concise, self-contained statement of a developer preference or convention.";
245
+ };
246
+ readonly category: {
247
+ readonly type: "string";
248
+ readonly description: "Category: coding_style, tools, architecture, conventions, debugging, workflow, or preferences.";
249
+ readonly enum: readonly ["coding_style", "tools", "architecture", "conventions", "debugging", "workflow", "preferences"];
250
+ };
251
+ };
252
+ readonly required: readonly ["fact", "category"];
253
+ };
239
254
  };
240
255
  readonly source: {
241
256
  readonly type: "string";
242
257
  readonly description: "Optional source identifier (e.g., \"conversation\", \"claude-code\", \"user-note\").";
243
258
  };
259
+ readonly project: {
260
+ readonly type: "string";
261
+ readonly description: "Optional project name to scope this memory (e.g., \"my-app\"). Defaults to \"global\" for cross-project memories.";
262
+ };
244
263
  };
245
- readonly required: readonly ["content"];
264
+ readonly required: readonly ["facts"];
246
265
  };
247
266
  }, {
248
267
  readonly name: "search_memory";
249
- readonly description: "Search stored developer memories using natural language. Returns semantically similar memories ranked by relevance.";
268
+ readonly description: "Search stored developer memories using natural language. Returns semantically similar memories ranked by relevance. When a project is specified, project-specific memories are ranked higher.";
250
269
  readonly inputSchema: {
251
270
  readonly type: "object";
252
271
  readonly properties: {
@@ -265,12 +284,16 @@ export declare const TOOL_DEFINITIONS: readonly [{
265
284
  readonly description: "Filter by category: coding_style, tools, architecture, conventions, debugging, workflow, preferences.";
266
285
  readonly enum: readonly ["coding_style", "tools", "architecture", "conventions", "debugging", "workflow", "preferences"];
267
286
  };
287
+ readonly project: {
288
+ readonly type: "string";
289
+ readonly description: "Optional project name to boost project-specific memories in results (e.g., \"my-app\"). Cross-project memories still appear but ranked lower.";
290
+ };
268
291
  };
269
292
  readonly required: readonly ["query"];
270
293
  };
271
294
  }, {
272
295
  readonly name: "list_memories";
273
- readonly description: "List all stored developer memories, optionally filtered by category.";
296
+ readonly description: "List all stored developer memories, optionally filtered by category or project.";
274
297
  readonly inputSchema: {
275
298
  readonly type: "object";
276
299
  readonly properties: {
@@ -285,6 +308,10 @@ export declare const TOOL_DEFINITIONS: readonly [{
285
308
  readonly default: 50;
286
309
  readonly maximum: 100;
287
310
  };
311
+ readonly project: {
312
+ readonly type: "string";
313
+ readonly description: "Optional project name to filter memories. Returns only project-specific and global memories.";
314
+ };
288
315
  };
289
316
  readonly required: readonly [];
290
317
  };
@@ -289,25 +289,52 @@ export const TOOL_DEFINITIONS = [
289
289
  },
290
290
  {
291
291
  name: 'add_memory',
292
- description: 'Extract and store developer knowledge from text. Uses LLM to identify facts about coding style, tools, architecture, conventions, debugging insights, and workflow preferences. Automatically deduplicates against existing memories.',
292
+ description: 'Store pre-extracted developer knowledge facts. Before calling, extract facts yourself from the relevant content. Each fact should be a concise, self-contained statement about coding style, tools, architecture, conventions, debugging insights, or workflow preferences. Automatically deduplicates against existing memories.',
293
293
  inputSchema: {
294
294
  type: 'object',
295
295
  properties: {
296
- content: {
297
- type: 'string',
298
- description: 'Text containing developer knowledge to extract and store (conversation snippets, notes, preferences).',
296
+ facts: {
297
+ type: 'array',
298
+ description: 'Array of facts to store. Extract these yourself before calling. Each fact must be a concise, self-contained statement.',
299
+ items: {
300
+ type: 'object',
301
+ properties: {
302
+ fact: {
303
+ type: 'string',
304
+ description: 'A concise, self-contained statement of a developer preference or convention.',
305
+ },
306
+ category: {
307
+ type: 'string',
308
+ description: 'Category: coding_style, tools, architecture, conventions, debugging, workflow, or preferences.',
309
+ enum: [
310
+ 'coding_style',
311
+ 'tools',
312
+ 'architecture',
313
+ 'conventions',
314
+ 'debugging',
315
+ 'workflow',
316
+ 'preferences',
317
+ ],
318
+ },
319
+ },
320
+ required: ['fact', 'category'],
321
+ },
299
322
  },
300
323
  source: {
301
324
  type: 'string',
302
325
  description: 'Optional source identifier (e.g., "conversation", "claude-code", "user-note").',
303
326
  },
327
+ project: {
328
+ type: 'string',
329
+ description: 'Optional project name to scope this memory (e.g., "my-app"). Defaults to "global" for cross-project memories.',
330
+ },
304
331
  },
305
- required: ['content'],
332
+ required: ['facts'],
306
333
  },
307
334
  },
308
335
  {
309
336
  name: 'search_memory',
310
- description: 'Search stored developer memories using natural language. Returns semantically similar memories ranked by relevance.',
337
+ description: 'Search stored developer memories using natural language. Returns semantically similar memories ranked by relevance. When a project is specified, project-specific memories are ranked higher.',
311
338
  inputSchema: {
312
339
  type: 'object',
313
340
  properties: {
@@ -334,13 +361,17 @@ export const TOOL_DEFINITIONS = [
334
361
  'preferences',
335
362
  ],
336
363
  },
364
+ project: {
365
+ type: 'string',
366
+ description: 'Optional project name to boost project-specific memories in results (e.g., "my-app"). Cross-project memories still appear but ranked lower.',
367
+ },
337
368
  },
338
369
  required: ['query'],
339
370
  },
340
371
  },
341
372
  {
342
373
  name: 'list_memories',
343
- description: 'List all stored developer memories, optionally filtered by category.',
374
+ description: 'List all stored developer memories, optionally filtered by category or project.',
344
375
  inputSchema: {
345
376
  type: 'object',
346
377
  properties: {
@@ -363,6 +394,10 @@ export const TOOL_DEFINITIONS = [
363
394
  default: 50,
364
395
  maximum: 100,
365
396
  },
397
+ project: {
398
+ type: 'string',
399
+ description: 'Optional project name to filter memories. Returns only project-specific and global memories.',
400
+ },
366
401
  },
367
402
  required: [],
368
403
  },
package/dist/tools.js CHANGED
@@ -257,12 +257,13 @@ export class ToolHandlers {
257
257
  async handleAddMemory(args) {
258
258
  if (!this.memoryStore)
259
259
  return textResult('Error: Memory system not initialized.');
260
- const content = args.content;
261
- if (!content)
262
- return textResult('Error: "content" is required. Provide text containing developer knowledge to extract.');
260
+ const facts = args.facts;
261
+ if (!facts || !Array.isArray(facts) || facts.length === 0)
262
+ return textResult('Error: "facts" is required. Provide an array of pre-extracted facts with fact and category fields.');
263
263
  const source = args.source;
264
+ const project = args.project;
264
265
  try {
265
- const actions = await this.memoryStore.addMemory(content, source);
266
+ const actions = await this.memoryStore.addMemory(facts, source, project);
266
267
  return textResult(formatMemoryActions(actions));
267
268
  }
268
269
  catch (err) {
@@ -278,8 +279,9 @@ export class ToolHandlers {
278
279
  return textResult('Error: "query" is required. Provide a natural language search query.');
279
280
  const limit = args.limit ?? 10;
280
281
  const category = args.category;
282
+ const project = args.project;
281
283
  try {
282
- const results = await this.memoryStore.searchMemory(query, limit, category);
284
+ const results = await this.memoryStore.searchMemory(query, limit, category, project);
283
285
  return textResult(formatMemorySearchResults(results, query));
284
286
  }
285
287
  catch (err) {
@@ -292,8 +294,9 @@ export class ToolHandlers {
292
294
  return textResult('Error: Memory system not initialized.');
293
295
  const category = args.category;
294
296
  const limit = args.limit ?? 50;
297
+ const project = args.project;
295
298
  try {
296
- const results = await this.memoryStore.listMemories(category, limit);
299
+ const results = await this.memoryStore.listMemories(category, limit, project);
297
300
  return textResult(formatMemoryList(results));
298
301
  }
299
302
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-eidetic",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Semantic code search MCP server — lean, correct, fast",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -37,7 +37,6 @@
37
37
  "release": "bash scripts/release.sh"
38
38
  },
39
39
  "dependencies": {
40
- "@anthropic-ai/sdk": "^0.78.0",
41
40
  "@modelcontextprotocol/sdk": "^1.12.1",
42
41
  "@qdrant/js-client-rest": "^1.13.0",
43
42
  "better-sqlite3": "^12.6.2",
@@ -81,7 +80,7 @@
81
80
  },
82
81
  "repository": {
83
82
  "type": "git",
84
- "url": "https://github.com/eidetics/claude-eidetic"
83
+ "url": "https://github.com/eidetics/claude-eidetic.git"
85
84
  },
86
85
  "license": "MIT"
87
86
  }
@@ -1,2 +0,0 @@
1
- export declare function chatCompletion(systemPrompt: string, userMessage: string): Promise<string>;
2
- //# sourceMappingURL=llm.d.ts.map
@@ -1,56 +0,0 @@
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
@@ -1,5 +0,0 @@
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
@@ -1,36 +0,0 @@
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