@aitytech/agentkits-memory 1.0.0

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 (116) hide show
  1. package/README.md +250 -0
  2. package/dist/cache-manager.d.ts +134 -0
  3. package/dist/cache-manager.d.ts.map +1 -0
  4. package/dist/cache-manager.js +407 -0
  5. package/dist/cache-manager.js.map +1 -0
  6. package/dist/cli/save.d.ts +20 -0
  7. package/dist/cli/save.d.ts.map +1 -0
  8. package/dist/cli/save.js +94 -0
  9. package/dist/cli/save.js.map +1 -0
  10. package/dist/cli/setup.d.ts +18 -0
  11. package/dist/cli/setup.d.ts.map +1 -0
  12. package/dist/cli/setup.js +163 -0
  13. package/dist/cli/setup.js.map +1 -0
  14. package/dist/cli/viewer.d.ts +21 -0
  15. package/dist/cli/viewer.d.ts.map +1 -0
  16. package/dist/cli/viewer.js +182 -0
  17. package/dist/cli/viewer.js.map +1 -0
  18. package/dist/hnsw-index.d.ts +111 -0
  19. package/dist/hnsw-index.d.ts.map +1 -0
  20. package/dist/hnsw-index.js +781 -0
  21. package/dist/hnsw-index.js.map +1 -0
  22. package/dist/hooks/cli.d.ts +20 -0
  23. package/dist/hooks/cli.d.ts.map +1 -0
  24. package/dist/hooks/cli.js +102 -0
  25. package/dist/hooks/cli.js.map +1 -0
  26. package/dist/hooks/context.d.ts +31 -0
  27. package/dist/hooks/context.d.ts.map +1 -0
  28. package/dist/hooks/context.js +64 -0
  29. package/dist/hooks/context.js.map +1 -0
  30. package/dist/hooks/index.d.ts +16 -0
  31. package/dist/hooks/index.d.ts.map +1 -0
  32. package/dist/hooks/index.js +20 -0
  33. package/dist/hooks/index.js.map +1 -0
  34. package/dist/hooks/observation.d.ts +30 -0
  35. package/dist/hooks/observation.d.ts.map +1 -0
  36. package/dist/hooks/observation.js +79 -0
  37. package/dist/hooks/observation.js.map +1 -0
  38. package/dist/hooks/service.d.ts +102 -0
  39. package/dist/hooks/service.d.ts.map +1 -0
  40. package/dist/hooks/service.js +454 -0
  41. package/dist/hooks/service.js.map +1 -0
  42. package/dist/hooks/session-init.d.ts +30 -0
  43. package/dist/hooks/session-init.d.ts.map +1 -0
  44. package/dist/hooks/session-init.js +54 -0
  45. package/dist/hooks/session-init.js.map +1 -0
  46. package/dist/hooks/summarize.d.ts +30 -0
  47. package/dist/hooks/summarize.d.ts.map +1 -0
  48. package/dist/hooks/summarize.js +74 -0
  49. package/dist/hooks/summarize.js.map +1 -0
  50. package/dist/hooks/types.d.ts +193 -0
  51. package/dist/hooks/types.d.ts.map +1 -0
  52. package/dist/hooks/types.js +137 -0
  53. package/dist/hooks/types.js.map +1 -0
  54. package/dist/index.d.ts +173 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +564 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/mcp/index.d.ts +9 -0
  59. package/dist/mcp/index.d.ts.map +1 -0
  60. package/dist/mcp/index.js +9 -0
  61. package/dist/mcp/index.js.map +1 -0
  62. package/dist/mcp/server.d.ts +22 -0
  63. package/dist/mcp/server.d.ts.map +1 -0
  64. package/dist/mcp/server.js +368 -0
  65. package/dist/mcp/server.js.map +1 -0
  66. package/dist/mcp/tools.d.ts +14 -0
  67. package/dist/mcp/tools.d.ts.map +1 -0
  68. package/dist/mcp/tools.js +110 -0
  69. package/dist/mcp/tools.js.map +1 -0
  70. package/dist/mcp/types.d.ts +100 -0
  71. package/dist/mcp/types.d.ts.map +1 -0
  72. package/dist/mcp/types.js +9 -0
  73. package/dist/mcp/types.js.map +1 -0
  74. package/dist/migration.d.ts +77 -0
  75. package/dist/migration.d.ts.map +1 -0
  76. package/dist/migration.js +457 -0
  77. package/dist/migration.js.map +1 -0
  78. package/dist/sqljs-backend.d.ts +128 -0
  79. package/dist/sqljs-backend.d.ts.map +1 -0
  80. package/dist/sqljs-backend.js +623 -0
  81. package/dist/sqljs-backend.js.map +1 -0
  82. package/dist/types.d.ts +481 -0
  83. package/dist/types.d.ts.map +1 -0
  84. package/dist/types.js +73 -0
  85. package/dist/types.js.map +1 -0
  86. package/hooks.json +46 -0
  87. package/package.json +67 -0
  88. package/src/__tests__/index.test.ts +407 -0
  89. package/src/__tests__/sqljs-backend.test.ts +410 -0
  90. package/src/cache-manager.ts +515 -0
  91. package/src/cli/save.ts +109 -0
  92. package/src/cli/setup.ts +203 -0
  93. package/src/cli/viewer.ts +218 -0
  94. package/src/hnsw-index.ts +1013 -0
  95. package/src/hooks/__tests__/handlers.test.ts +298 -0
  96. package/src/hooks/__tests__/integration.test.ts +431 -0
  97. package/src/hooks/__tests__/service.test.ts +487 -0
  98. package/src/hooks/__tests__/types.test.ts +341 -0
  99. package/src/hooks/cli.ts +121 -0
  100. package/src/hooks/context.ts +77 -0
  101. package/src/hooks/index.ts +23 -0
  102. package/src/hooks/observation.ts +102 -0
  103. package/src/hooks/service.ts +582 -0
  104. package/src/hooks/session-init.ts +70 -0
  105. package/src/hooks/summarize.ts +89 -0
  106. package/src/hooks/types.ts +365 -0
  107. package/src/index.ts +755 -0
  108. package/src/mcp/__tests__/server.test.ts +181 -0
  109. package/src/mcp/index.ts +9 -0
  110. package/src/mcp/server.ts +441 -0
  111. package/src/mcp/tools.ts +113 -0
  112. package/src/mcp/types.ts +109 -0
  113. package/src/migration.ts +574 -0
  114. package/src/sql.js.d.ts +70 -0
  115. package/src/sqljs-backend.ts +789 -0
  116. package/src/types.ts +715 -0
package/src/index.ts ADDED
@@ -0,0 +1,755 @@
1
+ /**
2
+ * @agentkits/memory - Project-Scoped Memory System
3
+ *
4
+ * Provides persistent memory for Claude Code sessions within a project.
5
+ * Stores data in .claude/memory/memory.db using SQLite with optional
6
+ * HNSW vector indexing for semantic search.
7
+ *
8
+ * @module @agentkits/memory
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { ProjectMemoryService } from '@agentkits/memory';
13
+ *
14
+ * // Initialize memory for current project
15
+ * const memory = new ProjectMemoryService('.claude/memory');
16
+ * await memory.initialize();
17
+ *
18
+ * // Store an entry
19
+ * await memory.store({
20
+ * key: 'auth-pattern',
21
+ * content: 'Use JWT with refresh tokens for authentication',
22
+ * namespace: 'patterns',
23
+ * tags: ['auth', 'security'],
24
+ * });
25
+ *
26
+ * // Query entries
27
+ * const patterns = await memory.query({
28
+ * type: 'hybrid',
29
+ * namespace: 'patterns',
30
+ * tags: ['auth'],
31
+ * limit: 10,
32
+ * });
33
+ *
34
+ * // Semantic search (if embeddings enabled)
35
+ * const similar = await memory.semanticSearch('how to authenticate users', 5);
36
+ *
37
+ * // Session management
38
+ * await memory.startSession();
39
+ * await memory.checkpoint('Completed authentication setup');
40
+ * await memory.endSession('Successfully implemented auth');
41
+ * ```
42
+ */
43
+
44
+ import { EventEmitter } from 'node:events';
45
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
46
+ import * as path from 'node:path';
47
+ import {
48
+ IMemoryBackend,
49
+ MemoryEntry,
50
+ MemoryEntryInput,
51
+ MemoryEntryUpdate,
52
+ MemoryQuery,
53
+ SearchResult,
54
+ SearchOptions,
55
+ BackendStats,
56
+ HealthCheckResult,
57
+ EmbeddingGenerator,
58
+ SessionInfo,
59
+ MigrationResult,
60
+ createDefaultEntry,
61
+ generateSessionId,
62
+ DEFAULT_NAMESPACES,
63
+ NAMESPACE_TYPE_MAP,
64
+ } from './types.js';
65
+ import { SqlJsBackend, SqlJsBackendConfig } from './sqljs-backend.js';
66
+ import { CacheManager } from './cache-manager.js';
67
+ import { HNSWIndex } from './hnsw-index.js';
68
+ import { MemoryMigrator, migrateMarkdownMemory } from './migration.js';
69
+
70
+ // Re-export types
71
+ export * from './types.js';
72
+ export { SqlJsBackend } from './sqljs-backend.js';
73
+ export { CacheManager, TieredCacheManager } from './cache-manager.js';
74
+ export { HNSWIndex } from './hnsw-index.js';
75
+ export { MemoryMigrator, migrateMarkdownMemory } from './migration.js';
76
+
77
+ /**
78
+ * Configuration for ProjectMemoryService
79
+ */
80
+ export interface ProjectMemoryConfig {
81
+ /** Base directory for memory storage (default: .claude/memory) */
82
+ baseDir: string;
83
+
84
+ /** Database filename (default: memory.db) */
85
+ dbFilename: string;
86
+
87
+ /** Enable HNSW vector indexing */
88
+ enableVectorIndex: boolean;
89
+
90
+ /** Vector dimensions for embeddings (default: 384 for local models) */
91
+ dimensions: number;
92
+
93
+ /** Embedding generator function (optional) */
94
+ embeddingGenerator?: EmbeddingGenerator;
95
+
96
+ /** Enable caching */
97
+ cacheEnabled: boolean;
98
+
99
+ /** Cache size (number of entries) */
100
+ cacheSize: number;
101
+
102
+ /** Cache TTL in milliseconds */
103
+ cacheTtl: number;
104
+
105
+ /** Auto-persist interval in milliseconds */
106
+ autoPersistInterval: number;
107
+
108
+ /** Maximum entries before cleanup */
109
+ maxEntries: number;
110
+
111
+ /** Enable verbose logging */
112
+ verbose: boolean;
113
+ }
114
+
115
+ /**
116
+ * Default configuration
117
+ */
118
+ const DEFAULT_CONFIG: ProjectMemoryConfig = {
119
+ baseDir: '.claude/memory',
120
+ dbFilename: 'memory.db',
121
+ enableVectorIndex: false, // Disabled by default for performance
122
+ dimensions: 384, // Local model dimensions (e.g., all-MiniLM-L6-v2)
123
+ cacheEnabled: true,
124
+ cacheSize: 1000,
125
+ cacheTtl: 300000, // 5 minutes
126
+ autoPersistInterval: 10000, // 10 seconds
127
+ maxEntries: 100000,
128
+ verbose: false,
129
+ };
130
+
131
+ /**
132
+ * Project-Scoped Memory Service
133
+ *
134
+ * High-level interface for project memory that provides:
135
+ * - Persistent storage in .claude/memory/memory.db
136
+ * - Session tracking and checkpoints
137
+ * - Optional semantic search with HNSW indexing
138
+ * - Migration from existing markdown files
139
+ * - Backward-compatible markdown exports
140
+ */
141
+ export class ProjectMemoryService extends EventEmitter implements IMemoryBackend {
142
+ private config: ProjectMemoryConfig;
143
+ private backend: SqlJsBackend;
144
+ private cache: CacheManager<MemoryEntry> | null = null;
145
+ private vectorIndex: HNSWIndex | null = null;
146
+ private initialized: boolean = false;
147
+ private currentSession: SessionInfo | null = null;
148
+
149
+ constructor(baseDirOrConfig: string | Partial<ProjectMemoryConfig> = {}) {
150
+ super();
151
+
152
+ // Handle string (baseDir) or config object
153
+ const configInput = typeof baseDirOrConfig === 'string'
154
+ ? { baseDir: baseDirOrConfig }
155
+ : baseDirOrConfig;
156
+
157
+ this.config = { ...DEFAULT_CONFIG, ...configInput };
158
+
159
+ // Ensure directory exists
160
+ if (!existsSync(this.config.baseDir)) {
161
+ mkdirSync(this.config.baseDir, { recursive: true });
162
+ }
163
+
164
+ // Initialize backend
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
+ });
172
+
173
+ // Initialize cache if enabled
174
+ if (this.config.cacheEnabled) {
175
+ this.cache = new CacheManager<MemoryEntry>({
176
+ maxSize: this.config.cacheSize,
177
+ ttl: this.config.cacheTtl,
178
+ lruEnabled: true,
179
+ });
180
+ }
181
+
182
+ // Initialize vector index if enabled
183
+ if (this.config.enableVectorIndex) {
184
+ this.vectorIndex = new HNSWIndex({
185
+ dimensions: this.config.dimensions,
186
+ M: 16,
187
+ efConstruction: 200,
188
+ maxElements: this.config.maxEntries,
189
+ metric: 'cosine',
190
+ });
191
+ }
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
+ }
199
+
200
+ // ===== Lifecycle =====
201
+
202
+ async initialize(): Promise<void> {
203
+ if (this.initialized) return;
204
+
205
+ await this.backend.initialize();
206
+
207
+ // Rebuild vector index from existing embeddings
208
+ if (this.vectorIndex) {
209
+ await this.rebuildVectorIndex();
210
+ }
211
+
212
+ this.initialized = true;
213
+ this.emit('initialized', { dbPath: path.join(this.config.baseDir, this.config.dbFilename) });
214
+ }
215
+
216
+ async shutdown(): Promise<void> {
217
+ if (!this.initialized) return;
218
+
219
+ // End session if active
220
+ if (this.currentSession) {
221
+ await this.endSession('Session ended by shutdown');
222
+ }
223
+
224
+ // Shutdown components
225
+ if (this.cache) {
226
+ this.cache.shutdown();
227
+ }
228
+
229
+ await this.backend.shutdown();
230
+ this.initialized = false;
231
+ this.emit('shutdown');
232
+ }
233
+
234
+ // ===== IMemoryBackend Implementation =====
235
+
236
+ async store(entry: MemoryEntry): Promise<void> {
237
+ this.ensureInitialized();
238
+
239
+ // Generate embedding if enabled and not present
240
+ if (this.config.embeddingGenerator && !entry.embedding) {
241
+ try {
242
+ entry.embedding = await this.config.embeddingGenerator(entry.content);
243
+ } catch (error) {
244
+ if (this.config.verbose) {
245
+ console.warn(`Failed to generate embedding: ${(error as Error).message}`);
246
+ }
247
+ }
248
+ }
249
+
250
+ // Add session ID if session active
251
+ if (this.currentSession && !entry.sessionId) {
252
+ entry.sessionId = this.currentSession.id;
253
+ }
254
+
255
+ // Store in backend
256
+ await this.backend.store(entry);
257
+
258
+ // Update cache
259
+ if (this.cache) {
260
+ this.cache.set(entry.id, entry);
261
+ this.cache.set(`${entry.namespace}:${entry.key}`, entry);
262
+ }
263
+
264
+ // Add to vector index
265
+ if (this.vectorIndex && entry.embedding) {
266
+ await this.vectorIndex.addPoint(entry.id, entry.embedding);
267
+ }
268
+ }
269
+
270
+ async get(id: string): Promise<MemoryEntry | null> {
271
+ this.ensureInitialized();
272
+
273
+ // Check cache first
274
+ if (this.cache) {
275
+ const cached = this.cache.get(id);
276
+ if (cached) return cached;
277
+ }
278
+
279
+ const entry = await this.backend.get(id);
280
+
281
+ // Update cache
282
+ if (entry && this.cache) {
283
+ this.cache.set(id, entry);
284
+ }
285
+
286
+ return entry;
287
+ }
288
+
289
+ async getByKey(namespace: string, key: string): Promise<MemoryEntry | null> {
290
+ this.ensureInitialized();
291
+
292
+ const cacheKey = `${namespace}:${key}`;
293
+
294
+ // Check cache first
295
+ if (this.cache) {
296
+ const cached = this.cache.get(cacheKey);
297
+ if (cached) return cached;
298
+ }
299
+
300
+ const entry = await this.backend.getByKey(namespace, key);
301
+
302
+ // Update cache
303
+ if (entry && this.cache) {
304
+ this.cache.set(cacheKey, entry);
305
+ this.cache.set(entry.id, entry);
306
+ }
307
+
308
+ return entry;
309
+ }
310
+
311
+ async update(id: string, update: MemoryEntryUpdate): Promise<MemoryEntry | null> {
312
+ this.ensureInitialized();
313
+
314
+ const updated = await this.backend.update(id, update);
315
+
316
+ if (updated) {
317
+ // Regenerate embedding if content changed
318
+ if (update.content && this.config.embeddingGenerator) {
319
+ try {
320
+ updated.embedding = await this.config.embeddingGenerator(updated.content);
321
+ await this.backend.store(updated);
322
+
323
+ // Update vector index
324
+ if (this.vectorIndex && updated.embedding) {
325
+ await this.vectorIndex.removePoint(id);
326
+ await this.vectorIndex.addPoint(id, updated.embedding);
327
+ }
328
+ } catch {
329
+ // Ignore embedding errors
330
+ }
331
+ }
332
+
333
+ // Update cache
334
+ if (this.cache) {
335
+ this.cache.set(id, updated);
336
+ this.cache.set(`${updated.namespace}:${updated.key}`, updated);
337
+ }
338
+ }
339
+
340
+ return updated;
341
+ }
342
+
343
+ async delete(id: string): Promise<boolean> {
344
+ this.ensureInitialized();
345
+
346
+ const entry = await this.get(id);
347
+ if (!entry) return false;
348
+
349
+ const result = await this.backend.delete(id);
350
+
351
+ if (result) {
352
+ // Remove from cache
353
+ if (this.cache) {
354
+ this.cache.delete(id);
355
+ this.cache.delete(`${entry.namespace}:${entry.key}`);
356
+ }
357
+
358
+ // Remove from vector index
359
+ if (this.vectorIndex) {
360
+ await this.vectorIndex.removePoint(id);
361
+ }
362
+ }
363
+
364
+ return result;
365
+ }
366
+
367
+ async query(query: MemoryQuery): Promise<MemoryEntry[]> {
368
+ this.ensureInitialized();
369
+ return this.backend.query(query);
370
+ }
371
+
372
+ async search(embedding: Float32Array, options: SearchOptions): Promise<SearchResult[]> {
373
+ this.ensureInitialized();
374
+
375
+ if (this.vectorIndex) {
376
+ // Use HNSW index for fast search
377
+ const results = await this.vectorIndex.search(embedding, options.k);
378
+
379
+ // Fetch full entries and apply threshold
380
+ const searchResults: SearchResult[] = [];
381
+ for (const { id, distance } of results) {
382
+ const entry = await this.get(id);
383
+ if (entry) {
384
+ const score = 1 - distance; // Convert distance to similarity
385
+ if (!options.threshold || score >= options.threshold) {
386
+ searchResults.push({ entry, score, distance });
387
+ }
388
+ }
389
+ }
390
+
391
+ return searchResults;
392
+ }
393
+
394
+ // Fallback to brute-force search in backend
395
+ return this.backend.search(embedding, options);
396
+ }
397
+
398
+ async bulkInsert(entries: MemoryEntry[]): Promise<void> {
399
+ this.ensureInitialized();
400
+
401
+ for (const entry of entries) {
402
+ await this.store(entry);
403
+ }
404
+
405
+ this.emit('bulk:inserted', { count: entries.length });
406
+ }
407
+
408
+ async bulkDelete(ids: string[]): Promise<number> {
409
+ this.ensureInitialized();
410
+
411
+ let count = 0;
412
+ for (const id of ids) {
413
+ const success = await this.delete(id);
414
+ if (success) count++;
415
+ }
416
+
417
+ return count;
418
+ }
419
+
420
+ async count(namespace?: string): Promise<number> {
421
+ this.ensureInitialized();
422
+ return this.backend.count(namespace);
423
+ }
424
+
425
+ async listNamespaces(): Promise<string[]> {
426
+ this.ensureInitialized();
427
+ return this.backend.listNamespaces();
428
+ }
429
+
430
+ async clearNamespace(namespace: string): Promise<number> {
431
+ this.ensureInitialized();
432
+
433
+ // Clear from cache
434
+ if (this.cache) {
435
+ this.cache.invalidatePattern(new RegExp(`^${namespace}:`));
436
+ }
437
+
438
+ return this.backend.clearNamespace(namespace);
439
+ }
440
+
441
+ async getStats(): Promise<BackendStats> {
442
+ this.ensureInitialized();
443
+
444
+ const stats = await this.backend.getStats();
445
+
446
+ // Add HNSW stats if available
447
+ if (this.vectorIndex) {
448
+ stats.hnswStats = this.vectorIndex.getStats();
449
+ }
450
+
451
+ // Add cache stats if available
452
+ if (this.cache) {
453
+ stats.cacheStats = this.cache.getStats();
454
+ }
455
+
456
+ return stats;
457
+ }
458
+
459
+ async healthCheck(): Promise<HealthCheckResult> {
460
+ this.ensureInitialized();
461
+ return this.backend.healthCheck();
462
+ }
463
+
464
+ // ===== Convenience Methods =====
465
+
466
+ /**
467
+ * Store an entry from simple input
468
+ */
469
+ async storeEntry(input: MemoryEntryInput): Promise<MemoryEntry> {
470
+ const entry = createDefaultEntry(input);
471
+ await this.store(entry);
472
+ return entry;
473
+ }
474
+
475
+ /**
476
+ * Semantic search by content string
477
+ */
478
+ async semanticSearch(
479
+ content: string,
480
+ k: number = 10,
481
+ threshold?: number
482
+ ): Promise<SearchResult[]> {
483
+ if (!this.config.embeddingGenerator) {
484
+ throw new Error('Embedding generator not configured. Cannot perform semantic search.');
485
+ }
486
+
487
+ const embedding = await this.config.embeddingGenerator(content);
488
+ return this.search(embedding, { k, threshold });
489
+ }
490
+
491
+ /**
492
+ * Get entries by namespace (convenience method)
493
+ */
494
+ async getByNamespace(namespace: string, limit: number = 100): Promise<MemoryEntry[]> {
495
+ return this.query({
496
+ type: 'hybrid',
497
+ namespace,
498
+ limit,
499
+ });
500
+ }
501
+
502
+ /**
503
+ * Get or create an entry
504
+ */
505
+ async getOrCreate(
506
+ namespace: string,
507
+ key: string,
508
+ creator: () => MemoryEntryInput | Promise<MemoryEntryInput>
509
+ ): Promise<MemoryEntry> {
510
+ const existing = await this.getByKey(namespace, key);
511
+ if (existing) return existing;
512
+
513
+ const input = await creator();
514
+ return this.storeEntry({ ...input, namespace, key });
515
+ }
516
+
517
+ // ===== Session Management =====
518
+
519
+ /**
520
+ * Start a new session
521
+ */
522
+ async startSession(): Promise<SessionInfo> {
523
+ const session: SessionInfo = {
524
+ id: generateSessionId(),
525
+ startedAt: Date.now(),
526
+ status: 'active',
527
+ };
528
+
529
+ this.currentSession = session;
530
+
531
+ // Store session info
532
+ await this.storeEntry({
533
+ key: `session:${session.id}`,
534
+ content: JSON.stringify(session),
535
+ type: 'episodic',
536
+ namespace: DEFAULT_NAMESPACES.SESSION,
537
+ tags: ['session', 'active'],
538
+ metadata: { sessionId: session.id },
539
+ });
540
+
541
+ this.emit('session:started', session);
542
+ return session;
543
+ }
544
+
545
+ /**
546
+ * Get current session
547
+ */
548
+ getCurrentSession(): SessionInfo | null {
549
+ return this.currentSession;
550
+ }
551
+
552
+ /**
553
+ * Create a checkpoint in current session
554
+ */
555
+ async checkpoint(description: string): Promise<void> {
556
+ if (!this.currentSession) {
557
+ throw new Error('No active session. Call startSession() first.');
558
+ }
559
+
560
+ this.currentSession.lastCheckpoint = description;
561
+
562
+ await this.storeEntry({
563
+ key: `checkpoint:${this.currentSession.id}:${Date.now()}`,
564
+ content: description,
565
+ type: 'episodic',
566
+ namespace: DEFAULT_NAMESPACES.SESSION,
567
+ tags: ['checkpoint'],
568
+ metadata: {
569
+ sessionId: this.currentSession.id,
570
+ timestamp: Date.now(),
571
+ },
572
+ });
573
+
574
+ this.emit('session:checkpoint', { session: this.currentSession, description });
575
+ }
576
+
577
+ /**
578
+ * End current session
579
+ */
580
+ async endSession(summary?: string): Promise<SessionInfo | null> {
581
+ if (!this.currentSession) return null;
582
+
583
+ this.currentSession.endedAt = Date.now();
584
+ this.currentSession.summary = summary;
585
+ this.currentSession.status = 'completed';
586
+
587
+ // Update session entry
588
+ const sessionEntry = await this.getByKey(
589
+ DEFAULT_NAMESPACES.SESSION,
590
+ `session:${this.currentSession.id}`
591
+ );
592
+
593
+ if (sessionEntry) {
594
+ await this.update(sessionEntry.id, {
595
+ content: JSON.stringify(this.currentSession),
596
+ tags: ['session', 'completed'],
597
+ });
598
+ }
599
+
600
+ const endedSession = { ...this.currentSession };
601
+ this.currentSession = null;
602
+
603
+ this.emit('session:ended', endedSession);
604
+ return endedSession;
605
+ }
606
+
607
+ /**
608
+ * Get recent sessions
609
+ */
610
+ async getRecentSessions(limit: number = 10): Promise<SessionInfo[]> {
611
+ const entries = await this.query({
612
+ type: 'hybrid',
613
+ namespace: DEFAULT_NAMESPACES.SESSION,
614
+ tags: ['session'],
615
+ limit,
616
+ });
617
+
618
+ return entries
619
+ .map((e) => {
620
+ try {
621
+ return JSON.parse(e.content) as SessionInfo;
622
+ } catch {
623
+ return null;
624
+ }
625
+ })
626
+ .filter((s): s is SessionInfo => s !== null);
627
+ }
628
+
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
+ // ===== Private Methods =====
693
+
694
+ private ensureInitialized(): void {
695
+ if (!this.initialized) {
696
+ throw new Error('ProjectMemoryService not initialized. Call initialize() first.');
697
+ }
698
+ }
699
+
700
+ private async rebuildVectorIndex(): Promise<void> {
701
+ if (!this.vectorIndex) return;
702
+
703
+ // Get all entries with embeddings
704
+ const entries = await this.query({
705
+ type: 'hybrid',
706
+ limit: this.config.maxEntries,
707
+ });
708
+
709
+ const entriesWithEmbeddings = entries.filter((e) => e.embedding);
710
+
711
+ if (entriesWithEmbeddings.length > 0) {
712
+ await this.vectorIndex.rebuild(
713
+ entriesWithEmbeddings.map((e) => ({
714
+ id: e.id,
715
+ vector: e.embedding!,
716
+ }))
717
+ );
718
+
719
+ if (this.config.verbose) {
720
+ console.log(`Rebuilt vector index with ${entriesWithEmbeddings.length} entries`);
721
+ }
722
+ }
723
+ }
724
+ }
725
+
726
+ // ===== Factory Functions =====
727
+
728
+ /**
729
+ * Create a memory service for the current project
730
+ */
731
+ export function createProjectMemory(
732
+ baseDir: string = '.claude/memory',
733
+ options: Partial<ProjectMemoryConfig> = {}
734
+ ): ProjectMemoryService {
735
+ return new ProjectMemoryService({ baseDir, ...options });
736
+ }
737
+
738
+ /**
739
+ * Create a memory service with embedding support
740
+ */
741
+ export function createEmbeddingMemory(
742
+ baseDir: string,
743
+ embeddingGenerator: EmbeddingGenerator,
744
+ dimensions: number = 384
745
+ ): ProjectMemoryService {
746
+ return new ProjectMemoryService({
747
+ baseDir,
748
+ embeddingGenerator,
749
+ dimensions,
750
+ enableVectorIndex: true,
751
+ });
752
+ }
753
+
754
+ // Default export
755
+ export default ProjectMemoryService;