claude-memory-layer 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 (127) hide show
  1. package/.claude-plugin/commands/memory-forget.md +42 -0
  2. package/.claude-plugin/commands/memory-history.md +34 -0
  3. package/.claude-plugin/commands/memory-import.md +56 -0
  4. package/.claude-plugin/commands/memory-list.md +37 -0
  5. package/.claude-plugin/commands/memory-search.md +36 -0
  6. package/.claude-plugin/commands/memory-stats.md +34 -0
  7. package/.claude-plugin/hooks.json +59 -0
  8. package/.claude-plugin/plugin.json +24 -0
  9. package/.history/package_20260201112328.json +45 -0
  10. package/.history/package_20260201113602.json +45 -0
  11. package/.history/package_20260201113713.json +45 -0
  12. package/.history/package_20260201114110.json +45 -0
  13. package/Memo.txt +558 -0
  14. package/README.md +520 -0
  15. package/context.md +636 -0
  16. package/dist/.claude-plugin/commands/memory-forget.md +42 -0
  17. package/dist/.claude-plugin/commands/memory-history.md +34 -0
  18. package/dist/.claude-plugin/commands/memory-import.md +56 -0
  19. package/dist/.claude-plugin/commands/memory-list.md +37 -0
  20. package/dist/.claude-plugin/commands/memory-search.md +36 -0
  21. package/dist/.claude-plugin/commands/memory-stats.md +34 -0
  22. package/dist/.claude-plugin/hooks.json +59 -0
  23. package/dist/.claude-plugin/plugin.json +24 -0
  24. package/dist/cli/index.js +3539 -0
  25. package/dist/cli/index.js.map +7 -0
  26. package/dist/core/index.js +4408 -0
  27. package/dist/core/index.js.map +7 -0
  28. package/dist/hooks/session-end.js +2971 -0
  29. package/dist/hooks/session-end.js.map +7 -0
  30. package/dist/hooks/session-start.js +2969 -0
  31. package/dist/hooks/session-start.js.map +7 -0
  32. package/dist/hooks/stop.js +3123 -0
  33. package/dist/hooks/stop.js.map +7 -0
  34. package/dist/hooks/user-prompt-submit.js +2960 -0
  35. package/dist/hooks/user-prompt-submit.js.map +7 -0
  36. package/dist/services/memory-service.js +2931 -0
  37. package/dist/services/memory-service.js.map +7 -0
  38. package/package.json +45 -0
  39. package/plan.md +1642 -0
  40. package/scripts/build.ts +102 -0
  41. package/spec.md +624 -0
  42. package/specs/citations-system/context.md +243 -0
  43. package/specs/citations-system/plan.md +495 -0
  44. package/specs/citations-system/spec.md +371 -0
  45. package/specs/endless-mode/context.md +305 -0
  46. package/specs/endless-mode/plan.md +620 -0
  47. package/specs/endless-mode/spec.md +455 -0
  48. package/specs/entity-edge-model/context.md +401 -0
  49. package/specs/entity-edge-model/plan.md +459 -0
  50. package/specs/entity-edge-model/spec.md +391 -0
  51. package/specs/evidence-aligner-v2/context.md +401 -0
  52. package/specs/evidence-aligner-v2/plan.md +303 -0
  53. package/specs/evidence-aligner-v2/spec.md +312 -0
  54. package/specs/mcp-desktop-integration/context.md +278 -0
  55. package/specs/mcp-desktop-integration/plan.md +550 -0
  56. package/specs/mcp-desktop-integration/spec.md +494 -0
  57. package/specs/post-tool-use-hook/context.md +319 -0
  58. package/specs/post-tool-use-hook/plan.md +469 -0
  59. package/specs/post-tool-use-hook/spec.md +364 -0
  60. package/specs/private-tags/context.md +288 -0
  61. package/specs/private-tags/plan.md +412 -0
  62. package/specs/private-tags/spec.md +345 -0
  63. package/specs/progressive-disclosure/context.md +346 -0
  64. package/specs/progressive-disclosure/plan.md +663 -0
  65. package/specs/progressive-disclosure/spec.md +415 -0
  66. package/specs/task-entity-system/context.md +297 -0
  67. package/specs/task-entity-system/plan.md +301 -0
  68. package/specs/task-entity-system/spec.md +314 -0
  69. package/specs/vector-outbox-v2/context.md +470 -0
  70. package/specs/vector-outbox-v2/plan.md +562 -0
  71. package/specs/vector-outbox-v2/spec.md +466 -0
  72. package/specs/web-viewer-ui/context.md +384 -0
  73. package/specs/web-viewer-ui/plan.md +797 -0
  74. package/specs/web-viewer-ui/spec.md +516 -0
  75. package/src/cli/index.ts +570 -0
  76. package/src/core/canonical-key.ts +186 -0
  77. package/src/core/citation-generator.ts +63 -0
  78. package/src/core/consolidated-store.ts +279 -0
  79. package/src/core/consolidation-worker.ts +384 -0
  80. package/src/core/context-formatter.ts +276 -0
  81. package/src/core/continuity-manager.ts +336 -0
  82. package/src/core/edge-repo.ts +324 -0
  83. package/src/core/embedder.ts +124 -0
  84. package/src/core/entity-repo.ts +342 -0
  85. package/src/core/event-store.ts +672 -0
  86. package/src/core/evidence-aligner.ts +635 -0
  87. package/src/core/graduation.ts +365 -0
  88. package/src/core/index.ts +32 -0
  89. package/src/core/matcher.ts +210 -0
  90. package/src/core/metadata-extractor.ts +203 -0
  91. package/src/core/privacy/filter.ts +179 -0
  92. package/src/core/privacy/index.ts +20 -0
  93. package/src/core/privacy/tag-parser.ts +145 -0
  94. package/src/core/progressive-retriever.ts +415 -0
  95. package/src/core/retriever.ts +235 -0
  96. package/src/core/task/blocker-resolver.ts +325 -0
  97. package/src/core/task/index.ts +9 -0
  98. package/src/core/task/task-matcher.ts +238 -0
  99. package/src/core/task/task-projector.ts +345 -0
  100. package/src/core/task/task-resolver.ts +414 -0
  101. package/src/core/types.ts +841 -0
  102. package/src/core/vector-outbox.ts +295 -0
  103. package/src/core/vector-store.ts +182 -0
  104. package/src/core/vector-worker.ts +488 -0
  105. package/src/core/working-set-store.ts +244 -0
  106. package/src/hooks/post-tool-use.ts +127 -0
  107. package/src/hooks/session-end.ts +78 -0
  108. package/src/hooks/session-start.ts +57 -0
  109. package/src/hooks/stop.ts +78 -0
  110. package/src/hooks/user-prompt-submit.ts +54 -0
  111. package/src/mcp/handlers.ts +212 -0
  112. package/src/mcp/index.ts +47 -0
  113. package/src/mcp/tools.ts +78 -0
  114. package/src/server/api/citations.ts +101 -0
  115. package/src/server/api/events.ts +101 -0
  116. package/src/server/api/index.ts +18 -0
  117. package/src/server/api/search.ts +98 -0
  118. package/src/server/api/sessions.ts +111 -0
  119. package/src/server/api/stats.ts +97 -0
  120. package/src/server/index.ts +91 -0
  121. package/src/services/memory-service.ts +626 -0
  122. package/src/services/session-history-importer.ts +367 -0
  123. package/tests/canonical-key.test.ts +101 -0
  124. package/tests/evidence-aligner.test.ts +152 -0
  125. package/tests/matcher.test.ts +112 -0
  126. package/tsconfig.json +24 -0
  127. package/vitest.config.ts +15 -0
@@ -0,0 +1,78 @@
1
+ /**
2
+ * MCP Tool Definitions
3
+ * Available tools for Claude Desktop
4
+ */
5
+
6
+ import type { Tool } from '@modelcontextprotocol/sdk/types.js';
7
+
8
+ export const tools: Tool[] = [
9
+ {
10
+ name: 'mem-search',
11
+ description: 'Search code-memory for relevant past conversations and insights. Returns a compact index of results - use mem-details to get full content.',
12
+ inputSchema: {
13
+ type: 'object',
14
+ properties: {
15
+ query: {
16
+ type: 'string',
17
+ description: 'Natural language search query'
18
+ },
19
+ topK: {
20
+ type: 'number',
21
+ description: 'Maximum number of results (default: 5, max: 20)'
22
+ },
23
+ sessionId: {
24
+ type: 'string',
25
+ description: 'Optional: filter by specific session ID'
26
+ },
27
+ eventType: {
28
+ type: 'string',
29
+ enum: ['user_prompt', 'agent_response', 'tool_observation', 'session_summary'],
30
+ description: 'Optional: filter by event type'
31
+ }
32
+ },
33
+ required: ['query']
34
+ }
35
+ },
36
+ {
37
+ name: 'mem-timeline',
38
+ description: 'Get chronological context around specific memories. Useful for understanding the conversation flow.',
39
+ inputSchema: {
40
+ type: 'object',
41
+ properties: {
42
+ ids: {
43
+ type: 'array',
44
+ items: { type: 'string' },
45
+ description: 'Memory IDs (from mem-search) to get timeline for'
46
+ },
47
+ windowSize: {
48
+ type: 'number',
49
+ description: 'Number of items before/after each ID (default: 3)'
50
+ }
51
+ },
52
+ required: ['ids']
53
+ }
54
+ },
55
+ {
56
+ name: 'mem-details',
57
+ description: 'Get full content of specific memories. Use after mem-search to get complete information.',
58
+ inputSchema: {
59
+ type: 'object',
60
+ properties: {
61
+ ids: {
62
+ type: 'array',
63
+ items: { type: 'string' },
64
+ description: 'Memory IDs to fetch full details for'
65
+ }
66
+ },
67
+ required: ['ids']
68
+ }
69
+ },
70
+ {
71
+ name: 'mem-stats',
72
+ description: 'Get statistics about the memory storage (total events, sessions, etc.)',
73
+ inputSchema: {
74
+ type: 'object',
75
+ properties: {}
76
+ }
77
+ }
78
+ ];
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Citations API
3
+ * Endpoints for citation management
4
+ */
5
+
6
+ import { Hono } from 'hono';
7
+ import { getDefaultMemoryService } from '../../services/memory-service.js';
8
+ import { generateCitationId, parseCitationId } from '../../core/citation-generator.js';
9
+
10
+ export const citationsRouter = new Hono();
11
+
12
+ // GET /api/citations/:id - Get citation by ID
13
+ citationsRouter.get('/:id', async (c) => {
14
+ const { id } = c.req.param();
15
+
16
+ // Support both formats: "a7Bc3x" or "mem:a7Bc3x"
17
+ const citationId = parseCitationId(id) || id;
18
+
19
+ try {
20
+ const memoryService = getDefaultMemoryService();
21
+ await memoryService.initialize();
22
+
23
+ // Search through recent events to find the one matching this citation ID
24
+ const recentEvents = await memoryService.getRecentEvents(10000);
25
+
26
+ const event = recentEvents.find(e => {
27
+ const eventCitationId = generateCitationId(e.id);
28
+ return eventCitationId === citationId;
29
+ });
30
+
31
+ if (!event) {
32
+ return c.json({ error: 'Citation not found' }, 404);
33
+ }
34
+
35
+ return c.json({
36
+ citation: {
37
+ id: citationId,
38
+ eventId: event.id
39
+ },
40
+ event: {
41
+ id: event.id,
42
+ eventType: event.eventType,
43
+ timestamp: event.timestamp,
44
+ sessionId: event.sessionId,
45
+ content: event.content,
46
+ metadata: event.metadata
47
+ }
48
+ });
49
+ } catch (error) {
50
+ return c.json({ error: (error as Error).message }, 500);
51
+ }
52
+ });
53
+
54
+ // GET /api/citations/:id/related - Get related citations
55
+ citationsRouter.get('/:id/related', async (c) => {
56
+ const { id } = c.req.param();
57
+ const citationId = parseCitationId(id) || id;
58
+
59
+ try {
60
+ const memoryService = getDefaultMemoryService();
61
+ await memoryService.initialize();
62
+
63
+ const recentEvents = await memoryService.getRecentEvents(10000);
64
+
65
+ // Find the main event
66
+ const event = recentEvents.find(e => {
67
+ const eventCitationId = generateCitationId(e.id);
68
+ return eventCitationId === citationId;
69
+ });
70
+
71
+ if (!event) {
72
+ return c.json({ error: 'Citation not found' }, 404);
73
+ }
74
+
75
+ // Get surrounding events from same session
76
+ const sessionEvents = recentEvents
77
+ .filter(e => e.sessionId === event.sessionId)
78
+ .sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
79
+
80
+ const eventIndex = sessionEvents.findIndex(e => e.id === event.id);
81
+ const prev = eventIndex > 0 ? sessionEvents[eventIndex - 1] : null;
82
+ const next = eventIndex < sessionEvents.length - 1 ? sessionEvents[eventIndex + 1] : null;
83
+
84
+ return c.json({
85
+ previous: prev ? {
86
+ citationId: generateCitationId(prev.id),
87
+ eventType: prev.eventType,
88
+ timestamp: prev.timestamp,
89
+ preview: prev.content.slice(0, 100) + (prev.content.length > 100 ? '...' : '')
90
+ } : null,
91
+ next: next ? {
92
+ citationId: generateCitationId(next.id),
93
+ eventType: next.eventType,
94
+ timestamp: next.timestamp,
95
+ preview: next.content.slice(0, 100) + (next.content.length > 100 ? '...' : '')
96
+ } : null
97
+ });
98
+ } catch (error) {
99
+ return c.json({ error: (error as Error).message }, 500);
100
+ }
101
+ });
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Events API
3
+ * Endpoints for event management
4
+ */
5
+
6
+ import { Hono } from 'hono';
7
+ import { getDefaultMemoryService } from '../../services/memory-service.js';
8
+
9
+ export const eventsRouter = new Hono();
10
+
11
+ // GET /api/events - List events with filters
12
+ eventsRouter.get('/', async (c) => {
13
+ const sessionId = c.req.query('sessionId');
14
+ const eventType = c.req.query('type');
15
+ const limit = parseInt(c.req.query('limit') || '100', 10);
16
+ const offset = parseInt(c.req.query('offset') || '0', 10);
17
+
18
+ try {
19
+ const memoryService = getDefaultMemoryService();
20
+ await memoryService.initialize();
21
+
22
+ let events = await memoryService.getRecentEvents(limit + offset + 1000);
23
+
24
+ // Filter by session
25
+ if (sessionId) {
26
+ events = events.filter(e => e.sessionId === sessionId);
27
+ }
28
+
29
+ // Filter by type
30
+ if (eventType) {
31
+ events = events.filter(e => e.eventType === eventType);
32
+ }
33
+
34
+ // Pagination
35
+ const total = events.length;
36
+ events = events.slice(offset, offset + limit);
37
+
38
+ return c.json({
39
+ events: events.map(e => ({
40
+ id: e.id,
41
+ eventType: e.eventType,
42
+ timestamp: e.timestamp,
43
+ sessionId: e.sessionId,
44
+ preview: e.content.slice(0, 200) + (e.content.length > 200 ? '...' : ''),
45
+ contentLength: e.content.length
46
+ })),
47
+ total,
48
+ limit,
49
+ offset,
50
+ hasMore: offset + limit < total
51
+ });
52
+ } catch (error) {
53
+ return c.json({ error: (error as Error).message }, 500);
54
+ }
55
+ });
56
+
57
+ // GET /api/events/:id - Get event details
58
+ eventsRouter.get('/:id', async (c) => {
59
+ const { id } = c.req.param();
60
+
61
+ try {
62
+ const memoryService = getDefaultMemoryService();
63
+ await memoryService.initialize();
64
+
65
+ const recentEvents = await memoryService.getRecentEvents(10000);
66
+ const event = recentEvents.find(e => e.id === id);
67
+
68
+ if (!event) {
69
+ return c.json({ error: 'Event not found' }, 404);
70
+ }
71
+
72
+ // Get surrounding events for context
73
+ const sessionEvents = recentEvents
74
+ .filter(e => e.sessionId === event.sessionId)
75
+ .sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
76
+
77
+ const eventIndex = sessionEvents.findIndex(e => e.id === id);
78
+ const start = Math.max(0, eventIndex - 2);
79
+ const end = Math.min(sessionEvents.length, eventIndex + 3);
80
+ const context = sessionEvents.slice(start, end).filter(e => e.id !== id);
81
+
82
+ return c.json({
83
+ event: {
84
+ id: event.id,
85
+ eventType: event.eventType,
86
+ timestamp: event.timestamp,
87
+ sessionId: event.sessionId,
88
+ content: event.content,
89
+ metadata: event.metadata
90
+ },
91
+ context: context.map(e => ({
92
+ id: e.id,
93
+ eventType: e.eventType,
94
+ timestamp: e.timestamp,
95
+ preview: e.content.slice(0, 100) + (e.content.length > 100 ? '...' : '')
96
+ }))
97
+ });
98
+ } catch (error) {
99
+ return c.json({ error: (error as Error).message }, 500);
100
+ }
101
+ });
@@ -0,0 +1,18 @@
1
+ /**
2
+ * API Router
3
+ * Central router for all API endpoints
4
+ */
5
+
6
+ import { Hono } from 'hono';
7
+ import { sessionsRouter } from './sessions.js';
8
+ import { eventsRouter } from './events.js';
9
+ import { searchRouter } from './search.js';
10
+ import { statsRouter } from './stats.js';
11
+ import { citationsRouter } from './citations.js';
12
+
13
+ export const apiRouter = new Hono()
14
+ .route('/sessions', sessionsRouter)
15
+ .route('/events', eventsRouter)
16
+ .route('/search', searchRouter)
17
+ .route('/stats', statsRouter)
18
+ .route('/citations', citationsRouter);
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Search API
3
+ * Endpoints for memory search
4
+ */
5
+
6
+ import { Hono } from 'hono';
7
+ import { getDefaultMemoryService } from '../../services/memory-service.js';
8
+
9
+ export const searchRouter = new Hono();
10
+
11
+ interface SearchRequest {
12
+ query: string;
13
+ options?: {
14
+ topK?: number;
15
+ minScore?: number;
16
+ sessionId?: string;
17
+ eventType?: string;
18
+ };
19
+ }
20
+
21
+ // POST /api/search - Search memories
22
+ searchRouter.post('/', async (c) => {
23
+ try {
24
+ const body = await c.req.json<SearchRequest>();
25
+
26
+ if (!body.query) {
27
+ return c.json({ error: 'Query is required' }, 400);
28
+ }
29
+
30
+ const memoryService = getDefaultMemoryService();
31
+ await memoryService.initialize();
32
+
33
+ const startTime = Date.now();
34
+
35
+ const result = await memoryService.retrieveMemories(body.query, {
36
+ topK: body.options?.topK ?? 10,
37
+ minScore: body.options?.minScore ?? 0.7,
38
+ sessionId: body.options?.sessionId
39
+ });
40
+
41
+ const searchTime = Date.now() - startTime;
42
+
43
+ return c.json({
44
+ results: result.memories.map(m => ({
45
+ id: m.event.id,
46
+ eventType: m.event.eventType,
47
+ timestamp: m.event.timestamp,
48
+ sessionId: m.event.sessionId,
49
+ score: m.score,
50
+ content: m.event.content,
51
+ preview: m.event.content.slice(0, 200) + (m.event.content.length > 200 ? '...' : ''),
52
+ context: m.sessionContext
53
+ })),
54
+ meta: {
55
+ totalMatches: result.memories.length,
56
+ searchTime,
57
+ confidence: result.matchResult.confidence,
58
+ totalTokens: result.totalTokens
59
+ }
60
+ });
61
+ } catch (error) {
62
+ return c.json({ error: (error as Error).message }, 500);
63
+ }
64
+ });
65
+
66
+ // GET /api/search - Simple search via query param
67
+ searchRouter.get('/', async (c) => {
68
+ const query = c.req.query('q');
69
+
70
+ if (!query) {
71
+ return c.json({ error: 'Query parameter "q" is required' }, 400);
72
+ }
73
+
74
+ const topK = parseInt(c.req.query('topK') || '5', 10);
75
+
76
+ try {
77
+ const memoryService = getDefaultMemoryService();
78
+ await memoryService.initialize();
79
+
80
+ const result = await memoryService.retrieveMemories(query, { topK });
81
+
82
+ return c.json({
83
+ results: result.memories.map(m => ({
84
+ id: m.event.id,
85
+ eventType: m.event.eventType,
86
+ timestamp: m.event.timestamp,
87
+ score: m.score,
88
+ preview: m.event.content.slice(0, 200) + (m.event.content.length > 200 ? '...' : '')
89
+ })),
90
+ meta: {
91
+ totalMatches: result.memories.length,
92
+ confidence: result.matchResult.confidence
93
+ }
94
+ });
95
+ } catch (error) {
96
+ return c.json({ error: (error as Error).message }, 500);
97
+ }
98
+ });
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Sessions API
3
+ * Endpoints for session management
4
+ */
5
+
6
+ import { Hono } from 'hono';
7
+ import { getDefaultMemoryService } from '../../services/memory-service.js';
8
+
9
+ export const sessionsRouter = new Hono();
10
+
11
+ // GET /api/sessions - List all sessions
12
+ sessionsRouter.get('/', async (c) => {
13
+ const page = parseInt(c.req.query('page') || '1', 10);
14
+ const pageSize = parseInt(c.req.query('pageSize') || '20', 10);
15
+
16
+ try {
17
+ const memoryService = getDefaultMemoryService();
18
+ await memoryService.initialize();
19
+
20
+ // Get recent events and extract sessions
21
+ const recentEvents = await memoryService.getRecentEvents(1000);
22
+
23
+ // Group by session
24
+ const sessionMap = new Map<string, {
25
+ id: string;
26
+ startedAt: Date;
27
+ eventCount: number;
28
+ lastEventAt: Date;
29
+ }>();
30
+
31
+ for (const event of recentEvents) {
32
+ const existing = sessionMap.get(event.sessionId);
33
+ if (!existing) {
34
+ sessionMap.set(event.sessionId, {
35
+ id: event.sessionId,
36
+ startedAt: event.timestamp,
37
+ eventCount: 1,
38
+ lastEventAt: event.timestamp
39
+ });
40
+ } else {
41
+ existing.eventCount++;
42
+ if (event.timestamp < existing.startedAt) {
43
+ existing.startedAt = event.timestamp;
44
+ }
45
+ if (event.timestamp > existing.lastEventAt) {
46
+ existing.lastEventAt = event.timestamp;
47
+ }
48
+ }
49
+ }
50
+
51
+ const sessions = Array.from(sessionMap.values())
52
+ .sort((a, b) => b.lastEventAt.getTime() - a.lastEventAt.getTime());
53
+
54
+ const total = sessions.length;
55
+ const start = (page - 1) * pageSize;
56
+ const end = start + pageSize;
57
+ const paginatedSessions = sessions.slice(start, end);
58
+
59
+ return c.json({
60
+ sessions: paginatedSessions,
61
+ total,
62
+ page,
63
+ pageSize,
64
+ hasMore: end < total
65
+ });
66
+ } catch (error) {
67
+ return c.json({ error: (error as Error).message }, 500);
68
+ }
69
+ });
70
+
71
+ // GET /api/sessions/:id - Get session details
72
+ sessionsRouter.get('/:id', async (c) => {
73
+ const { id } = c.req.param();
74
+
75
+ try {
76
+ const memoryService = getDefaultMemoryService();
77
+ await memoryService.initialize();
78
+
79
+ const events = await memoryService.getSessionHistory(id);
80
+
81
+ if (events.length === 0) {
82
+ return c.json({ error: 'Session not found' }, 404);
83
+ }
84
+
85
+ const session = {
86
+ id,
87
+ startedAt: events[0].timestamp,
88
+ endedAt: events[events.length - 1].timestamp,
89
+ eventCount: events.length
90
+ };
91
+
92
+ const eventsByType = {
93
+ user_prompt: events.filter(e => e.eventType === 'user_prompt').length,
94
+ agent_response: events.filter(e => e.eventType === 'agent_response').length,
95
+ tool_observation: events.filter(e => e.eventType === 'tool_observation').length
96
+ };
97
+
98
+ return c.json({
99
+ session,
100
+ events: events.slice(0, 100).map(e => ({
101
+ id: e.id,
102
+ eventType: e.eventType,
103
+ timestamp: e.timestamp,
104
+ preview: e.content.slice(0, 200) + (e.content.length > 200 ? '...' : '')
105
+ })),
106
+ stats: eventsByType
107
+ });
108
+ } catch (error) {
109
+ return c.json({ error: (error as Error).message }, 500);
110
+ }
111
+ });
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Stats API
3
+ * Endpoints for storage statistics
4
+ */
5
+
6
+ import { Hono } from 'hono';
7
+ import { getDefaultMemoryService } from '../../services/memory-service.js';
8
+
9
+ export const statsRouter = new Hono();
10
+
11
+ // GET /api/stats - Get overall statistics
12
+ statsRouter.get('/', async (c) => {
13
+ try {
14
+ const memoryService = getDefaultMemoryService();
15
+ await memoryService.initialize();
16
+
17
+ const stats = await memoryService.getStats();
18
+ const recentEvents = await memoryService.getRecentEvents(10000);
19
+
20
+ // Calculate event types
21
+ const eventsByType = recentEvents.reduce((acc, e) => {
22
+ acc[e.eventType] = (acc[e.eventType] || 0) + 1;
23
+ return acc;
24
+ }, {} as Record<string, number>);
25
+
26
+ // Calculate unique sessions
27
+ const uniqueSessions = new Set(recentEvents.map(e => e.sessionId));
28
+
29
+ // Calculate events by day (last 7 days)
30
+ const now = new Date();
31
+ const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
32
+ const eventsByDay = recentEvents
33
+ .filter(e => e.timestamp >= sevenDaysAgo)
34
+ .reduce((acc, e) => {
35
+ const day = e.timestamp.toISOString().split('T')[0];
36
+ acc[day] = (acc[day] || 0) + 1;
37
+ return acc;
38
+ }, {} as Record<string, number>);
39
+
40
+ return c.json({
41
+ storage: {
42
+ eventCount: stats.totalEvents,
43
+ vectorCount: stats.vectorCount
44
+ },
45
+ sessions: {
46
+ total: uniqueSessions.size
47
+ },
48
+ eventsByType,
49
+ activity: {
50
+ daily: eventsByDay,
51
+ total7Days: recentEvents.filter(e => e.timestamp >= sevenDaysAgo).length
52
+ },
53
+ memory: {
54
+ heapUsed: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
55
+ heapTotal: Math.round(process.memoryUsage().heapTotal / 1024 / 1024)
56
+ },
57
+ levelStats: stats.levelStats
58
+ });
59
+ } catch (error) {
60
+ return c.json({ error: (error as Error).message }, 500);
61
+ }
62
+ });
63
+
64
+ // GET /api/stats/timeline - Get activity timeline
65
+ statsRouter.get('/timeline', async (c) => {
66
+ const days = parseInt(c.req.query('days') || '7', 10);
67
+
68
+ try {
69
+ const memoryService = getDefaultMemoryService();
70
+ await memoryService.initialize();
71
+
72
+ const recentEvents = await memoryService.getRecentEvents(10000);
73
+
74
+ const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
75
+ const filteredEvents = recentEvents.filter(e => e.timestamp >= cutoff);
76
+
77
+ // Group by day
78
+ const daily = filteredEvents.reduce((acc, e) => {
79
+ const day = e.timestamp.toISOString().split('T')[0];
80
+ if (!acc[day]) {
81
+ acc[day] = { date: day, total: 0, prompts: 0, responses: 0, tools: 0 };
82
+ }
83
+ acc[day].total++;
84
+ if (e.eventType === 'user_prompt') acc[day].prompts++;
85
+ if (e.eventType === 'agent_response') acc[day].responses++;
86
+ if (e.eventType === 'tool_observation') acc[day].tools++;
87
+ return acc;
88
+ }, {} as Record<string, { date: string; total: number; prompts: number; responses: number; tools: number }>);
89
+
90
+ return c.json({
91
+ days,
92
+ daily: Object.values(daily).sort((a, b) => a.date.localeCompare(b.date))
93
+ });
94
+ } catch (error) {
95
+ return c.json({ error: (error as Error).message }, 500);
96
+ }
97
+ });
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Web Viewer HTTP Server
3
+ * Provides REST API and serves static UI files
4
+ */
5
+
6
+ import { Hono } from 'hono';
7
+ import { cors } from 'hono/cors';
8
+ import { logger } from 'hono/logger';
9
+ import { serveStatic } from 'hono/bun';
10
+ import * as path from 'path';
11
+ import * as fs from 'fs';
12
+
13
+ import { apiRouter } from './api/index.js';
14
+
15
+ const app = new Hono();
16
+
17
+ // Middleware
18
+ app.use('/*', cors());
19
+ app.use('/*', logger());
20
+
21
+ // API routes
22
+ app.route('/api', apiRouter);
23
+
24
+ // Health check
25
+ app.get('/health', (c) => c.json({ status: 'ok', timestamp: new Date().toISOString() }));
26
+
27
+ // Static files (UI)
28
+ const uiPath = path.join(import.meta.dir, '../../dist/ui');
29
+ if (fs.existsSync(uiPath)) {
30
+ app.use('/*', serveStatic({ root: uiPath }));
31
+ }
32
+
33
+ // Fallback for SPA routing
34
+ app.get('*', (c) => {
35
+ const indexPath = path.join(uiPath, 'index.html');
36
+ if (fs.existsSync(indexPath)) {
37
+ return c.html(fs.readFileSync(indexPath, 'utf-8'));
38
+ }
39
+ return c.text('UI not built. Run "npm run build:ui" first.', 404);
40
+ });
41
+
42
+ export { app };
43
+
44
+ let serverInstance: ReturnType<typeof Bun.serve> | null = null;
45
+
46
+ /**
47
+ * Start the HTTP server
48
+ */
49
+ export function startServer(port: number = 37777): ReturnType<typeof Bun.serve> {
50
+ if (serverInstance) {
51
+ return serverInstance;
52
+ }
53
+
54
+ serverInstance = Bun.serve({
55
+ hostname: '127.0.0.1',
56
+ port,
57
+ fetch: app.fetch
58
+ });
59
+
60
+ console.log(`🧠 Code Memory viewer started at http://localhost:${port}`);
61
+
62
+ return serverInstance;
63
+ }
64
+
65
+ /**
66
+ * Stop the HTTP server
67
+ */
68
+ export function stopServer(): void {
69
+ if (serverInstance) {
70
+ serverInstance.stop();
71
+ serverInstance = null;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Check if server is running on given port
77
+ */
78
+ export async function isServerRunning(port: number = 37777): Promise<boolean> {
79
+ try {
80
+ const response = await fetch(`http://127.0.0.1:${port}/health`);
81
+ return response.ok;
82
+ } catch {
83
+ return false;
84
+ }
85
+ }
86
+
87
+ // Start server if run directly
88
+ if (import.meta.main) {
89
+ const port = parseInt(process.env.PORT || '37777', 10);
90
+ startServer(port);
91
+ }