@aitytech/agentkits-memory 1.0.1 → 2.0.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.
- package/README.md +54 -5
- package/dist/better-sqlite3-backend.d.ts +192 -0
- package/dist/better-sqlite3-backend.d.ts.map +1 -0
- package/dist/better-sqlite3-backend.js +801 -0
- package/dist/better-sqlite3-backend.js.map +1 -0
- package/dist/cli/save.js +0 -0
- package/dist/cli/setup.d.ts +6 -2
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +289 -42
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/viewer.js +25 -56
- package/dist/cli/viewer.js.map +1 -1
- package/dist/cli/web-viewer.d.ts +2 -1
- package/dist/cli/web-viewer.d.ts.map +1 -1
- package/dist/cli/web-viewer.js +791 -141
- package/dist/cli/web-viewer.js.map +1 -1
- package/dist/embeddings/embedding-cache.d.ts +131 -0
- package/dist/embeddings/embedding-cache.d.ts.map +1 -0
- package/dist/embeddings/embedding-cache.js +217 -0
- package/dist/embeddings/embedding-cache.js.map +1 -0
- package/dist/embeddings/index.d.ts +11 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/index.js +11 -0
- package/dist/embeddings/index.js.map +1 -0
- package/dist/embeddings/local-embeddings.d.ts +140 -0
- package/dist/embeddings/local-embeddings.d.ts.map +1 -0
- package/dist/embeddings/local-embeddings.js +293 -0
- package/dist/embeddings/local-embeddings.js.map +1 -0
- package/dist/hooks/context.d.ts +6 -1
- package/dist/hooks/context.d.ts.map +1 -1
- package/dist/hooks/context.js +12 -2
- package/dist/hooks/context.js.map +1 -1
- package/dist/hooks/observation.d.ts +6 -1
- package/dist/hooks/observation.d.ts.map +1 -1
- package/dist/hooks/observation.js +12 -2
- package/dist/hooks/observation.js.map +1 -1
- package/dist/hooks/service.d.ts +1 -6
- package/dist/hooks/service.d.ts.map +1 -1
- package/dist/hooks/service.js +33 -85
- package/dist/hooks/service.js.map +1 -1
- package/dist/hooks/session-init.d.ts +6 -1
- package/dist/hooks/session-init.d.ts.map +1 -1
- package/dist/hooks/session-init.js +12 -2
- package/dist/hooks/session-init.js.map +1 -1
- package/dist/hooks/summarize.d.ts +6 -1
- package/dist/hooks/summarize.d.ts.map +1 -1
- package/dist/hooks/summarize.js +12 -2
- package/dist/hooks/summarize.js.map +1 -1
- package/dist/index.d.ts +10 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +172 -94
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +17 -3
- package/dist/mcp/server.js.map +1 -1
- package/dist/migration.js +3 -3
- package/dist/migration.js.map +1 -1
- package/dist/search/hybrid-search.d.ts +262 -0
- package/dist/search/hybrid-search.d.ts.map +1 -0
- package/dist/search/hybrid-search.js +688 -0
- package/dist/search/hybrid-search.js.map +1 -0
- package/dist/search/index.d.ts +13 -0
- package/dist/search/index.d.ts.map +1 -0
- package/dist/search/index.js +13 -0
- package/dist/search/index.js.map +1 -0
- package/dist/search/token-economics.d.ts +161 -0
- package/dist/search/token-economics.d.ts.map +1 -0
- package/dist/search/token-economics.js +239 -0
- package/dist/search/token-economics.js.map +1 -0
- package/dist/types.d.ts +0 -68
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +6 -4
- package/src/__tests__/better-sqlite3-backend.test.ts +1466 -0
- package/src/__tests__/cache-manager.test.ts +499 -0
- package/src/__tests__/embedding-integration.test.ts +481 -0
- package/src/__tests__/hnsw-index.test.ts +727 -0
- package/src/__tests__/index.test.ts +432 -0
- package/src/better-sqlite3-backend.ts +1000 -0
- package/src/cli/setup.ts +358 -47
- package/src/cli/viewer.ts +28 -63
- package/src/cli/web-viewer.ts +936 -182
- package/src/embeddings/__tests__/embedding-cache.test.ts +269 -0
- package/src/embeddings/__tests__/local-embeddings.test.ts +495 -0
- package/src/embeddings/embedding-cache.ts +318 -0
- package/src/embeddings/index.ts +20 -0
- package/src/embeddings/local-embeddings.ts +419 -0
- package/src/hooks/__tests__/handlers.test.ts +58 -17
- package/src/hooks/__tests__/integration.test.ts +77 -26
- package/src/hooks/context.ts +13 -2
- package/src/hooks/observation.ts +13 -2
- package/src/hooks/service.ts +39 -100
- package/src/hooks/session-init.ts +13 -2
- package/src/hooks/summarize.ts +13 -2
- package/src/index.ts +210 -116
- package/src/mcp/server.ts +20 -3
- package/src/search/__tests__/hybrid-search.test.ts +669 -0
- package/src/search/__tests__/token-economics.test.ts +276 -0
- package/src/search/hybrid-search.ts +968 -0
- package/src/search/index.ts +29 -0
- package/src/search/token-economics.ts +367 -0
- package/src/types.ts +0 -96
- package/src/__tests__/sqljs-backend.test.ts +0 -410
- package/src/migration.ts +0 -574
- package/src/sql.js.d.ts +0 -70
- package/src/sqljs-backend.ts +0 -789
package/src/hooks/summarize.ts
CHANGED
|
@@ -22,9 +22,20 @@ import { MemoryHookService } from './service.js';
|
|
|
22
22
|
*/
|
|
23
23
|
export class SummarizeHook implements EventHandler {
|
|
24
24
|
private service: MemoryHookService;
|
|
25
|
+
private ownsService: boolean;
|
|
25
26
|
|
|
26
|
-
constructor(service: MemoryHookService) {
|
|
27
|
+
constructor(service: MemoryHookService, ownsService = false) {
|
|
27
28
|
this.service = service;
|
|
29
|
+
this.ownsService = ownsService;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Shutdown the hook (closes database if owned)
|
|
34
|
+
*/
|
|
35
|
+
async shutdown(): Promise<void> {
|
|
36
|
+
if (this.ownsService) {
|
|
37
|
+
await this.service.shutdown();
|
|
38
|
+
}
|
|
28
39
|
}
|
|
29
40
|
|
|
30
41
|
/**
|
|
@@ -83,7 +94,7 @@ export class SummarizeHook implements EventHandler {
|
|
|
83
94
|
*/
|
|
84
95
|
export function createSummarizeHook(cwd: string): SummarizeHook {
|
|
85
96
|
const service = new MemoryHookService(cwd);
|
|
86
|
-
return new SummarizeHook(service);
|
|
97
|
+
return new SummarizeHook(service, true);
|
|
87
98
|
}
|
|
88
99
|
|
|
89
100
|
export default SummarizeHook;
|
package/src/index.ts
CHANGED
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
*/
|
|
43
43
|
|
|
44
44
|
import { EventEmitter } from 'node:events';
|
|
45
|
-
import { existsSync, mkdirSync
|
|
45
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
46
46
|
import * as path from 'node:path';
|
|
47
47
|
import {
|
|
48
48
|
IMemoryBackend,
|
|
@@ -56,23 +56,53 @@ import {
|
|
|
56
56
|
HealthCheckResult,
|
|
57
57
|
EmbeddingGenerator,
|
|
58
58
|
SessionInfo,
|
|
59
|
-
MigrationResult,
|
|
60
59
|
createDefaultEntry,
|
|
61
60
|
generateSessionId,
|
|
62
61
|
DEFAULT_NAMESPACES,
|
|
63
62
|
NAMESPACE_TYPE_MAP,
|
|
64
63
|
} from './types.js';
|
|
65
|
-
import {
|
|
64
|
+
import { BetterSqlite3Backend } from './better-sqlite3-backend.js';
|
|
66
65
|
import { CacheManager } from './cache-manager.js';
|
|
67
66
|
import { HNSWIndex } from './hnsw-index.js';
|
|
68
|
-
import { MemoryMigrator, migrateMarkdownMemory } from './migration.js';
|
|
69
67
|
|
|
70
68
|
// Re-export types
|
|
71
69
|
export * from './types.js';
|
|
72
|
-
export { SqlJsBackend } from './sqljs-backend.js';
|
|
73
70
|
export { CacheManager, TieredCacheManager } from './cache-manager.js';
|
|
74
71
|
export { HNSWIndex } from './hnsw-index.js';
|
|
75
|
-
export {
|
|
72
|
+
export {
|
|
73
|
+
LocalEmbeddingsService,
|
|
74
|
+
createLocalEmbeddings,
|
|
75
|
+
createEmbeddingGenerator,
|
|
76
|
+
PersistentEmbeddingCache,
|
|
77
|
+
createPersistentEmbeddingCache,
|
|
78
|
+
} from './embeddings/index.js';
|
|
79
|
+
|
|
80
|
+
export {
|
|
81
|
+
HybridSearchEngine,
|
|
82
|
+
createHybridSearchEngine,
|
|
83
|
+
TokenEconomicsTracker,
|
|
84
|
+
createTokenEconomicsTracker,
|
|
85
|
+
} from './search/index.js';
|
|
86
|
+
|
|
87
|
+
export {
|
|
88
|
+
BetterSqlite3Backend,
|
|
89
|
+
createBetterSqlite3Backend,
|
|
90
|
+
createJapaneseOptimizedBackend,
|
|
91
|
+
} from './better-sqlite3-backend.js';
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Create a better-sqlite3 backend with FTS5 trigram tokenizer for CJK support
|
|
95
|
+
*/
|
|
96
|
+
export function createAutoBackend(
|
|
97
|
+
databasePath: string,
|
|
98
|
+
options: { verbose?: boolean } = {}
|
|
99
|
+
): IMemoryBackend {
|
|
100
|
+
return new BetterSqlite3Backend({
|
|
101
|
+
databasePath,
|
|
102
|
+
ftsTokenizer: 'trigram', // Full CJK support
|
|
103
|
+
verbose: options.verbose,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
76
106
|
|
|
77
107
|
/**
|
|
78
108
|
* Configuration for ProjectMemoryService
|
|
@@ -140,7 +170,7 @@ const DEFAULT_CONFIG: ProjectMemoryConfig = {
|
|
|
140
170
|
*/
|
|
141
171
|
export class ProjectMemoryService extends EventEmitter implements IMemoryBackend {
|
|
142
172
|
private config: ProjectMemoryConfig;
|
|
143
|
-
private backend:
|
|
173
|
+
private backend: IMemoryBackend | null = null;
|
|
144
174
|
private cache: CacheManager<MemoryEntry> | null = null;
|
|
145
175
|
private vectorIndex: HNSWIndex | null = null;
|
|
146
176
|
private initialized: boolean = false;
|
|
@@ -161,14 +191,7 @@ export class ProjectMemoryService extends EventEmitter implements IMemoryBackend
|
|
|
161
191
|
mkdirSync(this.config.baseDir, { recursive: true });
|
|
162
192
|
}
|
|
163
193
|
|
|
164
|
-
//
|
|
165
|
-
const dbPath = path.join(this.config.baseDir, this.config.dbFilename);
|
|
166
|
-
this.backend = new SqlJsBackend({
|
|
167
|
-
databasePath: dbPath,
|
|
168
|
-
autoPersistInterval: this.config.autoPersistInterval,
|
|
169
|
-
maxEntries: this.config.maxEntries,
|
|
170
|
-
verbose: this.config.verbose,
|
|
171
|
-
});
|
|
194
|
+
// Backend is created lazily in initialize() for auto-detection
|
|
172
195
|
|
|
173
196
|
// Initialize cache if enabled
|
|
174
197
|
if (this.config.cacheEnabled) {
|
|
@@ -189,12 +212,6 @@ export class ProjectMemoryService extends EventEmitter implements IMemoryBackend
|
|
|
189
212
|
metric: 'cosine',
|
|
190
213
|
});
|
|
191
214
|
}
|
|
192
|
-
|
|
193
|
-
// Forward backend events
|
|
194
|
-
this.backend.on('entry:stored', (data) => this.emit('entry:stored', data));
|
|
195
|
-
this.backend.on('entry:updated', (data) => this.emit('entry:updated', data));
|
|
196
|
-
this.backend.on('entry:deleted', (data) => this.emit('entry:deleted', data));
|
|
197
|
-
this.backend.on('persisted', (data) => this.emit('persisted', data));
|
|
198
215
|
}
|
|
199
216
|
|
|
200
217
|
// ===== Lifecycle =====
|
|
@@ -202,6 +219,19 @@ export class ProjectMemoryService extends EventEmitter implements IMemoryBackend
|
|
|
202
219
|
async initialize(): Promise<void> {
|
|
203
220
|
if (this.initialized) return;
|
|
204
221
|
|
|
222
|
+
// Create backend with better-sqlite3 (FTS5 trigram tokenizer for CJK support)
|
|
223
|
+
const dbPath = path.join(this.config.baseDir, this.config.dbFilename);
|
|
224
|
+
this.backend = createAutoBackend(dbPath, { verbose: this.config.verbose });
|
|
225
|
+
|
|
226
|
+
// Forward backend events (if backend is an EventEmitter)
|
|
227
|
+
const backendAsEmitter = this.backend as unknown as EventEmitter | undefined;
|
|
228
|
+
if (backendAsEmitter && typeof backendAsEmitter.on === 'function') {
|
|
229
|
+
backendAsEmitter.on('entry:stored', (data) => this.emit('entry:stored', data));
|
|
230
|
+
backendAsEmitter.on('entry:updated', (data) => this.emit('entry:updated', data));
|
|
231
|
+
backendAsEmitter.on('entry:deleted', (data) => this.emit('entry:deleted', data));
|
|
232
|
+
backendAsEmitter.on('persisted', (data) => this.emit('persisted', data));
|
|
233
|
+
}
|
|
234
|
+
|
|
205
235
|
await this.backend.initialize();
|
|
206
236
|
|
|
207
237
|
// Rebuild vector index from existing embeddings
|
|
@@ -226,7 +256,9 @@ export class ProjectMemoryService extends EventEmitter implements IMemoryBackend
|
|
|
226
256
|
this.cache.shutdown();
|
|
227
257
|
}
|
|
228
258
|
|
|
229
|
-
|
|
259
|
+
if (this.backend) {
|
|
260
|
+
await this.backend.shutdown();
|
|
261
|
+
}
|
|
230
262
|
this.initialized = false;
|
|
231
263
|
this.emit('shutdown');
|
|
232
264
|
}
|
|
@@ -234,7 +266,7 @@ export class ProjectMemoryService extends EventEmitter implements IMemoryBackend
|
|
|
234
266
|
// ===== IMemoryBackend Implementation =====
|
|
235
267
|
|
|
236
268
|
async store(entry: MemoryEntry): Promise<void> {
|
|
237
|
-
this.ensureInitialized();
|
|
269
|
+
const backend = this.ensureInitialized();
|
|
238
270
|
|
|
239
271
|
// Generate embedding if enabled and not present
|
|
240
272
|
if (this.config.embeddingGenerator && !entry.embedding) {
|
|
@@ -253,7 +285,7 @@ export class ProjectMemoryService extends EventEmitter implements IMemoryBackend
|
|
|
253
285
|
}
|
|
254
286
|
|
|
255
287
|
// Store in backend
|
|
256
|
-
await
|
|
288
|
+
await backend.store(entry);
|
|
257
289
|
|
|
258
290
|
// Update cache
|
|
259
291
|
if (this.cache) {
|
|
@@ -268,7 +300,7 @@ export class ProjectMemoryService extends EventEmitter implements IMemoryBackend
|
|
|
268
300
|
}
|
|
269
301
|
|
|
270
302
|
async get(id: string): Promise<MemoryEntry | null> {
|
|
271
|
-
this.ensureInitialized();
|
|
303
|
+
const backend = this.ensureInitialized();
|
|
272
304
|
|
|
273
305
|
// Check cache first
|
|
274
306
|
if (this.cache) {
|
|
@@ -276,7 +308,7 @@ export class ProjectMemoryService extends EventEmitter implements IMemoryBackend
|
|
|
276
308
|
if (cached) return cached;
|
|
277
309
|
}
|
|
278
310
|
|
|
279
|
-
const entry = await
|
|
311
|
+
const entry = await backend.get(id);
|
|
280
312
|
|
|
281
313
|
// Update cache
|
|
282
314
|
if (entry && this.cache) {
|
|
@@ -287,7 +319,7 @@ export class ProjectMemoryService extends EventEmitter implements IMemoryBackend
|
|
|
287
319
|
}
|
|
288
320
|
|
|
289
321
|
async getByKey(namespace: string, key: string): Promise<MemoryEntry | null> {
|
|
290
|
-
this.ensureInitialized();
|
|
322
|
+
const backend = this.ensureInitialized();
|
|
291
323
|
|
|
292
324
|
const cacheKey = `${namespace}:${key}`;
|
|
293
325
|
|
|
@@ -297,7 +329,7 @@ export class ProjectMemoryService extends EventEmitter implements IMemoryBackend
|
|
|
297
329
|
if (cached) return cached;
|
|
298
330
|
}
|
|
299
331
|
|
|
300
|
-
const entry = await
|
|
332
|
+
const entry = await backend.getByKey(namespace, key);
|
|
301
333
|
|
|
302
334
|
// Update cache
|
|
303
335
|
if (entry && this.cache) {
|
|
@@ -309,16 +341,16 @@ export class ProjectMemoryService extends EventEmitter implements IMemoryBackend
|
|
|
309
341
|
}
|
|
310
342
|
|
|
311
343
|
async update(id: string, update: MemoryEntryUpdate): Promise<MemoryEntry | null> {
|
|
312
|
-
this.ensureInitialized();
|
|
344
|
+
const backend = this.ensureInitialized();
|
|
313
345
|
|
|
314
|
-
const updated = await
|
|
346
|
+
const updated = await backend.update(id, update);
|
|
315
347
|
|
|
316
348
|
if (updated) {
|
|
317
349
|
// Regenerate embedding if content changed
|
|
318
350
|
if (update.content && this.config.embeddingGenerator) {
|
|
319
351
|
try {
|
|
320
352
|
updated.embedding = await this.config.embeddingGenerator(updated.content);
|
|
321
|
-
await
|
|
353
|
+
await backend.store(updated);
|
|
322
354
|
|
|
323
355
|
// Update vector index
|
|
324
356
|
if (this.vectorIndex && updated.embedding) {
|
|
@@ -341,12 +373,12 @@ export class ProjectMemoryService extends EventEmitter implements IMemoryBackend
|
|
|
341
373
|
}
|
|
342
374
|
|
|
343
375
|
async delete(id: string): Promise<boolean> {
|
|
344
|
-
this.ensureInitialized();
|
|
376
|
+
const backend = this.ensureInitialized();
|
|
345
377
|
|
|
346
378
|
const entry = await this.get(id);
|
|
347
379
|
if (!entry) return false;
|
|
348
380
|
|
|
349
|
-
const result = await
|
|
381
|
+
const result = await backend.delete(id);
|
|
350
382
|
|
|
351
383
|
if (result) {
|
|
352
384
|
// Remove from cache
|
|
@@ -365,12 +397,136 @@ export class ProjectMemoryService extends EventEmitter implements IMemoryBackend
|
|
|
365
397
|
}
|
|
366
398
|
|
|
367
399
|
async query(query: MemoryQuery): Promise<MemoryEntry[]> {
|
|
368
|
-
this.ensureInitialized();
|
|
369
|
-
|
|
400
|
+
const backend = this.ensureInitialized();
|
|
401
|
+
|
|
402
|
+
// If no content search term, just use basic query (ordered by date)
|
|
403
|
+
if (!query.content && !query.embedding) {
|
|
404
|
+
return backend.query(query);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Handle different search types
|
|
408
|
+
// 'semantic' = vector only, 'hybrid' = combined, others fall back to FTS
|
|
409
|
+
const searchType = query.type || 'hybrid';
|
|
410
|
+
const limit = query.limit || 100;
|
|
411
|
+
|
|
412
|
+
// For exact, prefix, tag search - use backend directly
|
|
413
|
+
if (searchType === 'exact' || searchType === 'prefix' || searchType === 'tag') {
|
|
414
|
+
return backend.query(query);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// For semantic or hybrid search, need embedding
|
|
418
|
+
let embedding = query.embedding;
|
|
419
|
+
if (!embedding && query.content && this.config.embeddingGenerator) {
|
|
420
|
+
try {
|
|
421
|
+
embedding = await this.config.embeddingGenerator(query.content);
|
|
422
|
+
} catch (error) {
|
|
423
|
+
console.warn('[ProjectMemoryService] Failed to generate embedding, falling back to text search');
|
|
424
|
+
const betterBackend = backend as BetterSqlite3Backend;
|
|
425
|
+
if (typeof betterBackend.searchFts === 'function') {
|
|
426
|
+
return betterBackend.searchFts(query.content, {
|
|
427
|
+
namespace: query.namespace,
|
|
428
|
+
limit,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
return backend.query(query);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (!embedding) {
|
|
436
|
+
// No embedding available, fall back to FTS
|
|
437
|
+
const betterBackend = backend as BetterSqlite3Backend;
|
|
438
|
+
if (query.content && typeof betterBackend.searchFts === 'function') {
|
|
439
|
+
return betterBackend.searchFts(query.content, {
|
|
440
|
+
namespace: query.namespace,
|
|
441
|
+
limit,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
return backend.query(query);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// For semantic-only (vector) search
|
|
448
|
+
if (searchType === 'semantic') {
|
|
449
|
+
const results = await this.search(embedding, {
|
|
450
|
+
k: limit,
|
|
451
|
+
threshold: query.threshold,
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// Filter by namespace if specified
|
|
455
|
+
const filtered = query.namespace
|
|
456
|
+
? results.filter((r) => r.entry.namespace === query.namespace)
|
|
457
|
+
: results;
|
|
458
|
+
|
|
459
|
+
return filtered.map((r) => r.entry);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// For hybrid search: combine FTS and vector results
|
|
463
|
+
const vectorResults = await this.search(embedding, {
|
|
464
|
+
k: limit * 2, // Get more candidates for fusion
|
|
465
|
+
threshold: 0.1, // Low threshold to get more candidates
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// Filter vector results by namespace if specified
|
|
469
|
+
const filteredVectorResults = query.namespace
|
|
470
|
+
? vectorResults.filter((r) => r.entry.namespace === query.namespace)
|
|
471
|
+
: vectorResults;
|
|
472
|
+
|
|
473
|
+
// Get FTS results
|
|
474
|
+
const betterBackend = backend as BetterSqlite3Backend;
|
|
475
|
+
let ftsResults: MemoryEntry[] = [];
|
|
476
|
+
if (query.content && typeof betterBackend.searchFts === 'function') {
|
|
477
|
+
ftsResults = await betterBackend.searchFts(query.content, {
|
|
478
|
+
namespace: query.namespace,
|
|
479
|
+
limit: limit * 2,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Combine and rank results using score fusion
|
|
484
|
+
const scoreMap = new Map<string, { entry: MemoryEntry; score: number; vectorScore: number; ftsScore: number }>();
|
|
485
|
+
|
|
486
|
+
// Add vector results with scores (semantic weight: 0.7)
|
|
487
|
+
filteredVectorResults.forEach((r) => {
|
|
488
|
+
const vectorScore = r.score; // Already 0-1 similarity
|
|
489
|
+
scoreMap.set(r.entry.id, {
|
|
490
|
+
entry: r.entry,
|
|
491
|
+
score: vectorScore * 0.7,
|
|
492
|
+
vectorScore,
|
|
493
|
+
ftsScore: 0,
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// Add FTS results with scores (keyword weight: 0.3)
|
|
498
|
+
ftsResults.forEach((entry, index) => {
|
|
499
|
+
const ftsScore = 1 - (index / (ftsResults.length || 1)); // Rank-based score
|
|
500
|
+
const existing = scoreMap.get(entry.id);
|
|
501
|
+
if (existing) {
|
|
502
|
+
existing.ftsScore = ftsScore;
|
|
503
|
+
existing.score += ftsScore * 0.3;
|
|
504
|
+
} else {
|
|
505
|
+
scoreMap.set(entry.id, {
|
|
506
|
+
entry,
|
|
507
|
+
score: ftsScore * 0.3,
|
|
508
|
+
vectorScore: 0,
|
|
509
|
+
ftsScore,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// Sort by combined score and return top results
|
|
515
|
+
const sorted = Array.from(scoreMap.values())
|
|
516
|
+
.sort((a, b) => b.score - a.score)
|
|
517
|
+
.slice(0, limit);
|
|
518
|
+
|
|
519
|
+
// Attach scores to entries for debugging/display
|
|
520
|
+
return sorted.map((r) => ({
|
|
521
|
+
...r.entry,
|
|
522
|
+
_score: r.score,
|
|
523
|
+
_vectorScore: r.vectorScore,
|
|
524
|
+
_ftsScore: r.ftsScore,
|
|
525
|
+
}));
|
|
370
526
|
}
|
|
371
527
|
|
|
372
528
|
async search(embedding: Float32Array, options: SearchOptions): Promise<SearchResult[]> {
|
|
373
|
-
this.ensureInitialized();
|
|
529
|
+
const backend = this.ensureInitialized();
|
|
374
530
|
|
|
375
531
|
if (this.vectorIndex) {
|
|
376
532
|
// Use HNSW index for fast search
|
|
@@ -392,11 +548,11 @@ export class ProjectMemoryService extends EventEmitter implements IMemoryBackend
|
|
|
392
548
|
}
|
|
393
549
|
|
|
394
550
|
// Fallback to brute-force search in backend
|
|
395
|
-
return
|
|
551
|
+
return backend.search(embedding, options);
|
|
396
552
|
}
|
|
397
553
|
|
|
398
554
|
async bulkInsert(entries: MemoryEntry[]): Promise<void> {
|
|
399
|
-
this.ensureInitialized();
|
|
555
|
+
this.ensureInitialized(); // store() already gets backend
|
|
400
556
|
|
|
401
557
|
for (const entry of entries) {
|
|
402
558
|
await this.store(entry);
|
|
@@ -418,30 +574,30 @@ export class ProjectMemoryService extends EventEmitter implements IMemoryBackend
|
|
|
418
574
|
}
|
|
419
575
|
|
|
420
576
|
async count(namespace?: string): Promise<number> {
|
|
421
|
-
this.ensureInitialized();
|
|
422
|
-
return
|
|
577
|
+
const backend = this.ensureInitialized();
|
|
578
|
+
return backend.count(namespace);
|
|
423
579
|
}
|
|
424
580
|
|
|
425
581
|
async listNamespaces(): Promise<string[]> {
|
|
426
|
-
this.ensureInitialized();
|
|
427
|
-
return
|
|
582
|
+
const backend = this.ensureInitialized();
|
|
583
|
+
return backend.listNamespaces();
|
|
428
584
|
}
|
|
429
585
|
|
|
430
586
|
async clearNamespace(namespace: string): Promise<number> {
|
|
431
|
-
this.ensureInitialized();
|
|
587
|
+
const backend = this.ensureInitialized();
|
|
432
588
|
|
|
433
589
|
// Clear from cache
|
|
434
590
|
if (this.cache) {
|
|
435
591
|
this.cache.invalidatePattern(new RegExp(`^${namespace}:`));
|
|
436
592
|
}
|
|
437
593
|
|
|
438
|
-
return
|
|
594
|
+
return backend.clearNamespace(namespace);
|
|
439
595
|
}
|
|
440
596
|
|
|
441
597
|
async getStats(): Promise<BackendStats> {
|
|
442
|
-
this.ensureInitialized();
|
|
598
|
+
const backend = this.ensureInitialized();
|
|
443
599
|
|
|
444
|
-
const stats = await
|
|
600
|
+
const stats = await backend.getStats();
|
|
445
601
|
|
|
446
602
|
// Add HNSW stats if available
|
|
447
603
|
if (this.vectorIndex) {
|
|
@@ -457,8 +613,8 @@ export class ProjectMemoryService extends EventEmitter implements IMemoryBackend
|
|
|
457
613
|
}
|
|
458
614
|
|
|
459
615
|
async healthCheck(): Promise<HealthCheckResult> {
|
|
460
|
-
this.ensureInitialized();
|
|
461
|
-
return
|
|
616
|
+
const backend = this.ensureInitialized();
|
|
617
|
+
return backend.healthCheck();
|
|
462
618
|
}
|
|
463
619
|
|
|
464
620
|
// ===== Convenience Methods =====
|
|
@@ -626,82 +782,20 @@ export class ProjectMemoryService extends EventEmitter implements IMemoryBackend
|
|
|
626
782
|
.filter((s): s is SessionInfo => s !== null);
|
|
627
783
|
}
|
|
628
784
|
|
|
629
|
-
// ===== Migration =====
|
|
630
|
-
|
|
631
|
-
/**
|
|
632
|
-
* Migrate from existing markdown memory files
|
|
633
|
-
*/
|
|
634
|
-
async migrateFromMarkdown(options: { generateEmbeddings?: boolean } = {}): Promise<MigrationResult> {
|
|
635
|
-
this.ensureInitialized();
|
|
636
|
-
|
|
637
|
-
const result = await migrateMarkdownMemory(
|
|
638
|
-
this.config.baseDir,
|
|
639
|
-
async (entry) => this.store(entry),
|
|
640
|
-
{
|
|
641
|
-
generateEmbeddings: options.generateEmbeddings ?? false,
|
|
642
|
-
}
|
|
643
|
-
);
|
|
644
|
-
|
|
645
|
-
this.emit('migration:completed', result);
|
|
646
|
-
return result;
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
// ===== Export =====
|
|
650
|
-
|
|
651
|
-
/**
|
|
652
|
-
* Export namespace to markdown (for git-friendly backup)
|
|
653
|
-
*/
|
|
654
|
-
async exportToMarkdown(namespace: string, outputPath?: string): Promise<string> {
|
|
655
|
-
const entries = await this.getByNamespace(namespace);
|
|
656
|
-
const filePath = outputPath || path.join(this.config.baseDir, `${namespace}.md`);
|
|
657
|
-
|
|
658
|
-
let markdown = `---\nnamespace: ${namespace}\nexported: ${new Date().toISOString()}\nentries: ${entries.length}\n---\n\n`;
|
|
659
|
-
|
|
660
|
-
for (const entry of entries) {
|
|
661
|
-
markdown += `## ${entry.key}\n\n`;
|
|
662
|
-
markdown += entry.content;
|
|
663
|
-
markdown += '\n\n';
|
|
664
|
-
|
|
665
|
-
if (entry.tags.length > 0) {
|
|
666
|
-
markdown += `*Tags: ${entry.tags.join(', ')}*\n\n`;
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
markdown += '---\n\n';
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
writeFileSync(filePath, markdown, 'utf-8');
|
|
673
|
-
|
|
674
|
-
return filePath;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
/**
|
|
678
|
-
* Export all namespaces to markdown
|
|
679
|
-
*/
|
|
680
|
-
async exportAllToMarkdown(): Promise<string[]> {
|
|
681
|
-
const namespaces = await this.listNamespaces();
|
|
682
|
-
const files: string[] = [];
|
|
683
|
-
|
|
684
|
-
for (const namespace of namespaces) {
|
|
685
|
-
const file = await this.exportToMarkdown(namespace);
|
|
686
|
-
files.push(file);
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
return files;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
785
|
// ===== Private Methods =====
|
|
693
786
|
|
|
694
|
-
private ensureInitialized():
|
|
695
|
-
if (!this.initialized) {
|
|
787
|
+
private ensureInitialized(): IMemoryBackend {
|
|
788
|
+
if (!this.initialized || !this.backend) {
|
|
696
789
|
throw new Error('ProjectMemoryService not initialized. Call initialize() first.');
|
|
697
790
|
}
|
|
791
|
+
return this.backend;
|
|
698
792
|
}
|
|
699
793
|
|
|
700
794
|
private async rebuildVectorIndex(): Promise<void> {
|
|
701
|
-
if (!this.vectorIndex) return;
|
|
795
|
+
if (!this.vectorIndex || !this.backend) return;
|
|
702
796
|
|
|
703
|
-
// Get all entries with embeddings
|
|
704
|
-
const entries = await this.query({
|
|
797
|
+
// Get all entries with embeddings (use backend directly to avoid ensureInitialized check)
|
|
798
|
+
const entries = await this.backend.query({
|
|
705
799
|
type: 'hybrid',
|
|
706
800
|
limit: this.config.maxEntries,
|
|
707
801
|
});
|
package/src/mcp/server.ts
CHANGED
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import * as readline from 'node:readline';
|
|
23
|
-
import
|
|
23
|
+
import * as path from 'node:path';
|
|
24
|
+
import { ProjectMemoryService, MemoryEntry, MemoryQuery, DEFAULT_NAMESPACES, LocalEmbeddingsService } from '../index.js';
|
|
24
25
|
import { MEMORY_TOOLS } from './tools.js';
|
|
25
26
|
import type {
|
|
26
27
|
JSONRPCRequest,
|
|
@@ -47,6 +48,7 @@ const CATEGORY_TO_NAMESPACE: Record<string, string> = {
|
|
|
47
48
|
*/
|
|
48
49
|
class MemoryMCPServer {
|
|
49
50
|
private service: ProjectMemoryService | null = null;
|
|
51
|
+
private embeddingsService: LocalEmbeddingsService | null = null;
|
|
50
52
|
private projectDir: string;
|
|
51
53
|
private initialized = false;
|
|
52
54
|
|
|
@@ -55,13 +57,28 @@ class MemoryMCPServer {
|
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
/**
|
|
58
|
-
* Initialize the memory service
|
|
60
|
+
* Initialize the memory service with embeddings support
|
|
59
61
|
*/
|
|
60
62
|
private async ensureInitialized(): Promise<ProjectMemoryService> {
|
|
61
63
|
if (!this.service || !this.initialized) {
|
|
64
|
+
const baseDir = path.join(this.projectDir, '.claude/memory');
|
|
65
|
+
|
|
66
|
+
// Initialize embeddings service
|
|
67
|
+
this.embeddingsService = new LocalEmbeddingsService({
|
|
68
|
+
cacheDir: path.join(baseDir, 'embeddings-cache'),
|
|
69
|
+
});
|
|
70
|
+
await this.embeddingsService.initialize();
|
|
71
|
+
|
|
72
|
+
// Create embedding generator function
|
|
73
|
+
const embeddingGenerator = async (text: string): Promise<Float32Array> => {
|
|
74
|
+
const result = await this.embeddingsService!.embed(text);
|
|
75
|
+
return result.embedding;
|
|
76
|
+
};
|
|
77
|
+
|
|
62
78
|
this.service = new ProjectMemoryService({
|
|
63
|
-
baseDir
|
|
79
|
+
baseDir,
|
|
64
80
|
dbFilename: 'memory.db',
|
|
81
|
+
embeddingGenerator,
|
|
65
82
|
});
|
|
66
83
|
await this.service.initialize();
|
|
67
84
|
this.initialized = true;
|