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/tools.js CHANGED
@@ -1,9 +1,19 @@
1
+ import { readFile, stat } from 'node:fs/promises';
1
2
  import { normalizePath, pathToCollectionName } from './paths.js';
2
3
  import { indexCodebase, previewCodebase, deleteSnapshot } from './core/indexer.js';
4
+ import { cleanupVectors } from './core/cleanup.js';
5
+ import { scanFiles, buildSnapshot, diffSnapshots } from './core/sync.js';
6
+ import { loadSnapshot } from './core/snapshot-io.js';
3
7
  import { getConfig } from './config.js';
4
8
  import { searchCode, formatSearchResults, formatCompactResults } from './core/searcher.js';
9
+ import { indexDocument } from './core/doc-indexer.js';
10
+ import { searchDocuments } from './core/doc-searcher.js';
11
+ import { generateRepoMap, listSymbolsTable, VectorDBSymbolSource } from './core/repo-map.js';
5
12
  import { registerProject, resolveProject, listProjects } from './state/registry.js';
6
- import { textResult, formatPreview, formatIndexResult, formatListIndexed } from './format.js';
13
+ import { textResult, formatCleanupResult, formatPreview, formatIndexResult, formatListIndexed, formatDocIndexResult, formatDocSearchResults, formatMemoryActions, formatMemorySearchResults, formatMemoryList, formatMemoryHistory, } from './format.js';
14
+ function getErrorMessage(err) {
15
+ return err instanceof Error ? err.message : String(err);
16
+ }
7
17
  function resolvePath(args) {
8
18
  const pathArg = args.path;
9
19
  if (pathArg)
@@ -17,26 +27,25 @@ function noPathError() {
17
27
  const projects = listProjects();
18
28
  const names = Object.keys(projects);
19
29
  if (names.length > 0) {
20
- const list = names.map(n => ` - ${n} → ${projects[n]}`).join('\n');
30
+ const list = names.map((n) => ` - ${n} → ${projects[n]}`).join('\n');
21
31
  return textResult(`Error: provide \`path\` or \`project\`. Registered projects:\n${list}`);
22
32
  }
23
- return textResult('Error: provide \`path\` (absolute) or \`project\` (name). No projects registered yet — index a codebase first.');
33
+ return textResult('Error: provide `path` (absolute) or `project` (name). No projects registered yet — index a codebase first.');
24
34
  }
25
35
  const locks = new Map();
26
36
  async function withMutex(key, fn) {
27
- // Chain onto any existing operation for this key (FIFO ordering, no race)
28
37
  const prev = locks.get(key) ?? Promise.resolve();
29
38
  let resolve;
30
- const current = new Promise(r => { resolve = r; });
39
+ const current = new Promise((r) => {
40
+ resolve = r;
41
+ });
31
42
  locks.set(key, current);
32
- // Wait for previous operation to complete
33
43
  await prev;
34
44
  try {
35
45
  return await fn();
36
46
  }
37
47
  finally {
38
48
  resolve();
39
- // Only delete if we're still the latest operation
40
49
  if (locks.get(key) === current) {
41
50
  locks.delete(key);
42
51
  }
@@ -46,11 +55,15 @@ export class ToolHandlers {
46
55
  embedding;
47
56
  vectordb;
48
57
  state;
58
+ memoryStore = null;
49
59
  constructor(embedding, vectordb, state) {
50
60
  this.embedding = embedding;
51
61
  this.vectordb = vectordb;
52
62
  this.state = state;
53
63
  }
64
+ setMemoryStore(store) {
65
+ this.memoryStore = store;
66
+ }
54
67
  async handleIndexCodebase(args) {
55
68
  const normalizedPath = resolvePath(args);
56
69
  if (!normalizedPath)
@@ -63,24 +76,26 @@ export class ToolHandlers {
63
76
  const collectionName = pathToCollectionName(normalizedPath);
64
77
  if (dryRun) {
65
78
  try {
66
- const preview = await previewCodebase(normalizedPath, this.embedding, customExt, customIgnore);
79
+ const preview = await previewCodebase(normalizedPath, customExt, customIgnore);
67
80
  return textResult(formatPreview(preview, normalizedPath));
68
81
  }
69
82
  catch (err) {
70
- const message = err instanceof Error ? err.message : String(err);
83
+ const message = getErrorMessage(err);
71
84
  return textResult(`Error previewing ${normalizedPath}: ${message}`);
72
85
  }
73
86
  }
74
87
  return withMutex(normalizedPath, async () => {
75
88
  this.state.setIndexing(normalizedPath, collectionName);
76
89
  try {
77
- const result = await indexCodebase(normalizedPath, this.embedding, this.vectordb, force, (pct, msg) => this.state.updateProgress(normalizedPath, pct, msg), customExt, customIgnore);
90
+ const result = await indexCodebase(normalizedPath, this.embedding, this.vectordb, force, (pct, msg) => {
91
+ this.state.updateProgress(normalizedPath, pct, msg);
92
+ }, customExt, customIgnore);
78
93
  this.state.setIndexed(normalizedPath, result.totalFiles, result.totalChunks);
79
94
  registerProject(normalizedPath);
80
95
  return textResult(formatIndexResult(result, normalizedPath));
81
96
  }
82
97
  catch (err) {
83
- const message = err instanceof Error ? err.message : String(err);
98
+ const message = getErrorMessage(err);
84
99
  this.state.setError(normalizedPath, message);
85
100
  return textResult(`Error indexing ${normalizedPath}: ${message}`);
86
101
  }
@@ -94,20 +109,21 @@ export class ToolHandlers {
94
109
  if (!normalizedPath)
95
110
  return noPathError();
96
111
  const rawLimit = args.limit;
97
- const limit = (rawLimit !== undefined && Number.isFinite(rawLimit) && rawLimit >= 1)
98
- ? rawLimit
99
- : undefined;
112
+ const limit = rawLimit !== undefined && Number.isFinite(rawLimit) && rawLimit >= 1 ? rawLimit : undefined;
100
113
  const extensionFilter = args.extensionFilter;
101
114
  const compact = args.compact !== false; // default true
102
115
  try {
103
- const results = await searchCode(normalizedPath, query, this.embedding, this.vectordb, { limit, extensionFilter });
116
+ const results = await searchCode(normalizedPath, query, this.embedding, this.vectordb, {
117
+ limit,
118
+ extensionFilter,
119
+ });
104
120
  const formatted = compact
105
121
  ? formatCompactResults(results, query, normalizedPath)
106
122
  : formatSearchResults(results, query, normalizedPath);
107
123
  return textResult(formatted);
108
124
  }
109
125
  catch (err) {
110
- const message = err instanceof Error ? err.message : String(err);
126
+ const message = getErrorMessage(err);
111
127
  return textResult(`Error: ${message}`);
112
128
  }
113
129
  }
@@ -124,7 +140,7 @@ export class ToolHandlers {
124
140
  return textResult(`Index cleared for ${normalizedPath}.`);
125
141
  }
126
142
  catch (err) {
127
- const message = err instanceof Error ? err.message : String(err);
143
+ const message = getErrorMessage(err);
128
144
  return textResult(`Error clearing index: ${message}`);
129
145
  }
130
146
  });
@@ -158,6 +174,7 @@ export class ToolHandlers {
158
174
  }
159
175
  return textResult(lines.join('\n'));
160
176
  }
177
+ // eslint-disable-next-line @typescript-eslint/require-await
161
178
  async handleListIndexed() {
162
179
  const states = this.state.getAllStates();
163
180
  if (states.length === 0) {
@@ -165,5 +182,257 @@ export class ToolHandlers {
165
182
  }
166
183
  return textResult(formatListIndexed(states));
167
184
  }
185
+ async handleCleanupVectors(args) {
186
+ const normalizedPath = resolvePath(args);
187
+ if (!normalizedPath)
188
+ return noPathError();
189
+ const dryRun = args.dryRun ?? false;
190
+ const config = getConfig();
191
+ const customExt = args.customExtensions ?? config.customExtensions;
192
+ const customIgnore = args.customIgnorePatterns ?? config.customIgnorePatterns;
193
+ return withMutex(normalizedPath, async () => {
194
+ try {
195
+ if (dryRun) {
196
+ const previousSnapshot = loadSnapshot(normalizedPath);
197
+ if (!previousSnapshot) {
198
+ return textResult(`Error: No snapshot found for ${normalizedPath}. Index the codebase first before running cleanup.`);
199
+ }
200
+ const filePaths = await scanFiles(normalizedPath, customExt, customIgnore);
201
+ const currentSnapshot = buildSnapshot(normalizedPath, filePaths);
202
+ const { removed } = diffSnapshots(previousSnapshot, currentSnapshot);
203
+ const dryResult = { removedFiles: removed, totalRemoved: removed.length, durationMs: 0 };
204
+ return textResult(formatCleanupResult(dryResult, normalizedPath, true));
205
+ }
206
+ const result = await cleanupVectors(normalizedPath, this.vectordb, undefined, customExt, customIgnore);
207
+ return textResult(formatCleanupResult(result, normalizedPath, false));
208
+ }
209
+ catch (err) {
210
+ const message = getErrorMessage(err);
211
+ return textResult(`Error during cleanup: ${message}`);
212
+ }
213
+ });
214
+ }
215
+ async handleIndexDocument(args) {
216
+ const content = args.content;
217
+ if (!content)
218
+ return textResult('Error: "content" is required. Provide the documentation text to cache.');
219
+ const source = args.source;
220
+ if (!source)
221
+ return textResult('Error: "source" is required. Provide the source URL or identifier.');
222
+ const library = args.library;
223
+ if (!library)
224
+ return textResult('Error: "library" is required. Provide the library name (e.g., "react", "langfuse").');
225
+ const topic = args.topic;
226
+ if (!topic)
227
+ return textResult('Error: "topic" is required. Provide the topic within the library (e.g., "hooks").');
228
+ const ttlDays = args.ttlDays ?? 7;
229
+ try {
230
+ const result = await indexDocument(content, source, library, topic, this.embedding, this.vectordb, ttlDays);
231
+ return textResult(formatDocIndexResult(result));
232
+ }
233
+ catch (err) {
234
+ const message = getErrorMessage(err);
235
+ return textResult(`Error caching documentation: ${message}`);
236
+ }
237
+ }
238
+ async handleSearchDocuments(args) {
239
+ const query = args.query;
240
+ if (!query)
241
+ return textResult('Error: "query" is required. Provide a natural language search query.');
242
+ const library = args.library;
243
+ const rawLimit = args.limit;
244
+ const limit = rawLimit !== undefined && Number.isFinite(rawLimit) && rawLimit >= 1 ? rawLimit : undefined;
245
+ try {
246
+ const results = await searchDocuments(query, this.embedding, this.vectordb, {
247
+ library,
248
+ limit,
249
+ });
250
+ return textResult(formatDocSearchResults(results, query));
251
+ }
252
+ catch (err) {
253
+ const message = getErrorMessage(err);
254
+ return textResult(`Error: ${message}`);
255
+ }
256
+ }
257
+ async handleAddMemory(args) {
258
+ if (!this.memoryStore)
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.');
263
+ const source = args.source;
264
+ try {
265
+ const actions = await this.memoryStore.addMemory(content, source);
266
+ return textResult(formatMemoryActions(actions));
267
+ }
268
+ catch (err) {
269
+ const message = getErrorMessage(err);
270
+ return textResult(`Error adding memory: ${message}`);
271
+ }
272
+ }
273
+ async handleSearchMemory(args) {
274
+ if (!this.memoryStore)
275
+ return textResult('Error: Memory system not initialized.');
276
+ const query = args.query;
277
+ if (!query)
278
+ return textResult('Error: "query" is required. Provide a natural language search query.');
279
+ const limit = args.limit ?? 10;
280
+ const category = args.category;
281
+ try {
282
+ const results = await this.memoryStore.searchMemory(query, limit, category);
283
+ return textResult(formatMemorySearchResults(results, query));
284
+ }
285
+ catch (err) {
286
+ const message = getErrorMessage(err);
287
+ return textResult(`Error searching memories: ${message}`);
288
+ }
289
+ }
290
+ async handleListMemories(args) {
291
+ if (!this.memoryStore)
292
+ return textResult('Error: Memory system not initialized.');
293
+ const category = args.category;
294
+ const limit = args.limit ?? 50;
295
+ try {
296
+ const results = await this.memoryStore.listMemories(category, limit);
297
+ return textResult(formatMemoryList(results));
298
+ }
299
+ catch (err) {
300
+ const message = getErrorMessage(err);
301
+ return textResult(`Error listing memories: ${message}`);
302
+ }
303
+ }
304
+ async handleDeleteMemory(args) {
305
+ if (!this.memoryStore)
306
+ return textResult('Error: Memory system not initialized.');
307
+ const id = args.id;
308
+ if (!id)
309
+ return textResult('Error: "id" is required. Provide the UUID of the memory to delete.');
310
+ try {
311
+ const deleted = await this.memoryStore.deleteMemory(id);
312
+ if (!deleted)
313
+ return textResult(`Memory not found: ${id}`);
314
+ return textResult(`Memory deleted: ${id}`);
315
+ }
316
+ catch (err) {
317
+ const message = getErrorMessage(err);
318
+ return textResult(`Error deleting memory: ${message}`);
319
+ }
320
+ }
321
+ // eslint-disable-next-line @typescript-eslint/require-await
322
+ async handleMemoryHistory(args) {
323
+ if (!this.memoryStore)
324
+ return textResult('Error: Memory system not initialized.');
325
+ const id = args.id;
326
+ if (!id)
327
+ return textResult('Error: "id" is required. Provide the UUID of the memory to view history for.');
328
+ try {
329
+ const entries = this.memoryStore.getHistory(id);
330
+ return textResult(formatMemoryHistory(entries, id));
331
+ }
332
+ catch (err) {
333
+ const message = getErrorMessage(err);
334
+ return textResult(`Error retrieving memory history: ${message}`);
335
+ }
336
+ }
337
+ async handleBrowseStructure(args) {
338
+ const normalizedPath = resolvePath(args);
339
+ if (!normalizedPath)
340
+ return noPathError();
341
+ const pathFilter = args.pathFilter;
342
+ const kindFilter = args.kind;
343
+ const maxTokens = args.maxTokens;
344
+ try {
345
+ const source = new VectorDBSymbolSource(this.vectordb);
346
+ const map = await generateRepoMap(normalizedPath, source, {
347
+ pathFilter,
348
+ kindFilter,
349
+ maxTokens,
350
+ });
351
+ return textResult(map);
352
+ }
353
+ catch (err) {
354
+ const message = getErrorMessage(err);
355
+ return textResult(`Error: ${message}`);
356
+ }
357
+ }
358
+ async handleListSymbols(args) {
359
+ const normalizedPath = resolvePath(args);
360
+ if (!normalizedPath)
361
+ return noPathError();
362
+ const pathFilter = args.pathFilter;
363
+ const kindFilter = args.kind;
364
+ const nameFilter = args.nameFilter;
365
+ try {
366
+ const source = new VectorDBSymbolSource(this.vectordb);
367
+ const table = await listSymbolsTable(normalizedPath, source, {
368
+ pathFilter,
369
+ kindFilter,
370
+ nameFilter,
371
+ });
372
+ return textResult(table);
373
+ }
374
+ catch (err) {
375
+ const message = getErrorMessage(err);
376
+ return textResult(`Error: ${message}`);
377
+ }
378
+ }
379
+ }
380
+ const MAX_LINES = 10_000;
381
+ const DEFAULT_LINES = 5_000;
382
+ const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
383
+ export async function handleReadFile(args) {
384
+ const rawPath = args.path;
385
+ if (!rawPath)
386
+ return textResult('Error: "path" is required. Provide an absolute file path.');
387
+ const filePath = normalizePath(rawPath);
388
+ const offset = Math.max(0, args.offset ?? 0);
389
+ const limit = Math.min(MAX_LINES, Math.max(1, args.limit ?? DEFAULT_LINES));
390
+ const lineNumbers = args.lineNumbers ?? false;
391
+ let raw;
392
+ try {
393
+ const fileStat = await stat(filePath);
394
+ if (fileStat.isDirectory())
395
+ return textResult(`Error: Path is a directory, not a file: ${filePath}`);
396
+ if (fileStat.size > MAX_FILE_SIZE) {
397
+ return textResult(`Error: File too large (${(fileStat.size / 1024 / 1024).toFixed(1)}MB). Maximum: 10MB.`);
398
+ }
399
+ raw = await readFile(filePath, 'utf-8');
400
+ }
401
+ catch (err) {
402
+ const nodeErr = err;
403
+ if (nodeErr.code === 'ENOENT')
404
+ return textResult(`Error: File not found: ${filePath}`);
405
+ if (nodeErr.code === 'EACCES' || nodeErr.code === 'EPERM')
406
+ return textResult(`Error: Permission denied: ${filePath}`);
407
+ if (nodeErr.code === 'EISDIR')
408
+ return textResult(`Error: Path is a directory, not a file: ${filePath}`);
409
+ const message = getErrorMessage(err);
410
+ return textResult(`Error reading file: ${message}`);
411
+ }
412
+ if (raw.includes('\x00'))
413
+ return textResult(`Error: Binary file detected: ${filePath}`);
414
+ if (raw.length === 0) {
415
+ return textResult(`File: ${filePath} | Lines: 0 total | (empty file)`);
416
+ }
417
+ const allLines = raw.split('\n');
418
+ const totalLines = allLines.length;
419
+ // offset is 1-based line number; convert to 0-based index
420
+ const startIndex = offset > 0 ? offset - 1 : 0;
421
+ const sliced = allLines.slice(startIndex, startIndex + limit);
422
+ const startLine = startIndex + 1;
423
+ const endLine = startIndex + sliced.length;
424
+ let content;
425
+ if (lineNumbers) {
426
+ const pad = String(endLine).length;
427
+ content = sliced.map((line, i) => `${String(startLine + i).padStart(pad)} ${line}`).join('\n');
428
+ }
429
+ else {
430
+ content = sliced.join('\n');
431
+ }
432
+ let meta = `File: ${filePath} | Lines: ${totalLines} total | Showing: ${startLine}–${endLine}`;
433
+ if (endLine < totalLines) {
434
+ meta += ` | Next: read_file(path="${filePath}", offset=${endLine + 1})`;
435
+ }
436
+ return textResult(meta + '\n\n' + content);
168
437
  }
169
438
  //# sourceMappingURL=tools.js.map
@@ -1,4 +1,4 @@
1
- import type { VectorDB, CodeDocument, HybridSearchParams, SearchResult } from './types.js';
1
+ import type { VectorDB, CodeDocument, HybridSearchParams, SearchResult, SymbolEntry } from './types.js';
2
2
  /**
3
3
  * Detects the "data type 104 not supported" error from older Milvus versions
4
4
  * that lack SparseFloatVector support (< v2.4).
@@ -21,12 +21,14 @@ export declare class MilvusVectorDB implements VectorDB {
21
21
  private hybridSearch;
22
22
  private denseOnlySearch;
23
23
  private mapResults;
24
- /**
25
- * Detect whether a collection has the sparse_vector field (hybrid) or not.
26
- * Caches result in hybridCollections set.
27
- */
28
24
  private detectHybrid;
25
+ getById(name: string, id: string): Promise<{
26
+ payload: Record<string, unknown>;
27
+ vector: number[];
28
+ } | null>;
29
+ updatePoint(name: string, id: string, vector: number[], payload: Record<string, unknown>): Promise<void>;
29
30
  deleteByPath(name: string, relativePath: string): Promise<void>;
31
+ listSymbols(name: string): Promise<SymbolEntry[]>;
30
32
  private ensureLoaded;
31
33
  private waitForLoad;
32
34
  }
@@ -1,6 +1,6 @@
1
1
  import { VectorDBError } from '../errors.js';
2
2
  import { getConfig } from '../config.js';
3
- // Dynamic import -- @zilliz/milvus2-sdk-node is an optional dependency
3
+ import { RRF_K } from './qdrant.js';
4
4
  let MilvusClient;
5
5
  let DataType;
6
6
  let MetricType;
@@ -19,14 +19,13 @@ async function loadMilvusSDK() {
19
19
  throw new VectorDBError('Milvus SDK not installed. Run: npm install @zilliz/milvus2-sdk-node');
20
20
  }
21
21
  }
22
- const RRF_K = 60;
23
22
  /**
24
23
  * Detects the "data type 104 not supported" error from older Milvus versions
25
24
  * that lack SparseFloatVector support (< v2.4).
26
25
  */
27
26
  export function isSparseUnsupportedError(err) {
28
27
  const msg = String(err && typeof err === 'object' && 'reason' in err ? err.reason : err);
29
- return /data type[:\s]*104/i.test(msg) || /not supported/i.test(msg) && /104/.test(msg);
28
+ return /data type[:\s]*104/i.test(msg) || (/not supported/i.test(msg) && msg.includes('104'));
30
29
  }
31
30
  export class MilvusVectorDB {
32
31
  client = null;
@@ -56,7 +55,6 @@ export class MilvusVectorDB {
56
55
  }
57
56
  async createCollection(name, dimension) {
58
57
  await this.ready();
59
- // Try hybrid first, fall back to dense-only if Milvus version doesn't support sparse
60
58
  try {
61
59
  await this.createHybridCollection(name, dimension);
62
60
  this.hybridCollections.add(name);
@@ -67,12 +65,11 @@ export class MilvusVectorDB {
67
65
  if (isSparseUnsupportedError(err)) {
68
66
  console.warn(`Milvus does not support SparseFloatVector (requires >= v2.4). ` +
69
67
  `Falling back to dense-only collection for "${name}".`);
70
- // Clean up the failed collection attempt
71
68
  try {
72
69
  await this.client.dropCollection({ collection_name: name });
73
70
  }
74
71
  catch (cleanupErr) {
75
- console.warn(`Failed to clean up collection "${name}": ${cleanupErr}`);
72
+ console.warn(`Failed to clean up collection "${name}":`, cleanupErr);
76
73
  }
77
74
  }
78
75
  else {
@@ -98,14 +95,21 @@ export class MilvusVectorDB {
98
95
  { name: 'endLine', data_type: DataType.Int64 },
99
96
  { name: 'fileExtension', data_type: DataType.VarChar, max_length: 32 },
100
97
  { name: 'language', data_type: DataType.VarChar, max_length: 64 },
98
+ { name: 'fileCategory', data_type: DataType.VarChar, max_length: 32 },
99
+ { name: 'symbolName', data_type: DataType.VarChar, max_length: 256 },
100
+ { name: 'symbolKind', data_type: DataType.VarChar, max_length: 64 },
101
+ { name: 'symbolSignature', data_type: DataType.VarChar, max_length: 512 },
102
+ { name: 'parentSymbol', data_type: DataType.VarChar, max_length: 256 },
101
103
  ];
102
- const functions = [{
104
+ const functions = [
105
+ {
103
106
  name: 'content_bm25',
104
107
  type: FunctionType.BM25,
105
108
  input_field_names: ['content'],
106
109
  output_field_names: ['sparse_vector'],
107
110
  params: {},
108
- }];
111
+ },
112
+ ];
109
113
  await this.client.createCollection({
110
114
  collection_name: name,
111
115
  fields: schema,
@@ -135,6 +139,11 @@ export class MilvusVectorDB {
135
139
  { name: 'endLine', data_type: DataType.Int64 },
136
140
  { name: 'fileExtension', data_type: DataType.VarChar, max_length: 32 },
137
141
  { name: 'language', data_type: DataType.VarChar, max_length: 64 },
142
+ { name: 'fileCategory', data_type: DataType.VarChar, max_length: 32 },
143
+ { name: 'symbolName', data_type: DataType.VarChar, max_length: 256 },
144
+ { name: 'symbolKind', data_type: DataType.VarChar, max_length: 64 },
145
+ { name: 'symbolSignature', data_type: DataType.VarChar, max_length: 512 },
146
+ { name: 'parentSymbol', data_type: DataType.VarChar, max_length: 256 },
138
147
  ];
139
148
  await this.client.createCollection({
140
149
  collection_name: name,
@@ -176,7 +185,7 @@ export class MilvusVectorDB {
176
185
  await this.ready();
177
186
  await this.ensureLoaded(name);
178
187
  try {
179
- const data = documents.map(doc => ({
188
+ const data = documents.map((doc) => ({
180
189
  id: doc.id,
181
190
  content: doc.content,
182
191
  vector: doc.vector,
@@ -185,6 +194,11 @@ export class MilvusVectorDB {
185
194
  endLine: doc.endLine,
186
195
  fileExtension: doc.fileExtension,
187
196
  language: doc.language,
197
+ fileCategory: doc.fileCategory ?? 'source',
198
+ symbolName: doc.symbolName ?? '',
199
+ symbolKind: doc.symbolKind ?? '',
200
+ symbolSignature: doc.symbolSignature ?? '',
201
+ parentSymbol: doc.parentSymbol ?? '',
188
202
  }));
189
203
  await this.client.insert({ collection_name: name, data });
190
204
  }
@@ -199,7 +213,9 @@ export class MilvusVectorDB {
199
213
  try {
200
214
  let expr;
201
215
  if (params.extensionFilter?.length) {
202
- const exts = params.extensionFilter.map(e => `"${e.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`).join(', ');
216
+ const exts = params.extensionFilter
217
+ .map((e) => `"${e.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`)
218
+ .join(', ');
203
219
  expr = `fileExtension in [${exts}]`;
204
220
  }
205
221
  if (isHybrid) {
@@ -231,7 +247,16 @@ export class MilvusVectorDB {
231
247
  ],
232
248
  limit: params.limit,
233
249
  rerank: { strategy: 'rrf', params: { k: RRF_K } },
234
- output_fields: ['id', 'content', 'relativePath', 'startLine', 'endLine', 'fileExtension', 'language'],
250
+ output_fields: [
251
+ 'id',
252
+ 'content',
253
+ 'relativePath',
254
+ 'startLine',
255
+ 'endLine',
256
+ 'fileExtension',
257
+ 'language',
258
+ 'fileCategory',
259
+ ],
235
260
  };
236
261
  if (expr)
237
262
  searchParams.expr = expr;
@@ -243,7 +268,16 @@ export class MilvusVectorDB {
243
268
  collection_name: name,
244
269
  data: [params.queryVector],
245
270
  limit: params.limit,
246
- output_fields: ['id', 'content', 'relativePath', 'startLine', 'endLine', 'fileExtension', 'language'],
271
+ output_fields: [
272
+ 'id',
273
+ 'content',
274
+ 'relativePath',
275
+ 'startLine',
276
+ 'endLine',
277
+ 'fileExtension',
278
+ 'language',
279
+ 'fileCategory',
280
+ ],
247
281
  };
248
282
  if (expr)
249
283
  searchParams.expr = expr;
@@ -261,12 +295,9 @@ export class MilvusVectorDB {
261
295
  fileExtension: r.fileExtension ?? '',
262
296
  language: r.language ?? '',
263
297
  score: r.score ?? 0,
298
+ fileCategory: r.fileCategory ?? '',
264
299
  }));
265
300
  }
266
- /**
267
- * Detect whether a collection has the sparse_vector field (hybrid) or not.
268
- * Caches result in hybridCollections set.
269
- */
270
301
  async detectHybrid(name) {
271
302
  if (this.hybridCollections.has(name))
272
303
  return true;
@@ -283,6 +314,39 @@ export class MilvusVectorDB {
283
314
  return false;
284
315
  }
285
316
  }
317
+ async getById(name, id) {
318
+ await this.ready();
319
+ try {
320
+ const result = await this.client.get({
321
+ collection_name: name,
322
+ ids: [id],
323
+ output_fields: ['*'],
324
+ });
325
+ if (!result.data?.length)
326
+ return null;
327
+ const point = result.data[0];
328
+ return {
329
+ payload: point,
330
+ vector: point.vector ?? [],
331
+ };
332
+ }
333
+ catch (err) {
334
+ throw new VectorDBError(`Failed to retrieve point "${id}" from "${name}"`, err);
335
+ }
336
+ }
337
+ async updatePoint(name, id, vector, payload) {
338
+ await this.ready();
339
+ await this.ensureLoaded(name);
340
+ try {
341
+ await this.client.upsert({
342
+ collection_name: name,
343
+ data: [{ id, vector, ...payload }],
344
+ });
345
+ }
346
+ catch (err) {
347
+ throw new VectorDBError(`Failed to update point "${id}" in Milvus collection "${name}"`, err);
348
+ }
349
+ }
286
350
  async deleteByPath(name, relativePath) {
287
351
  await this.ready();
288
352
  await this.ensureLoaded(name);
@@ -297,6 +361,39 @@ export class MilvusVectorDB {
297
361
  throw new VectorDBError(`Failed to delete by path "${relativePath}" from "${name}"`, err);
298
362
  }
299
363
  }
364
+ async listSymbols(name) {
365
+ await this.ready();
366
+ await this.ensureLoaded(name);
367
+ try {
368
+ const result = await this.client.query({
369
+ collection_name: name,
370
+ filter: 'symbolName != ""',
371
+ output_fields: [
372
+ 'symbolName',
373
+ 'symbolKind',
374
+ 'relativePath',
375
+ 'startLine',
376
+ 'symbolSignature',
377
+ 'parentSymbol',
378
+ ],
379
+ limit: 16384,
380
+ });
381
+ const rows = result.data ?? [];
382
+ return rows
383
+ .filter((r) => r.symbolName)
384
+ .map((r) => ({
385
+ name: String(r.symbolName),
386
+ kind: String(r.symbolKind ?? ''),
387
+ relativePath: String(r.relativePath ?? ''),
388
+ startLine: Number(r.startLine ?? 0),
389
+ ...(r.symbolSignature ? { signature: String(r.symbolSignature) } : {}),
390
+ ...(r.parentSymbol ? { parentName: String(r.parentSymbol) } : {}),
391
+ }));
392
+ }
393
+ catch (err) {
394
+ throw new VectorDBError(`Failed to list symbols from "${name}"`, err);
395
+ }
396
+ }
300
397
  async ensureLoaded(name) {
301
398
  try {
302
399
  const result = await this.client.getLoadState({ collection_name: name });
@@ -305,7 +402,7 @@ export class MilvusVectorDB {
305
402
  }
306
403
  }
307
404
  catch (err) {
308
- console.warn(`Failed to ensure collection "${name}" is loaded: ${err}`);
405
+ console.warn(`Failed to ensure collection "${name}" is loaded:`, err);
309
406
  }
310
407
  }
311
408
  async waitForLoad(name, timeoutMs = 30_000) {
@@ -318,9 +415,9 @@ export class MilvusVectorDB {
318
415
  return;
319
416
  }
320
417
  catch (err) {
321
- console.warn(`Load state check failed for "${name}": ${err}`);
418
+ console.warn(`Load state check failed for "${name}":`, err);
322
419
  }
323
- await new Promise(r => setTimeout(r, 500));
420
+ await new Promise((r) => setTimeout(r, 500));
324
421
  }
325
422
  throw new VectorDBError(`Collection "${name}" failed to load within ${timeoutMs}ms`);
326
423
  }