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