claude-memory-layer 1.0.6 → 1.0.8

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 (45) hide show
  1. package/.claude/settings.local.json +4 -1
  2. package/.claude-plugin/plugin.json +3 -3
  3. package/.history/package_20260201142928.json +46 -0
  4. package/.history/package_20260201192048.json +47 -0
  5. package/README.md +26 -26
  6. package/dist/.claude-plugin/plugin.json +3 -3
  7. package/dist/cli/index.js +1109 -25
  8. package/dist/cli/index.js.map +4 -4
  9. package/dist/core/index.js +192 -5
  10. package/dist/core/index.js.map +4 -4
  11. package/dist/hooks/session-end.js +262 -18
  12. package/dist/hooks/session-end.js.map +4 -4
  13. package/dist/hooks/session-start.js +262 -18
  14. package/dist/hooks/session-start.js.map +4 -4
  15. package/dist/hooks/stop.js +262 -18
  16. package/dist/hooks/stop.js.map +4 -4
  17. package/dist/hooks/user-prompt-submit.js +262 -18
  18. package/dist/hooks/user-prompt-submit.js.map +4 -4
  19. package/dist/server/api/index.js +4728 -0
  20. package/dist/server/api/index.js.map +7 -0
  21. package/dist/server/index.js +4790 -0
  22. package/dist/server/index.js.map +7 -0
  23. package/dist/services/memory-service.js +269 -18
  24. package/dist/services/memory-service.js.map +4 -4
  25. package/dist/ui/index.html +1225 -0
  26. package/package.json +4 -2
  27. package/scripts/build.ts +33 -3
  28. package/src/cli/index.ts +311 -6
  29. package/src/core/db-wrapper.ts +8 -1
  30. package/src/core/event-store.ts +52 -3
  31. package/src/core/graduation-worker.ts +171 -0
  32. package/src/core/graduation.ts +15 -2
  33. package/src/core/index.ts +1 -0
  34. package/src/core/retriever.ts +18 -0
  35. package/src/core/types.ts +1 -1
  36. package/src/mcp/index.ts +2 -2
  37. package/src/mcp/tools.ts +1 -1
  38. package/src/server/api/citations.ts +7 -3
  39. package/src/server/api/events.ts +7 -3
  40. package/src/server/api/search.ts +7 -3
  41. package/src/server/api/sessions.ts +7 -3
  42. package/src/server/api/stats.ts +175 -5
  43. package/src/server/index.ts +18 -9
  44. package/src/services/memory-service.ts +107 -19
  45. package/src/ui/index.html +1225 -0
@@ -4,16 +4,109 @@
4
4
  */
5
5
 
6
6
  import { Hono } from 'hono';
7
- import { getDefaultMemoryService } from '../../services/memory-service.js';
7
+ import { getReadOnlyMemoryService, getMemoryServiceForProject } from '../../services/memory-service.js';
8
8
 
9
9
  export const statsRouter = new Hono();
10
10
 
11
+ // GET /api/stats/shared - Get shared store statistics
12
+ statsRouter.get('/shared', async (c) => {
13
+ const memoryService = getReadOnlyMemoryService();
14
+ try {
15
+ await memoryService.initialize();
16
+ const sharedStats = await memoryService.getSharedStoreStats();
17
+ return c.json({
18
+ troubleshooting: sharedStats?.troubleshooting || 0,
19
+ bestPractices: sharedStats?.bestPractices || 0,
20
+ commonErrors: sharedStats?.commonErrors || 0,
21
+ totalUsageCount: sharedStats?.totalUsageCount || 0,
22
+ lastUpdated: sharedStats?.lastUpdated || null
23
+ });
24
+ } catch (error) {
25
+ return c.json({
26
+ troubleshooting: 0,
27
+ bestPractices: 0,
28
+ commonErrors: 0,
29
+ totalUsageCount: 0,
30
+ lastUpdated: null
31
+ });
32
+ } finally {
33
+ await memoryService.shutdown();
34
+ }
35
+ });
36
+
37
+ // GET /api/stats/endless - Get endless mode status
38
+ statsRouter.get('/endless', async (c) => {
39
+ const projectPath = c.req.query('project') || process.cwd();
40
+ const memoryService = getMemoryServiceForProject(projectPath);
41
+ try {
42
+ await memoryService.initialize();
43
+ const status = await memoryService.getEndlessModeStatus();
44
+ return c.json({
45
+ mode: status.mode,
46
+ continuityScore: status.continuityScore,
47
+ workingSetSize: status.workingSetSize,
48
+ consolidatedCount: status.consolidatedCount,
49
+ lastConsolidation: status.lastConsolidation?.toISOString() || null
50
+ });
51
+ } catch (error) {
52
+ return c.json({
53
+ mode: 'session',
54
+ continuityScore: 0,
55
+ workingSetSize: 0,
56
+ consolidatedCount: 0,
57
+ lastConsolidation: null
58
+ });
59
+ } finally {
60
+ await memoryService.shutdown();
61
+ }
62
+ });
63
+
64
+ // GET /api/stats/levels/:level - Get events by memory level
65
+ statsRouter.get('/levels/:level', async (c) => {
66
+ const { level } = c.req.param();
67
+ const limit = parseInt(c.req.query('limit') || '20', 10);
68
+ const offset = parseInt(c.req.query('offset') || '0', 10);
69
+
70
+ // Validate level
71
+ const validLevels = ['L0', 'L1', 'L2', 'L3', 'L4'];
72
+ if (!validLevels.includes(level)) {
73
+ return c.json({ error: `Invalid level. Must be one of: ${validLevels.join(', ')}` }, 400);
74
+ }
75
+
76
+ const memoryService = getReadOnlyMemoryService();
77
+ try {
78
+ await memoryService.initialize();
79
+ const events = await memoryService.getEventsByLevel(level, { limit, offset });
80
+ const stats = await memoryService.getStats();
81
+ const levelStat = stats.levelStats.find(s => s.level === level);
82
+
83
+ return c.json({
84
+ level,
85
+ events: events.map(e => ({
86
+ id: e.id,
87
+ eventType: e.eventType,
88
+ sessionId: e.sessionId,
89
+ timestamp: e.timestamp.toISOString(),
90
+ content: e.content.slice(0, 500) + (e.content.length > 500 ? '...' : ''),
91
+ metadata: e.metadata
92
+ })),
93
+ total: levelStat?.count || 0,
94
+ limit,
95
+ offset,
96
+ hasMore: events.length === limit
97
+ });
98
+ } catch (error) {
99
+ return c.json({ error: (error as Error).message }, 500);
100
+ } finally {
101
+ await memoryService.shutdown();
102
+ }
103
+ });
104
+
11
105
  // GET /api/stats - Get overall statistics
12
106
  statsRouter.get('/', async (c) => {
107
+ const memoryService = getReadOnlyMemoryService();
13
108
  try {
14
- const memoryService = getDefaultMemoryService();
15
109
  await memoryService.initialize();
16
-
17
110
  const stats = await memoryService.getStats();
18
111
  const recentEvents = await memoryService.getRecentEvents(10000);
19
112
 
@@ -58,17 +151,51 @@ statsRouter.get('/', async (c) => {
58
151
  });
59
152
  } catch (error) {
60
153
  return c.json({ error: (error as Error).message }, 500);
154
+ } finally {
155
+ await memoryService.shutdown();
156
+ }
157
+ });
158
+
159
+ // GET /api/stats/most-accessed - Get most accessed memories
160
+ statsRouter.get('/most-accessed', async (c) => {
161
+ const limit = parseInt(c.req.query('limit') || '10', 10);
162
+ const projectPath = c.req.query('project') || process.cwd();
163
+ const memoryService = getMemoryServiceForProject(projectPath);
164
+
165
+ try {
166
+ await memoryService.initialize();
167
+ const memories = await memoryService.getMostAccessedMemories(limit);
168
+
169
+ return c.json({
170
+ memories: memories.map(m => ({
171
+ memoryId: m.memoryId,
172
+ summary: m.summary,
173
+ topics: m.topics,
174
+ accessCount: m.accessCount,
175
+ lastAccessed: m.accessedAt?.toISOString() || null,
176
+ confidence: m.confidence,
177
+ createdAt: m.createdAt.toISOString()
178
+ })),
179
+ total: memories.length
180
+ });
181
+ } catch (error) {
182
+ return c.json({
183
+ memories: [],
184
+ total: 0,
185
+ error: (error as Error).message
186
+ });
187
+ } finally {
188
+ await memoryService.shutdown();
61
189
  }
62
190
  });
63
191
 
64
192
  // GET /api/stats/timeline - Get activity timeline
65
193
  statsRouter.get('/timeline', async (c) => {
66
194
  const days = parseInt(c.req.query('days') || '7', 10);
195
+ const memoryService = getReadOnlyMemoryService();
67
196
 
68
197
  try {
69
- const memoryService = getDefaultMemoryService();
70
198
  await memoryService.initialize();
71
-
72
199
  const recentEvents = await memoryService.getRecentEvents(10000);
73
200
 
74
201
  const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
@@ -93,5 +220,48 @@ statsRouter.get('/timeline', async (c) => {
93
220
  });
94
221
  } catch (error) {
95
222
  return c.json({ error: (error as Error).message }, 500);
223
+ } finally {
224
+ await memoryService.shutdown();
96
225
  }
97
226
  });
227
+
228
+ // POST /api/stats/graduation/run - Force graduation evaluation
229
+ statsRouter.post('/graduation/run', async (c) => {
230
+ const memoryService = getReadOnlyMemoryService();
231
+ try {
232
+ await memoryService.initialize();
233
+ const result = await memoryService.forceGraduation();
234
+
235
+ return c.json({
236
+ success: true,
237
+ evaluated: result.evaluated,
238
+ graduated: result.graduated,
239
+ byLevel: result.byLevel
240
+ });
241
+ } catch (error) {
242
+ return c.json({
243
+ success: false,
244
+ error: (error as Error).message
245
+ }, 500);
246
+ } finally {
247
+ await memoryService.shutdown();
248
+ }
249
+ });
250
+
251
+ // GET /api/stats/graduation - Get graduation criteria info
252
+ statsRouter.get('/graduation', async (c) => {
253
+ return c.json({
254
+ criteria: {
255
+ L0toL1: { minAccessCount: 1, minConfidence: 0.5, minCrossSessionRefs: 0, maxAgeDays: 30 },
256
+ L1toL2: { minAccessCount: 3, minConfidence: 0.7, minCrossSessionRefs: 1, maxAgeDays: 60 },
257
+ L2toL3: { minAccessCount: 5, minConfidence: 0.85, minCrossSessionRefs: 2, maxAgeDays: 90 },
258
+ L3toL4: { minAccessCount: 10, minConfidence: 0.92, minCrossSessionRefs: 3, maxAgeDays: 180 }
259
+ },
260
+ description: {
261
+ accessCount: 'Number of times the memory was retrieved/referenced',
262
+ confidence: 'Match confidence score when retrieved (0.0-1.0)',
263
+ crossSessionRefs: 'Number of different sessions that referenced this memory',
264
+ maxAgeDays: 'Maximum days since last access (prevents stale promotion)'
265
+ }
266
+ });
267
+ });
@@ -3,12 +3,18 @@
3
3
  * Provides REST API and serves static UI files
4
4
  */
5
5
 
6
+ // These are injected by the esbuild banner
7
+ declare const __dirname: string;
8
+ declare const __filename: string;
9
+
6
10
  import { Hono } from 'hono';
7
11
  import { cors } from 'hono/cors';
8
12
  import { logger } from 'hono/logger';
9
- import { serveStatic } from 'hono/bun';
13
+ import { serve } from '@hono/node-server';
14
+ import { serveStatic } from '@hono/node-server/serve-static';
10
15
  import * as path from 'path';
11
16
  import * as fs from 'fs';
17
+ import type { Server } from 'http';
12
18
 
13
19
  import { apiRouter } from './api/index.js';
14
20
 
@@ -25,7 +31,7 @@ app.route('/api', apiRouter);
25
31
  app.get('/health', (c) => c.json({ status: 'ok', timestamp: new Date().toISOString() }));
26
32
 
27
33
  // Static files (UI)
28
- const uiPath = path.join(import.meta.dir, '../../dist/ui');
34
+ const uiPath = path.join(__dirname, '../../dist/ui');
29
35
  if (fs.existsSync(uiPath)) {
30
36
  app.use('/*', serveStatic({ root: uiPath }));
31
37
  }
@@ -41,20 +47,20 @@ app.get('*', (c) => {
41
47
 
42
48
  export { app };
43
49
 
44
- let serverInstance: ReturnType<typeof Bun.serve> | null = null;
50
+ let serverInstance: Server | null = null;
45
51
 
46
52
  /**
47
53
  * Start the HTTP server
48
54
  */
49
- export function startServer(port: number = 37777): ReturnType<typeof Bun.serve> {
55
+ export function startServer(port: number = 37777): Server {
50
56
  if (serverInstance) {
51
57
  return serverInstance;
52
58
  }
53
59
 
54
- serverInstance = Bun.serve({
55
- hostname: '127.0.0.1',
60
+ serverInstance = serve({
61
+ fetch: app.fetch,
56
62
  port,
57
- fetch: app.fetch
63
+ hostname: '127.0.0.1'
58
64
  });
59
65
 
60
66
  console.log(`🧠 Code Memory viewer started at http://localhost:${port}`);
@@ -67,7 +73,7 @@ export function startServer(port: number = 37777): ReturnType<typeof Bun.serve>
67
73
  */
68
74
  export function stopServer(): void {
69
75
  if (serverInstance) {
70
- serverInstance.stop();
76
+ serverInstance.close();
71
77
  serverInstance = null;
72
78
  }
73
79
  }
@@ -85,7 +91,10 @@ export async function isServerRunning(port: number = 37777): Promise<boolean> {
85
91
  }
86
92
 
87
93
  // Start server if run directly
88
- if (import.meta.main) {
94
+ // Check if this file is being run directly (not imported)
95
+ const isMainModule = process.argv[1]?.includes('server/index') ||
96
+ process.argv[1]?.endsWith('server.js');
97
+ if (isMainModule) {
89
98
  const port = parseInt(process.env.PORT || '37777', 10);
90
99
  startServer(port);
91
100
  }
@@ -42,10 +42,12 @@ import { WorkingSetStore, createWorkingSetStore } from '../core/working-set-stor
42
42
  import { ConsolidatedStore, createConsolidatedStore } from '../core/consolidated-store.js';
43
43
  import { ConsolidationWorker, createConsolidationWorker } from '../core/consolidation-worker.js';
44
44
  import { ContinuityManager, createContinuityManager } from '../core/continuity-manager.js';
45
+ import { GraduationWorker, createGraduationWorker, GraduationRunResult } from '../core/graduation-worker.js';
45
46
 
46
47
  export interface MemoryServiceConfig {
47
48
  storagePath: string;
48
49
  embeddingModel?: string;
50
+ readOnly?: boolean;
49
51
  }
50
52
 
51
53
  // ============================================================
@@ -170,6 +172,7 @@ export class MemoryService {
170
172
  private readonly retriever: Retriever;
171
173
  private readonly graduation: GraduationPipeline;
172
174
  private vectorWorker: VectorWorker | null = null;
175
+ private graduationWorker: GraduationWorker | null = null;
173
176
  private initialized = false;
174
177
 
175
178
  // Endless Mode components
@@ -187,11 +190,14 @@ export class MemoryService {
187
190
  private sharedStoreConfig: SharedStoreConfig | null = null;
188
191
  private projectHash: string | null = null;
189
192
 
193
+ private readonly readOnly: boolean;
194
+
190
195
  constructor(config: MemoryServiceConfig & { projectHash?: string; sharedStoreConfig?: SharedStoreConfig }) {
191
196
  const storagePath = this.expandPath(config.storagePath);
197
+ this.readOnly = config.readOnly ?? false;
192
198
 
193
- // Ensure storage directory exists
194
- if (!fs.existsSync(storagePath)) {
199
+ // Ensure storage directory exists (only if not read-only)
200
+ if (!this.readOnly && !fs.existsSync(storagePath)) {
195
201
  fs.mkdirSync(storagePath, { recursive: true });
196
202
  }
197
203
 
@@ -201,7 +207,7 @@ export class MemoryService {
201
207
  this.sharedStoreConfig = config.sharedStoreConfig ?? { enabled: true };
202
208
 
203
209
  // Initialize components
204
- this.eventStore = new EventStore(path.join(storagePath, 'events.duckdb'));
210
+ this.eventStore = new EventStore(path.join(storagePath, 'events.duckdb'), { readOnly: this.readOnly });
205
211
  this.vectorStore = new VectorStore(path.join(storagePath, 'vectors'));
206
212
  this.embedder = config.embeddingModel
207
213
  ? new Embedder(config.embeddingModel)
@@ -226,24 +232,37 @@ export class MemoryService {
226
232
  await this.vectorStore.initialize();
227
233
  await this.embedder.initialize();
228
234
 
229
- // Start vector worker
230
- this.vectorWorker = createVectorWorker(
231
- this.eventStore,
232
- this.vectorStore,
233
- this.embedder
234
- );
235
- this.vectorWorker.start();
235
+ // Skip write-related workers in read-only mode
236
+ if (!this.readOnly) {
237
+ // Start vector worker
238
+ this.vectorWorker = createVectorWorker(
239
+ this.eventStore,
240
+ this.vectorStore,
241
+ this.embedder
242
+ );
243
+ this.vectorWorker.start();
236
244
 
237
- // Load endless mode setting
238
- const savedMode = await this.eventStore.getEndlessConfig('mode') as MemoryMode | null;
239
- if (savedMode === 'endless') {
240
- this.endlessMode = 'endless';
241
- await this.initializeEndlessMode();
242
- }
245
+ // Connect graduation pipeline to retriever for access tracking
246
+ this.retriever.setGraduationPipeline(this.graduation);
247
+
248
+ // Start graduation worker for automatic level promotion
249
+ this.graduationWorker = createGraduationWorker(
250
+ this.eventStore,
251
+ this.graduation
252
+ );
253
+ this.graduationWorker.start();
243
254
 
244
- // Initialize shared store (enabled by default)
245
- if (this.sharedStoreConfig?.enabled !== false) {
246
- await this.initializeSharedStore();
255
+ // Load endless mode setting
256
+ const savedMode = await this.eventStore.getEndlessConfig('mode') as MemoryMode | null;
257
+ if (savedMode === 'endless') {
258
+ this.endlessMode = 'endless';
259
+ await this.initializeEndlessMode();
260
+ }
261
+
262
+ // Initialize shared store (enabled by default)
263
+ if (this.sharedStoreConfig?.enabled !== false) {
264
+ await this.initializeSharedStore();
265
+ }
247
266
  }
248
267
 
249
268
  this.initialized = true;
@@ -499,6 +518,22 @@ export class MemoryService {
499
518
  return 0;
500
519
  }
501
520
 
521
+ /**
522
+ * Get events by memory level
523
+ */
524
+ async getEventsByLevel(level: string, options?: { limit?: number; offset?: number }): Promise<MemoryEvent[]> {
525
+ await this.initialize();
526
+ return this.eventStore.getEventsByLevel(level, options);
527
+ }
528
+
529
+ /**
530
+ * Get memory level for a specific event
531
+ */
532
+ async getEventLevel(eventId: string): Promise<string | null> {
533
+ await this.initialize();
534
+ return this.eventStore.getEventLevel(eventId);
535
+ }
536
+
502
537
  /**
503
538
  * Format retrieval results as context for Claude
504
539
  */
@@ -713,6 +748,22 @@ export class MemoryService {
713
748
  return this.consolidatedStore.getAll({ limit });
714
749
  }
715
750
 
751
+ /**
752
+ * Get most accessed consolidated memories
753
+ */
754
+ async getMostAccessedMemories(limit: number = 10): Promise<ConsolidatedMemory[]> {
755
+ if (!this.consolidatedStore) return [];
756
+ return this.consolidatedStore.getMostAccessed(limit);
757
+ }
758
+
759
+ /**
760
+ * Mark a consolidated memory as accessed
761
+ */
762
+ async markMemoryAccessed(memoryId: string): Promise<void> {
763
+ if (!this.consolidatedStore) return;
764
+ await this.consolidatedStore.markAccessed(memoryId);
765
+ }
766
+
716
767
  /**
717
768
  * Calculate continuity score for current context
718
769
  */
@@ -822,10 +873,32 @@ export class MemoryService {
822
873
  return parts.join('\n');
823
874
  }
824
875
 
876
+ /**
877
+ * Force a graduation evaluation run
878
+ */
879
+ async forceGraduation(): Promise<GraduationRunResult> {
880
+ if (!this.graduationWorker) {
881
+ return { evaluated: 0, graduated: 0, byLevel: {} };
882
+ }
883
+ return this.graduationWorker.forceRun();
884
+ }
885
+
886
+ /**
887
+ * Record access to a memory event (for graduation scoring)
888
+ */
889
+ recordMemoryAccess(eventId: string, sessionId: string, confidence: number = 1.0): void {
890
+ this.graduation.recordAccess(eventId, sessionId, confidence);
891
+ }
892
+
825
893
  /**
826
894
  * Shutdown service
827
895
  */
828
896
  async shutdown(): Promise<void> {
897
+ // Stop graduation worker
898
+ if (this.graduationWorker) {
899
+ this.graduationWorker.stop();
900
+ }
901
+
829
902
  // Stop endless mode components
830
903
  if (this.consolidationWorker) {
831
904
  this.consolidationWorker.stop();
@@ -861,6 +934,7 @@ export class MemoryService {
861
934
  // Instance cache: Map from project hash (or '__global__') to MemoryService
862
935
  const serviceCache = new Map<string, MemoryService>();
863
936
  const GLOBAL_KEY = '__global__';
937
+ const GLOBAL_READONLY_KEY = '__global_readonly__';
864
938
 
865
939
  /**
866
940
  * Get the global memory service (backward compatibility)
@@ -875,6 +949,20 @@ export function getDefaultMemoryService(): MemoryService {
875
949
  return serviceCache.get(GLOBAL_KEY)!;
876
950
  }
877
951
 
952
+ /**
953
+ * Get a read-only global memory service
954
+ * Use this for web server/dashboard that only needs to read data
955
+ * Creates a fresh connection each time to avoid blocking the main writer process
956
+ */
957
+ export function getReadOnlyMemoryService(): MemoryService {
958
+ // Don't cache - create fresh instance each time to avoid holding locks
959
+ // The connection will be closed when the request completes
960
+ return new MemoryService({
961
+ storagePath: '~/.claude-code/memory',
962
+ readOnly: true
963
+ });
964
+ }
965
+
878
966
  /**
879
967
  * Get memory service for a specific project path
880
968
  * Creates isolated storage at ~/.claude-code/memory/projects/{hash}/