@synth-coder/memhub 0.1.6 → 0.2.1

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 (43) hide show
  1. package/AGENTS.md +26 -0
  2. package/README.md +2 -2
  3. package/README.zh-CN.md +1 -1
  4. package/dist/src/contracts/mcp.d.ts +2 -5
  5. package/dist/src/contracts/mcp.d.ts.map +1 -1
  6. package/dist/src/contracts/mcp.js +77 -23
  7. package/dist/src/contracts/mcp.js.map +1 -1
  8. package/dist/src/contracts/schemas.d.ts +67 -76
  9. package/dist/src/contracts/schemas.d.ts.map +1 -1
  10. package/dist/src/contracts/schemas.js +4 -8
  11. package/dist/src/contracts/schemas.js.map +1 -1
  12. package/dist/src/contracts/types.d.ts +1 -4
  13. package/dist/src/contracts/types.d.ts.map +1 -1
  14. package/dist/src/contracts/types.js.map +1 -1
  15. package/dist/src/server/mcp-server.d.ts.map +1 -1
  16. package/dist/src/server/mcp-server.js +21 -4
  17. package/dist/src/server/mcp-server.js.map +1 -1
  18. package/dist/src/services/embedding-service.d.ts +43 -0
  19. package/dist/src/services/embedding-service.d.ts.map +1 -0
  20. package/dist/src/services/embedding-service.js +80 -0
  21. package/dist/src/services/embedding-service.js.map +1 -0
  22. package/dist/src/services/memory-service.d.ts +30 -44
  23. package/dist/src/services/memory-service.d.ts.map +1 -1
  24. package/dist/src/services/memory-service.js +212 -161
  25. package/dist/src/services/memory-service.js.map +1 -1
  26. package/dist/src/storage/vector-index.d.ts +62 -0
  27. package/dist/src/storage/vector-index.d.ts.map +1 -0
  28. package/dist/src/storage/vector-index.js +123 -0
  29. package/dist/src/storage/vector-index.js.map +1 -0
  30. package/package.json +17 -13
  31. package/src/contracts/mcp.ts +84 -30
  32. package/src/contracts/schemas.ts +4 -8
  33. package/src/contracts/types.ts +4 -8
  34. package/src/server/mcp-server.ts +23 -7
  35. package/src/services/embedding-service.ts +114 -0
  36. package/src/services/memory-service.ts +252 -179
  37. package/src/storage/vector-index.ts +160 -0
  38. package/test/server/mcp-server.test.ts +11 -9
  39. package/test/services/memory-service-edge.test.ts +1 -1
  40. package/test/services/memory-service.test.ts +1 -1
  41. package/test/storage/vector-index.test.ts +153 -0
  42. package/vitest.config.ts +3 -1
  43. /package/docs/{proposal-close-gates.md → proposals/proposal-close-gates.md} +0 -0
@@ -1,7 +1,3 @@
1
- /**
2
- * Memory Service - Business logic for memory operations
3
- */
4
-
5
1
  import { randomUUID } from 'crypto';
6
2
  import type {
7
3
  Memory,
@@ -28,6 +24,19 @@ import type {
28
24
  import { ErrorCode } from '../contracts/types.js';
29
25
  import { MarkdownStorage, StorageError } from '../storage/markdown-storage.js';
30
26
 
27
+ /** Minimal interface required from VectorIndex (avoids static import of native module) */
28
+ interface IVectorIndex {
29
+ upsert(memory: Memory, vector: number[]): Promise<void>;
30
+ delete(id: string): Promise<void>;
31
+ search(vector: number[], limit?: number): Promise<Array<{ id: string; _distance: number }>>;
32
+ }
33
+
34
+ /** Minimal interface required from EmbeddingService */
35
+ interface IEmbeddingService {
36
+ embedMemory(title: string, content: string): Promise<number[]>;
37
+ embed(text: string): Promise<number[]>;
38
+ }
39
+
31
40
  /**
32
41
  * Custom error for service operations
33
42
  */
@@ -47,6 +56,12 @@ export class ServiceError extends Error {
47
56
  */
48
57
  export interface MemoryServiceConfig {
49
58
  storagePath: string;
59
+ /**
60
+ * Enable vector semantic search via LanceDB + local ONNX model.
61
+ * Set to false in unit tests to avoid loading the model.
62
+ * @default true
63
+ */
64
+ vectorSearch?: boolean;
50
65
  }
51
66
 
52
67
  /**
@@ -54,17 +69,107 @@ export interface MemoryServiceConfig {
54
69
  */
55
70
  export class MemoryService {
56
71
  private readonly storage: MarkdownStorage;
72
+ private readonly vectorIndex: IVectorIndex | null;
73
+ private readonly embedding: IEmbeddingService | null;
74
+ private readonly vectorSearchEnabled: boolean;
57
75
 
58
76
  constructor(config: MemoryServiceConfig) {
59
77
  this.storage = new MarkdownStorage({ storagePath: config.storagePath });
78
+ this.vectorSearchEnabled = config.vectorSearch !== false;
79
+
80
+ if (this.vectorSearchEnabled) {
81
+ // Lazily resolved at runtime — do not use top-level static imports so that
82
+ // native modules (onnxruntime-node, sharp) are never loaded when vectorSearch=false.
83
+ let resolvedVectorIndex: IVectorIndex | null = null;
84
+ let resolvedEmbedding: IEmbeddingService | null = null;
85
+
86
+ // Kick off async initialisation without blocking the constructor.
87
+ // The proxy objects below delegate to the real instances once ready.
88
+ const storagePath = config.storagePath;
89
+ const initPromise = (async () => {
90
+ const [{ VectorIndex }, { EmbeddingService }] = await Promise.all([
91
+ import('../storage/vector-index.js'),
92
+ import('./embedding-service.js'),
93
+ ]);
94
+ resolvedVectorIndex = new VectorIndex(storagePath);
95
+ resolvedEmbedding = EmbeddingService.getInstance();
96
+ })();
97
+
98
+ // Lightweight proxy that waits for init before delegating
99
+ this.vectorIndex = {
100
+ upsert: async (memory, vector) => {
101
+ await initPromise;
102
+ return resolvedVectorIndex!.upsert(memory, vector);
103
+ },
104
+ delete: async (id) => {
105
+ await initPromise;
106
+ return resolvedVectorIndex!.delete(id);
107
+ },
108
+ search: async (vector, limit) => {
109
+ await initPromise;
110
+ return resolvedVectorIndex!.search(vector, limit);
111
+ },
112
+ };
113
+
114
+ this.embedding = {
115
+ embedMemory: async (title, content) => {
116
+ await initPromise;
117
+ return resolvedEmbedding!.embedMemory(title, content);
118
+ },
119
+ embed: async (text) => {
120
+ await initPromise;
121
+ return resolvedEmbedding!.embed(text);
122
+ },
123
+ };
124
+ } else {
125
+ this.vectorIndex = null;
126
+ this.embedding = null;
127
+ }
128
+ }
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Internal helpers
132
+ // ---------------------------------------------------------------------------
133
+
134
+ /**
135
+ * Asynchronously embeds a memory and upserts it into the vector index.
136
+ * Fire-and-forget: failures are logged but do not propagate.
137
+ */
138
+ private scheduleVectorUpsert(memory: Memory): void {
139
+ if (!this.vectorIndex || !this.embedding) return;
140
+
141
+ const vectorIndex = this.vectorIndex;
142
+ const embedding = this.embedding;
143
+
144
+ // Intentionally not awaited
145
+ embedding
146
+ .embedMemory(memory.title, memory.content)
147
+ .then(vec => vectorIndex.upsert(memory, vec))
148
+ .catch(err => {
149
+ // Non-fatal: Markdown file is the source of truth
150
+ console.error('[MemHub] Vector upsert failed (non-fatal):', err);
151
+ });
152
+ }
153
+
154
+ /**
155
+ * Removes a memory from the vector index.
156
+ * Called synchronously (awaited) on delete.
157
+ */
158
+ private async removeFromVectorIndex(id: string): Promise<void> {
159
+ if (!this.vectorIndex) return;
160
+ try {
161
+ await this.vectorIndex.delete(id);
162
+ } catch (err) {
163
+ console.error('[MemHub] Vector delete failed (non-fatal):', err);
164
+ }
60
165
  }
61
166
 
167
+ // ---------------------------------------------------------------------------
168
+ // CRUD operations
169
+ // ---------------------------------------------------------------------------
170
+
62
171
  /**
63
172
  * Creates a new memory entry
64
- *
65
- * @param input - Create memory input
66
- * @returns Create result with ID, file path, and memory object
67
- * @throws ServiceError if creation fails
68
173
  */
69
174
  async create(input: CreateMemoryInput): Promise<CreateResult> {
70
175
  const now = new Date().toISOString();
@@ -83,11 +188,8 @@ export class MemoryService {
83
188
 
84
189
  try {
85
190
  const filePath = await this.storage.write(memory);
86
- return {
87
- id,
88
- filePath,
89
- memory,
90
- };
191
+ this.scheduleVectorUpsert(memory);
192
+ return { id, filePath, memory };
91
193
  } catch (error) {
92
194
  throw new ServiceError(
93
195
  `Failed to create memory: ${error instanceof Error ? error.message : 'Unknown error'}`,
@@ -98,10 +200,6 @@ export class MemoryService {
98
200
 
99
201
  /**
100
202
  * Reads a memory by ID
101
- *
102
- * @param input - Read memory input
103
- * @returns Memory object
104
- * @throws ServiceError if memory not found
105
203
  */
106
204
  async read(input: ReadMemoryInput): Promise<{ memory: Memory }> {
107
205
  try {
@@ -120,13 +218,8 @@ export class MemoryService {
120
218
 
121
219
  /**
122
220
  * Updates an existing memory
123
- *
124
- * @param input - Update memory input
125
- * @returns Updated memory object
126
- * @throws ServiceError if memory not found
127
221
  */
128
222
  async update(input: UpdateMemoryInput): Promise<UpdateResult> {
129
- // First read the existing memory
130
223
  let existing: Memory;
131
224
  try {
132
225
  existing = await this.storage.read(input.id);
@@ -140,7 +233,6 @@ export class MemoryService {
140
233
  );
141
234
  }
142
235
 
143
- // Apply updates
144
236
  const updated: Memory = {
145
237
  ...existing,
146
238
  updatedAt: new Date().toISOString(),
@@ -153,6 +245,7 @@ export class MemoryService {
153
245
 
154
246
  try {
155
247
  await this.storage.write(updated);
248
+ this.scheduleVectorUpsert(updated);
156
249
  return { memory: updated };
157
250
  } catch (error) {
158
251
  throw new ServiceError(
@@ -164,18 +257,12 @@ export class MemoryService {
164
257
 
165
258
  /**
166
259
  * Deletes a memory by ID
167
- *
168
- * @param input - Delete memory input
169
- * @returns Delete result
170
- * @throws ServiceError if memory not found
171
260
  */
172
261
  async delete(input: DeleteMemoryInput): Promise<DeleteResult> {
173
262
  try {
174
263
  const filePath = await this.storage.delete(input.id);
175
- return {
176
- success: true,
177
- filePath,
178
- };
264
+ await this.removeFromVectorIndex(input.id);
265
+ return { success: true, filePath };
179
266
  } catch (error) {
180
267
  if (error instanceof StorageError && error.message.includes('not found')) {
181
268
  throw new ServiceError(`Memory not found: ${input.id}`, ErrorCode.NOT_FOUND);
@@ -187,17 +274,17 @@ export class MemoryService {
187
274
  }
188
275
  }
189
276
 
277
+ // ---------------------------------------------------------------------------
278
+ // List / Search
279
+ // ---------------------------------------------------------------------------
280
+
190
281
  /**
191
282
  * Lists memories with filtering and pagination
192
- *
193
- * @param input - List memory input
194
- * @returns List result with memories, total count, and hasMore flag
195
283
  */
196
284
  async list(input: ListMemoryInput): Promise<ListResult> {
197
285
  try {
198
286
  const files = await this.storage.list();
199
287
 
200
- // Parse all files into memories
201
288
  let memories: Memory[] = [];
202
289
  for (const file of files) {
203
290
  try {
@@ -206,31 +293,25 @@ export class MemoryService {
206
293
  );
207
294
  memories.push(memory);
208
295
  } catch {
209
- // Skip invalid files
210
296
  continue;
211
297
  }
212
298
  }
213
299
 
214
- // Apply filters
215
300
  if (input.category) {
216
301
  memories = memories.filter(m => m.category === input.category);
217
302
  }
218
-
219
303
  if (input.tags && input.tags.length > 0) {
220
304
  memories = memories.filter(m =>
221
305
  input.tags!.every(tag => m.tags.includes(tag))
222
306
  );
223
307
  }
224
-
225
308
  if (input.fromDate) {
226
309
  memories = memories.filter(m => m.createdAt >= input.fromDate!);
227
310
  }
228
-
229
311
  if (input.toDate) {
230
312
  memories = memories.filter(m => m.createdAt <= input.toDate!);
231
313
  }
232
314
 
233
- // Sort
234
315
  const sortBy: SortField = input.sortBy ?? 'createdAt';
235
316
  const sortOrder: SortOrder = input.sortOrder ?? 'desc';
236
317
 
@@ -253,18 +334,15 @@ export class MemoryService {
253
334
  return sortOrder === 'asc' ? comparison : -comparison;
254
335
  });
255
336
 
256
- // Apply pagination
257
337
  const total = memories.length;
258
338
  const limit = input.limit ?? 20;
259
339
  const offset = input.offset ?? 0;
260
-
261
340
  const paginatedMemories = memories.slice(offset, offset + limit);
262
- const hasMore = offset + limit < total;
263
341
 
264
342
  return {
265
343
  memories: paginatedMemories,
266
344
  total,
267
- hasMore,
345
+ hasMore: offset + limit < total,
268
346
  };
269
347
  } catch (error) {
270
348
  throw new ServiceError(
@@ -275,116 +353,148 @@ export class MemoryService {
275
353
  }
276
354
 
277
355
  /**
278
- * Searches memories by query
279
- *
280
- * @param input - Search memory input
281
- * @returns Search results with scores and matches
356
+ * Searches memories by query.
357
+ * Uses vector semantic search when available, falls back to keyword search.
282
358
  */
283
359
  async search(input: SearchMemoryInput): Promise<{ results: SearchResult[]; total: number }> {
284
- try {
285
- const listResult = await this.list({
286
- category: input.category,
287
- tags: input.tags,
288
- limit: 1000, // Get all for search
289
- });
290
-
291
- const query = input.query.toLowerCase();
292
- const keywords = query.split(/\s+/).filter(k => k.length > 0);
293
-
294
- const results: SearchResult[] = [];
360
+ // --- Vector semantic search path ---
361
+ if (this.vectorSearchEnabled && this.vectorIndex && this.embedding) {
362
+ try {
363
+ const queryVec = await this.embedding.embed(input.query);
364
+ const vectorResults = await this.vectorIndex.search(
365
+ queryVec,
366
+ input.limit ?? 10
367
+ );
295
368
 
296
- for (const memory of listResult.memories) {
297
- let score = 0;
298
- const matches: string[] = [];
299
-
300
- // Search in title (higher weight)
301
- const titleLower = memory.title.toLowerCase();
302
- if (titleLower.includes(query)) {
303
- score += 10;
304
- matches.push(memory.title);
305
- } else {
306
- // Check individual keywords in title
307
- for (const keyword of keywords) {
308
- if (titleLower.includes(keyword)) {
309
- score += 5;
310
- if (!matches.includes(memory.title)) {
311
- matches.push(memory.title);
312
- }
369
+ const results: SearchResult[] = [];
370
+ for (const vr of vectorResults) {
371
+ try {
372
+ const { memory } = await this.read({ id: vr.id });
373
+
374
+ // Apply metadata filters
375
+ if (input.category && memory.category !== input.category) continue;
376
+ if (
377
+ input.tags &&
378
+ input.tags.length > 0 &&
379
+ !input.tags.every(t => memory.tags.includes(t))
380
+ ) {
381
+ continue;
313
382
  }
383
+
384
+ // Convert cosine distance (0‥2) → similarity score (0‥1)
385
+ const score = Math.max(0, 1 - vr._distance / 2);
386
+ results.push({ memory, score, matches: [memory.title] });
387
+ } catch {
388
+ // Memory in index but missing on disk — skip
314
389
  }
315
390
  }
316
391
 
317
- // Search in content
318
- const contentLower = memory.content.toLowerCase();
319
- if (contentLower.includes(query)) {
320
- score += 3;
321
- // Extract matching snippet
322
- const index = contentLower.indexOf(query);
323
- const start = Math.max(0, index - 50);
324
- const end = Math.min(contentLower.length, index + query.length + 50);
325
- const snippet = memory.content.slice(start, end);
326
- matches.push(snippet);
327
- } else {
328
- // Check individual keywords in content
329
- for (const keyword of keywords) {
330
- if (contentLower.includes(keyword)) {
331
- score += 1;
332
- const index = contentLower.indexOf(keyword);
333
- const start = Math.max(0, index - 30);
334
- const end = Math.min(contentLower.length, index + keyword.length + 30);
335
- const snippet = memory.content.slice(start, end);
336
- if (!matches.some(m => m.includes(snippet))) {
337
- matches.push(snippet);
338
- }
339
- }
392
+ return { results, total: results.length };
393
+ } catch (err) {
394
+ // Fall through to keyword search on vector failure
395
+ console.error('[MemHub] Vector search failed, falling back to keyword search:', err);
396
+ }
397
+ }
398
+
399
+ // --- Keyword search fallback ---
400
+ return this.keywordSearch(input);
401
+ }
402
+
403
+ /**
404
+ * Legacy keyword-based search (used as fallback when vector search is unavailable).
405
+ */
406
+ private async keywordSearch(
407
+ input: SearchMemoryInput
408
+ ): Promise<{ results: SearchResult[]; total: number }> {
409
+ const listResult = await this.list({
410
+ category: input.category,
411
+ tags: input.tags,
412
+ limit: 1000,
413
+ });
414
+
415
+ const query = input.query.toLowerCase();
416
+ const keywords = query.split(/\s+/).filter(k => k.length > 0);
417
+ const results: SearchResult[] = [];
418
+
419
+ for (const memory of listResult.memories) {
420
+ let score = 0;
421
+ const matches: string[] = [];
422
+
423
+ const titleLower = memory.title.toLowerCase();
424
+ if (titleLower.includes(query)) {
425
+ score += 10;
426
+ matches.push(memory.title);
427
+ } else {
428
+ for (const keyword of keywords) {
429
+ if (titleLower.includes(keyword)) {
430
+ score += 5;
431
+ if (!matches.includes(memory.title)) matches.push(memory.title);
340
432
  }
341
433
  }
434
+ }
342
435
 
343
- // Search in tags
344
- for (const tag of memory.tags) {
345
- if (tag.toLowerCase().includes(query) || keywords.some(k => tag.toLowerCase().includes(k))) {
346
- score += 2;
347
- matches.push(`Tag: ${tag}`);
436
+ const contentLower = memory.content.toLowerCase();
437
+ if (contentLower.includes(query)) {
438
+ score += 3;
439
+ const index = contentLower.indexOf(query);
440
+ const start = Math.max(0, index - 50);
441
+ const end = Math.min(contentLower.length, index + query.length + 50);
442
+ matches.push(memory.content.slice(start, end));
443
+ } else {
444
+ for (const keyword of keywords) {
445
+ if (contentLower.includes(keyword)) {
446
+ score += 1;
447
+ const index = contentLower.indexOf(keyword);
448
+ const start = Math.max(0, index - 30);
449
+ const end = Math.min(contentLower.length, index + keyword.length + 30);
450
+ const snippet = memory.content.slice(start, end);
451
+ if (!matches.some(m => m.includes(snippet))) matches.push(snippet);
348
452
  }
349
453
  }
454
+ }
350
455
 
351
- if (score > 0) {
352
- results.push({
353
- memory,
354
- score: Math.min(score / 20, 1), // Normalize to 0-1
355
- matches: matches.slice(0, 3), // Limit matches
356
- });
456
+ for (const tag of memory.tags) {
457
+ if (
458
+ tag.toLowerCase().includes(query) ||
459
+ keywords.some(k => tag.toLowerCase().includes(k))
460
+ ) {
461
+ score += 2;
462
+ matches.push(`Tag: ${tag}`);
357
463
  }
358
464
  }
359
465
 
360
- // Sort by score descending
361
- results.sort((a, b) => b.score - a.score);
362
-
363
- // Apply limit
364
- const limit = input.limit ?? 10;
365
- const limitedResults = results.slice(0, limit);
366
-
367
- return {
368
- results: limitedResults,
369
- total: results.length,
370
- };
371
- } catch (error) {
372
- throw new ServiceError(
373
- `Failed to search memories: ${error instanceof Error ? error.message : 'Unknown error'}`,
374
- ErrorCode.STORAGE_ERROR
375
- );
466
+ if (score > 0) {
467
+ results.push({
468
+ memory,
469
+ score: Math.min(score / 20, 1),
470
+ matches: matches.slice(0, 3),
471
+ });
472
+ }
376
473
  }
474
+
475
+ results.sort((a, b) => b.score - a.score);
476
+ const limit = input.limit ?? 10;
477
+ return { results: results.slice(0, limit), total: results.length };
377
478
  }
378
479
 
480
+ // ---------------------------------------------------------------------------
481
+ // MCP unified tools
482
+ // ---------------------------------------------------------------------------
483
+
379
484
  /**
380
- * memory_load unified read API (STM-first)
485
+ * memory_load unified read API.
486
+ *
487
+ * Requires either `id` (exact lookup) or `query` (semantic search).
488
+ * Calling without either returns an empty result.
381
489
  */
382
490
  async memoryLoad(input: MemoryLoadInput): Promise<MemoryLoadOutput> {
491
+ // By-ID lookup
383
492
  if (input.id) {
384
493
  const { memory } = await this.read({ id: input.id });
385
494
  return { items: [memory], total: 1 };
386
495
  }
387
496
 
497
+ // Semantic / keyword search
388
498
  if (input.query) {
389
499
  const searched = await this.search({
390
500
  query: input.query,
@@ -392,37 +502,16 @@ export class MemoryService {
392
502
  tags: input.tags,
393
503
  limit: input.limit,
394
504
  });
395
- let items = searched.results.map(r => r.memory);
396
- if (input.sessionId) {
397
- items = items.filter(m => m.sessionId === input.sessionId);
398
- }
505
+ const items = searched.results.map(r => r.memory);
399
506
  return { items, total: items.length };
400
507
  }
401
508
 
402
- const listResult = await this.list({
403
- category: input.category,
404
- tags: input.tags,
405
- limit: input.limit ?? 20,
406
- sortBy: 'updatedAt',
407
- sortOrder: 'desc',
408
- });
409
-
410
- let items = [...listResult.memories];
411
-
412
- if (input.sessionId) {
413
- items = items.filter(m => m.sessionId === input.sessionId);
414
- }
415
-
416
- if (input.date) {
417
- const date = input.date;
418
- items = items.filter(m => m.createdAt.startsWith(date));
419
- }
420
-
421
- return { items, total: items.length };
509
+ // No id and no query — return empty (not supported)
510
+ return { items: [], total: 0 };
422
511
  }
423
512
 
424
513
  /**
425
- * memory_update unified write API (append/upsert)
514
+ * memory_update unified write API (append/upsert)
426
515
  */
427
516
  async memoryUpdate(input: MemoryUpdateInput): Promise<MemoryUpdateOutput> {
428
517
  const now = new Date().toISOString();
@@ -445,6 +534,8 @@ export class MemoryService {
445
534
  };
446
535
 
447
536
  const filePath = await this.storage.write(updatedMemory);
537
+ this.scheduleVectorUpsert(updatedMemory);
538
+
448
539
  return {
449
540
  id: updatedMemory.id,
450
541
  sessionId,
@@ -470,6 +561,8 @@ export class MemoryService {
470
561
  };
471
562
 
472
563
  const filePath = await this.storage.write(createdMemory);
564
+ this.scheduleVectorUpsert(createdMemory);
565
+
473
566
  return {
474
567
  id,
475
568
  sessionId,
@@ -480,23 +573,18 @@ export class MemoryService {
480
573
  };
481
574
  }
482
575
 
483
- /**
484
- * Gets all unique categories
485
- *
486
- * @returns Array of category names
487
- */
576
+ // ---------------------------------------------------------------------------
577
+ // Metadata helpers
578
+ // ---------------------------------------------------------------------------
579
+
488
580
  async getCategories(): Promise<GetCategoriesOutput> {
489
581
  try {
490
582
  const listResult = await this.list({ limit: 1000 });
491
583
  const categories = new Set<string>();
492
-
493
584
  for (const memory of listResult.memories) {
494
585
  categories.add(memory.category);
495
586
  }
496
-
497
- return {
498
- categories: Array.from(categories).sort(),
499
- };
587
+ return { categories: Array.from(categories).sort() };
500
588
  } catch (error) {
501
589
  throw new ServiceError(
502
590
  `Failed to get categories: ${error instanceof Error ? error.message : 'Unknown error'}`,
@@ -505,25 +593,16 @@ export class MemoryService {
505
593
  }
506
594
  }
507
595
 
508
- /**
509
- * Gets all unique tags
510
- *
511
- * @returns Array of tag names
512
- */
513
596
  async getTags(): Promise<GetTagsOutput> {
514
597
  try {
515
598
  const listResult = await this.list({ limit: 1000 });
516
599
  const tags = new Set<string>();
517
-
518
600
  for (const memory of listResult.memories) {
519
601
  for (const tag of memory.tags) {
520
602
  tags.add(tag);
521
603
  }
522
604
  }
523
-
524
- return {
525
- tags: Array.from(tags).sort(),
526
- };
605
+ return { tags: Array.from(tags).sort() };
527
606
  } catch (error) {
528
607
  throw new ServiceError(
529
608
  `Failed to get tags: ${error instanceof Error ? error.message : 'Unknown error'}`,
@@ -532,12 +611,6 @@ export class MemoryService {
532
611
  }
533
612
  }
534
613
 
535
- /**
536
- * Extracts ID from file content
537
- *
538
- * @param content - File content
539
- * @returns ID string
540
- */
541
614
  private extractIdFromContent(content: string): string {
542
615
  const match = content.match(/id:\s*"?([^"\n]+)"?/);
543
616
  if (!match) {