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.
- package/.claude-plugin/commands/memory-forget.md +42 -0
- package/.claude-plugin/commands/memory-history.md +34 -0
- package/.claude-plugin/commands/memory-import.md +56 -0
- package/.claude-plugin/commands/memory-list.md +37 -0
- package/.claude-plugin/commands/memory-search.md +36 -0
- package/.claude-plugin/commands/memory-stats.md +34 -0
- package/.claude-plugin/hooks.json +59 -0
- package/.claude-plugin/plugin.json +24 -0
- package/.history/package_20260201112328.json +45 -0
- package/.history/package_20260201113602.json +45 -0
- package/.history/package_20260201113713.json +45 -0
- package/.history/package_20260201114110.json +45 -0
- package/Memo.txt +558 -0
- package/README.md +520 -0
- package/context.md +636 -0
- package/dist/.claude-plugin/commands/memory-forget.md +42 -0
- package/dist/.claude-plugin/commands/memory-history.md +34 -0
- package/dist/.claude-plugin/commands/memory-import.md +56 -0
- package/dist/.claude-plugin/commands/memory-list.md +37 -0
- package/dist/.claude-plugin/commands/memory-search.md +36 -0
- package/dist/.claude-plugin/commands/memory-stats.md +34 -0
- package/dist/.claude-plugin/hooks.json +59 -0
- package/dist/.claude-plugin/plugin.json +24 -0
- package/dist/cli/index.js +3539 -0
- package/dist/cli/index.js.map +7 -0
- package/dist/core/index.js +4408 -0
- package/dist/core/index.js.map +7 -0
- package/dist/hooks/session-end.js +2971 -0
- package/dist/hooks/session-end.js.map +7 -0
- package/dist/hooks/session-start.js +2969 -0
- package/dist/hooks/session-start.js.map +7 -0
- package/dist/hooks/stop.js +3123 -0
- package/dist/hooks/stop.js.map +7 -0
- package/dist/hooks/user-prompt-submit.js +2960 -0
- package/dist/hooks/user-prompt-submit.js.map +7 -0
- package/dist/services/memory-service.js +2931 -0
- package/dist/services/memory-service.js.map +7 -0
- package/package.json +45 -0
- package/plan.md +1642 -0
- package/scripts/build.ts +102 -0
- package/spec.md +624 -0
- package/specs/citations-system/context.md +243 -0
- package/specs/citations-system/plan.md +495 -0
- package/specs/citations-system/spec.md +371 -0
- package/specs/endless-mode/context.md +305 -0
- package/specs/endless-mode/plan.md +620 -0
- package/specs/endless-mode/spec.md +455 -0
- package/specs/entity-edge-model/context.md +401 -0
- package/specs/entity-edge-model/plan.md +459 -0
- package/specs/entity-edge-model/spec.md +391 -0
- package/specs/evidence-aligner-v2/context.md +401 -0
- package/specs/evidence-aligner-v2/plan.md +303 -0
- package/specs/evidence-aligner-v2/spec.md +312 -0
- package/specs/mcp-desktop-integration/context.md +278 -0
- package/specs/mcp-desktop-integration/plan.md +550 -0
- package/specs/mcp-desktop-integration/spec.md +494 -0
- package/specs/post-tool-use-hook/context.md +319 -0
- package/specs/post-tool-use-hook/plan.md +469 -0
- package/specs/post-tool-use-hook/spec.md +364 -0
- package/specs/private-tags/context.md +288 -0
- package/specs/private-tags/plan.md +412 -0
- package/specs/private-tags/spec.md +345 -0
- package/specs/progressive-disclosure/context.md +346 -0
- package/specs/progressive-disclosure/plan.md +663 -0
- package/specs/progressive-disclosure/spec.md +415 -0
- package/specs/task-entity-system/context.md +297 -0
- package/specs/task-entity-system/plan.md +301 -0
- package/specs/task-entity-system/spec.md +314 -0
- package/specs/vector-outbox-v2/context.md +470 -0
- package/specs/vector-outbox-v2/plan.md +562 -0
- package/specs/vector-outbox-v2/spec.md +466 -0
- package/specs/web-viewer-ui/context.md +384 -0
- package/specs/web-viewer-ui/plan.md +797 -0
- package/specs/web-viewer-ui/spec.md +516 -0
- package/src/cli/index.ts +570 -0
- package/src/core/canonical-key.ts +186 -0
- package/src/core/citation-generator.ts +63 -0
- package/src/core/consolidated-store.ts +279 -0
- package/src/core/consolidation-worker.ts +384 -0
- package/src/core/context-formatter.ts +276 -0
- package/src/core/continuity-manager.ts +336 -0
- package/src/core/edge-repo.ts +324 -0
- package/src/core/embedder.ts +124 -0
- package/src/core/entity-repo.ts +342 -0
- package/src/core/event-store.ts +672 -0
- package/src/core/evidence-aligner.ts +635 -0
- package/src/core/graduation.ts +365 -0
- package/src/core/index.ts +32 -0
- package/src/core/matcher.ts +210 -0
- package/src/core/metadata-extractor.ts +203 -0
- package/src/core/privacy/filter.ts +179 -0
- package/src/core/privacy/index.ts +20 -0
- package/src/core/privacy/tag-parser.ts +145 -0
- package/src/core/progressive-retriever.ts +415 -0
- package/src/core/retriever.ts +235 -0
- package/src/core/task/blocker-resolver.ts +325 -0
- package/src/core/task/index.ts +9 -0
- package/src/core/task/task-matcher.ts +238 -0
- package/src/core/task/task-projector.ts +345 -0
- package/src/core/task/task-resolver.ts +414 -0
- package/src/core/types.ts +841 -0
- package/src/core/vector-outbox.ts +295 -0
- package/src/core/vector-store.ts +182 -0
- package/src/core/vector-worker.ts +488 -0
- package/src/core/working-set-store.ts +244 -0
- package/src/hooks/post-tool-use.ts +127 -0
- package/src/hooks/session-end.ts +78 -0
- package/src/hooks/session-start.ts +57 -0
- package/src/hooks/stop.ts +78 -0
- package/src/hooks/user-prompt-submit.ts +54 -0
- package/src/mcp/handlers.ts +212 -0
- package/src/mcp/index.ts +47 -0
- package/src/mcp/tools.ts +78 -0
- package/src/server/api/citations.ts +101 -0
- package/src/server/api/events.ts +101 -0
- package/src/server/api/index.ts +18 -0
- package/src/server/api/search.ts +98 -0
- package/src/server/api/sessions.ts +111 -0
- package/src/server/api/stats.ts +97 -0
- package/src/server/index.ts +91 -0
- package/src/services/memory-service.ts +626 -0
- package/src/services/session-history-importer.ts +367 -0
- package/tests/canonical-key.test.ts +101 -0
- package/tests/evidence-aligner.test.ts +152 -0
- package/tests/matcher.test.ts +112 -0
- package/tsconfig.json +24 -0
- package/vitest.config.ts +15 -0
package/src/mcp/tools.ts
ADDED
|
@@ -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
|
+
}
|