claude-cortex 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/README.md +291 -0
- package/dist/api/events.d.ts +134 -0
- package/dist/api/events.d.ts.map +1 -0
- package/dist/api/events.js +73 -0
- package/dist/api/events.js.map +1 -0
- package/dist/api/visualization-server.d.ts +11 -0
- package/dist/api/visualization-server.d.ts.map +1 -0
- package/dist/api/visualization-server.js +653 -0
- package/dist/api/visualization-server.js.map +1 -0
- package/dist/context/project-context.d.ts +57 -0
- package/dist/context/project-context.d.ts.map +1 -0
- package/dist/context/project-context.js +135 -0
- package/dist/context/project-context.js.map +1 -0
- package/dist/database/init.d.ts +49 -0
- package/dist/database/init.d.ts.map +1 -0
- package/dist/database/init.js +336 -0
- package/dist/database/init.js.map +1 -0
- package/dist/embeddings/generator.d.ts +20 -0
- package/dist/embeddings/generator.d.ts.map +1 -0
- package/dist/embeddings/generator.js +77 -0
- package/dist/embeddings/generator.js.map +1 -0
- package/dist/embeddings/index.d.ts +2 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/index.js +2 -0
- package/dist/embeddings/index.js.map +1 -0
- package/dist/errors.d.ts +74 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +131 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +83 -0
- package/dist/index.js.map +1 -0
- package/dist/memory/activation.d.ts +69 -0
- package/dist/memory/activation.d.ts.map +1 -0
- package/dist/memory/activation.js +168 -0
- package/dist/memory/activation.js.map +1 -0
- package/dist/memory/consolidate.d.ts +96 -0
- package/dist/memory/consolidate.d.ts.map +1 -0
- package/dist/memory/consolidate.js +400 -0
- package/dist/memory/consolidate.js.map +1 -0
- package/dist/memory/contradiction.d.ts +69 -0
- package/dist/memory/contradiction.d.ts.map +1 -0
- package/dist/memory/contradiction.js +286 -0
- package/dist/memory/contradiction.js.map +1 -0
- package/dist/memory/decay.d.ts +62 -0
- package/dist/memory/decay.d.ts.map +1 -0
- package/dist/memory/decay.js +184 -0
- package/dist/memory/decay.js.map +1 -0
- package/dist/memory/salience.d.ts +36 -0
- package/dist/memory/salience.d.ts.map +1 -0
- package/dist/memory/salience.js +200 -0
- package/dist/memory/salience.js.map +1 -0
- package/dist/memory/similarity.d.ts +57 -0
- package/dist/memory/similarity.d.ts.map +1 -0
- package/dist/memory/similarity.js +114 -0
- package/dist/memory/similarity.js.map +1 -0
- package/dist/memory/store.d.ts +170 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +973 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/memory/types.d.ts +91 -0
- package/dist/memory/types.d.ts.map +1 -0
- package/dist/memory/types.js +30 -0
- package/dist/memory/types.js.map +1 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +466 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/context.d.ts +135 -0
- package/dist/tools/context.d.ts.map +1 -0
- package/dist/tools/context.js +273 -0
- package/dist/tools/context.js.map +1 -0
- package/dist/tools/forget.d.ts +53 -0
- package/dist/tools/forget.d.ts.map +1 -0
- package/dist/tools/forget.js +179 -0
- package/dist/tools/forget.js.map +1 -0
- package/dist/tools/recall.d.ts +74 -0
- package/dist/tools/recall.d.ts.map +1 -0
- package/dist/tools/recall.js +140 -0
- package/dist/tools/recall.js.map +1 -0
- package/dist/tools/remember.d.ts +65 -0
- package/dist/tools/remember.d.ts.map +1 -0
- package/dist/tools/remember.js +147 -0
- package/dist/tools/remember.js.map +1 -0
- package/dist/worker/brain-worker.d.ts +100 -0
- package/dist/worker/brain-worker.d.ts.map +1 -0
- package/dist/worker/brain-worker.js +261 -0
- package/dist/worker/brain-worker.js.map +1 -0
- package/dist/worker/link-discovery.d.ts +47 -0
- package/dist/worker/link-discovery.d.ts.map +1 -0
- package/dist/worker/link-discovery.js +103 -0
- package/dist/worker/link-discovery.js.map +1 -0
- package/dist/worker/predictive-consolidation.d.ts +46 -0
- package/dist/worker/predictive-consolidation.d.ts.map +1 -0
- package/dist/worker/predictive-consolidation.js +110 -0
- package/dist/worker/predictive-consolidation.js.map +1 -0
- package/dist/worker/types.d.ts +91 -0
- package/dist/worker/types.d.ts.map +1 -0
- package/dist/worker/types.js +22 -0
- package/dist/worker/types.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visualization API Server
|
|
3
|
+
*
|
|
4
|
+
* Provides REST endpoints and WebSocket for the Brain Dashboard.
|
|
5
|
+
* Runs alongside or instead of the MCP server.
|
|
6
|
+
*/
|
|
7
|
+
import express from 'express';
|
|
8
|
+
import cors from 'cors';
|
|
9
|
+
import { createServer } from 'http';
|
|
10
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
11
|
+
import { getDatabase, initDatabase, checkpointWal } from '../database/init.js';
|
|
12
|
+
import { DEFAULT_CONFIG } from '../memory/types.js';
|
|
13
|
+
import { searchMemories, getRecentMemories, getHighPriorityMemories, getMemoryStats, getMemoryById, addMemory, deleteMemory, accessMemory, updateDecayScores, rowToMemory, } from '../memory/store.js';
|
|
14
|
+
import { consolidate, generateContextSummary, formatContextSummary, } from '../memory/consolidate.js';
|
|
15
|
+
import { calculateDecayedScore } from '../memory/decay.js';
|
|
16
|
+
import { getActivationStats, getActiveMemories } from '../memory/activation.js';
|
|
17
|
+
import { detectContradictions, getContradictionsFor } from '../memory/contradiction.js';
|
|
18
|
+
import { enrichMemory } from '../memory/store.js';
|
|
19
|
+
import { memoryEvents, emitDecayTick } from './events.js';
|
|
20
|
+
import { BrainWorker } from '../worker/brain-worker.js';
|
|
21
|
+
const PORT = process.env.PORT || 3001;
|
|
22
|
+
// Track connected WebSocket clients
|
|
23
|
+
const clients = new Set();
|
|
24
|
+
/**
|
|
25
|
+
* Start the visualization API server
|
|
26
|
+
*/
|
|
27
|
+
export function startVisualizationServer(dbPath) {
|
|
28
|
+
// Initialize database
|
|
29
|
+
initDatabase(dbPath || DEFAULT_CONFIG.dbPath);
|
|
30
|
+
const app = express();
|
|
31
|
+
const server = createServer(app);
|
|
32
|
+
// Middleware
|
|
33
|
+
app.use(cors());
|
|
34
|
+
app.use(express.json());
|
|
35
|
+
// ============================================
|
|
36
|
+
// REST API ENDPOINTS
|
|
37
|
+
// ============================================
|
|
38
|
+
// Health check
|
|
39
|
+
app.get('/api/health', (_req, res) => {
|
|
40
|
+
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
|
41
|
+
});
|
|
42
|
+
// Get all memories with filters and pagination
|
|
43
|
+
app.get('/api/memories', async (req, res) => {
|
|
44
|
+
try {
|
|
45
|
+
// Extract query params as strings
|
|
46
|
+
const project = typeof req.query.project === 'string' ? req.query.project : undefined;
|
|
47
|
+
const type = typeof req.query.type === 'string' ? req.query.type : undefined;
|
|
48
|
+
const category = typeof req.query.category === 'string' ? req.query.category : undefined;
|
|
49
|
+
const limitStr = typeof req.query.limit === 'string' ? req.query.limit : '50';
|
|
50
|
+
const offsetStr = typeof req.query.offset === 'string' ? req.query.offset : '0';
|
|
51
|
+
const mode = typeof req.query.mode === 'string' ? req.query.mode : 'recent';
|
|
52
|
+
const query = typeof req.query.query === 'string' ? req.query.query : undefined;
|
|
53
|
+
const limit = Math.min(parseInt(limitStr), 200); // Cap at 200
|
|
54
|
+
const offset = parseInt(offsetStr);
|
|
55
|
+
let memories;
|
|
56
|
+
if (mode === 'search' && query) {
|
|
57
|
+
const results = await searchMemories({
|
|
58
|
+
query,
|
|
59
|
+
project,
|
|
60
|
+
type: type,
|
|
61
|
+
category: category,
|
|
62
|
+
limit: limit + offset + 1, // Fetch extra to check hasMore
|
|
63
|
+
});
|
|
64
|
+
memories = results.map(r => r.memory);
|
|
65
|
+
}
|
|
66
|
+
else if (mode === 'important') {
|
|
67
|
+
memories = getHighPriorityMemories(limit + offset + 1, project);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
memories = getRecentMemories(limit + offset + 1, project);
|
|
71
|
+
}
|
|
72
|
+
// Filter by type and category if provided
|
|
73
|
+
if (type) {
|
|
74
|
+
memories = memories.filter(m => m.type === type);
|
|
75
|
+
}
|
|
76
|
+
if (category) {
|
|
77
|
+
memories = memories.filter(m => m.category === category);
|
|
78
|
+
}
|
|
79
|
+
// Get total count for pagination
|
|
80
|
+
const stats = getMemoryStats(project);
|
|
81
|
+
const total = stats.total;
|
|
82
|
+
// Apply pagination
|
|
83
|
+
const hasMore = memories.length > offset + limit;
|
|
84
|
+
const paginatedMemories = memories.slice(offset, offset + limit);
|
|
85
|
+
// Add computed decayed score to each memory
|
|
86
|
+
const memoriesWithDecay = paginatedMemories.map(m => ({
|
|
87
|
+
...m,
|
|
88
|
+
decayedScore: calculateDecayedScore(m),
|
|
89
|
+
}));
|
|
90
|
+
res.json({
|
|
91
|
+
memories: memoriesWithDecay,
|
|
92
|
+
pagination: {
|
|
93
|
+
offset,
|
|
94
|
+
limit,
|
|
95
|
+
total,
|
|
96
|
+
hasMore,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
res.status(500).json({ error: error.message });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
// Get single memory by ID
|
|
105
|
+
app.get('/api/memories/:id', (req, res) => {
|
|
106
|
+
try {
|
|
107
|
+
const id = parseInt(req.params.id);
|
|
108
|
+
const memory = getMemoryById(id);
|
|
109
|
+
if (!memory) {
|
|
110
|
+
return res.status(404).json({ error: 'Memory not found' });
|
|
111
|
+
}
|
|
112
|
+
res.json({
|
|
113
|
+
...memory,
|
|
114
|
+
decayedScore: calculateDecayedScore(memory),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
res.status(500).json({ error: error.message });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
// Create memory
|
|
122
|
+
app.post('/api/memories', (req, res) => {
|
|
123
|
+
try {
|
|
124
|
+
const { title, content, type, category, project, tags, salience } = req.body;
|
|
125
|
+
if (!title || !content) {
|
|
126
|
+
return res.status(400).json({ error: 'Title and content required' });
|
|
127
|
+
}
|
|
128
|
+
const memory = addMemory({
|
|
129
|
+
title,
|
|
130
|
+
content,
|
|
131
|
+
type: type || 'short_term',
|
|
132
|
+
category: category || 'note',
|
|
133
|
+
project,
|
|
134
|
+
tags: tags || [],
|
|
135
|
+
salience,
|
|
136
|
+
});
|
|
137
|
+
res.status(201).json(memory);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
res.status(500).json({ error: error.message });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
// Delete memory
|
|
144
|
+
app.delete('/api/memories/:id', (req, res) => {
|
|
145
|
+
try {
|
|
146
|
+
const id = parseInt(req.params.id);
|
|
147
|
+
const success = deleteMemory(id);
|
|
148
|
+
if (!success) {
|
|
149
|
+
return res.status(404).json({ error: 'Memory not found' });
|
|
150
|
+
}
|
|
151
|
+
res.json({ success: true });
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
res.status(500).json({ error: error.message });
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
// Access/reinforce memory
|
|
158
|
+
app.post('/api/memories/:id/access', (req, res) => {
|
|
159
|
+
try {
|
|
160
|
+
const id = parseInt(req.params.id);
|
|
161
|
+
const memory = accessMemory(id);
|
|
162
|
+
if (!memory) {
|
|
163
|
+
return res.status(404).json({ error: 'Memory not found' });
|
|
164
|
+
}
|
|
165
|
+
res.json({
|
|
166
|
+
...memory,
|
|
167
|
+
decayedScore: calculateDecayedScore(memory),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
res.status(500).json({ error: error.message });
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
// Get statistics
|
|
175
|
+
app.get('/api/stats', (req, res) => {
|
|
176
|
+
try {
|
|
177
|
+
const project = typeof req.query.project === 'string' ? req.query.project : undefined;
|
|
178
|
+
const stats = getMemoryStats(project);
|
|
179
|
+
// Add decay distribution
|
|
180
|
+
const db = getDatabase();
|
|
181
|
+
const rawRows = db.prepare(project
|
|
182
|
+
? 'SELECT * FROM memories WHERE project = ?'
|
|
183
|
+
: 'SELECT * FROM memories').all(project ? [project] : []);
|
|
184
|
+
// Convert raw DB rows to Memory objects (snake_case -> camelCase)
|
|
185
|
+
const allMemories = rawRows.map(rowToMemory);
|
|
186
|
+
const decayDistribution = {
|
|
187
|
+
healthy: 0, // > 0.35 (realistic given base salience 0.25 + access bonus)
|
|
188
|
+
fading: 0, // 0.2 - 0.35
|
|
189
|
+
critical: 0, // < 0.2 (approaching deletion threshold)
|
|
190
|
+
};
|
|
191
|
+
for (const m of allMemories) {
|
|
192
|
+
const score = calculateDecayedScore(m);
|
|
193
|
+
if (score > 0.35)
|
|
194
|
+
decayDistribution.healthy++;
|
|
195
|
+
else if (score > 0.2)
|
|
196
|
+
decayDistribution.fading++;
|
|
197
|
+
else
|
|
198
|
+
decayDistribution.critical++;
|
|
199
|
+
}
|
|
200
|
+
// Get spreading activation stats (Phase 2 organic feature)
|
|
201
|
+
const activationStats = getActivationStats();
|
|
202
|
+
res.json({
|
|
203
|
+
...stats,
|
|
204
|
+
decayDistribution,
|
|
205
|
+
activation: activationStats,
|
|
206
|
+
timestamp: new Date().toISOString(),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
res.status(500).json({ error: error.message });
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
// Get currently activated memories (spreading activation)
|
|
214
|
+
app.get('/api/activation', (_req, res) => {
|
|
215
|
+
try {
|
|
216
|
+
const activeMemories = getActiveMemories();
|
|
217
|
+
const stats = getActivationStats();
|
|
218
|
+
res.json({
|
|
219
|
+
activeMemories,
|
|
220
|
+
stats,
|
|
221
|
+
timestamp: new Date().toISOString(),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
res.status(500).json({ error: error.message });
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
// ============================================
|
|
229
|
+
// ORGANIC BRAIN ENDPOINTS (Phase 3)
|
|
230
|
+
// ============================================
|
|
231
|
+
// Get detected contradictions
|
|
232
|
+
app.get('/api/contradictions', (req, res) => {
|
|
233
|
+
try {
|
|
234
|
+
const project = typeof req.query.project === 'string' ? req.query.project : undefined;
|
|
235
|
+
const category = typeof req.query.category === 'string' ? req.query.category : undefined;
|
|
236
|
+
const minScoreStr = typeof req.query.minScore === 'string' ? req.query.minScore : '0.4';
|
|
237
|
+
const limitStr = typeof req.query.limit === 'string' ? req.query.limit : '20';
|
|
238
|
+
const minScore = parseFloat(minScoreStr);
|
|
239
|
+
const limit = parseInt(limitStr);
|
|
240
|
+
const contradictions = detectContradictions({
|
|
241
|
+
project,
|
|
242
|
+
category: category,
|
|
243
|
+
minScore,
|
|
244
|
+
limit,
|
|
245
|
+
});
|
|
246
|
+
res.json({
|
|
247
|
+
contradictions: contradictions.map(c => ({
|
|
248
|
+
memoryAId: c.memoryA.id,
|
|
249
|
+
memoryATitle: c.memoryA.title,
|
|
250
|
+
memoryBId: c.memoryB.id,
|
|
251
|
+
memoryBTitle: c.memoryB.title,
|
|
252
|
+
score: c.score,
|
|
253
|
+
reason: c.reason,
|
|
254
|
+
sharedTopics: c.sharedTopics,
|
|
255
|
+
})),
|
|
256
|
+
count: contradictions.length,
|
|
257
|
+
timestamp: new Date().toISOString(),
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
res.status(500).json({ error: error.message });
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
// Get contradictions for a specific memory
|
|
265
|
+
app.get('/api/memories/:id/contradictions', (req, res) => {
|
|
266
|
+
try {
|
|
267
|
+
const id = parseInt(req.params.id);
|
|
268
|
+
if (isNaN(id)) {
|
|
269
|
+
return res.status(400).json({ error: 'Invalid memory ID' });
|
|
270
|
+
}
|
|
271
|
+
const contradictions = getContradictionsFor(id);
|
|
272
|
+
res.json({
|
|
273
|
+
memoryId: id,
|
|
274
|
+
contradictions: contradictions.map(c => ({
|
|
275
|
+
contradictingMemoryId: c.memoryB.id,
|
|
276
|
+
contradictingMemoryTitle: c.memoryB.title,
|
|
277
|
+
score: c.score,
|
|
278
|
+
reason: c.reason,
|
|
279
|
+
sharedTopics: c.sharedTopics,
|
|
280
|
+
})),
|
|
281
|
+
count: contradictions.length,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
res.status(500).json({ error: error.message });
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
// Manually enrich a memory with new context
|
|
289
|
+
app.post('/api/memories/:id/enrich', (req, res) => {
|
|
290
|
+
try {
|
|
291
|
+
const id = parseInt(req.params.id);
|
|
292
|
+
if (isNaN(id)) {
|
|
293
|
+
return res.status(400).json({ error: 'Invalid memory ID' });
|
|
294
|
+
}
|
|
295
|
+
const { context, contextType } = req.body;
|
|
296
|
+
if (!context || typeof context !== 'string') {
|
|
297
|
+
return res.status(400).json({ error: 'Context string required in request body' });
|
|
298
|
+
}
|
|
299
|
+
const validTypes = ['search', 'access', 'related'];
|
|
300
|
+
const type = validTypes.includes(contextType) ? contextType : 'access';
|
|
301
|
+
const result = enrichMemory(id, context, type);
|
|
302
|
+
res.json(result);
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
res.status(500).json({ error: error.message });
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
// Get list of all projects
|
|
309
|
+
app.get('/api/projects', (_req, res) => {
|
|
310
|
+
try {
|
|
311
|
+
const db = getDatabase();
|
|
312
|
+
const projects = db.prepare(`
|
|
313
|
+
SELECT DISTINCT project, COUNT(*) as memory_count
|
|
314
|
+
FROM memories
|
|
315
|
+
WHERE project IS NOT NULL AND project != ''
|
|
316
|
+
GROUP BY project
|
|
317
|
+
ORDER BY memory_count DESC
|
|
318
|
+
`).all();
|
|
319
|
+
// Add "All Projects" option with total count
|
|
320
|
+
const totalCount = db.prepare('SELECT COUNT(*) as count FROM memories').get();
|
|
321
|
+
res.json({
|
|
322
|
+
projects: [
|
|
323
|
+
{ project: null, memory_count: totalCount.count, label: 'All Projects' },
|
|
324
|
+
...projects.map(p => ({ ...p, label: p.project })),
|
|
325
|
+
],
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
res.status(500).json({ error: error.message });
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
// Get memory links/relationships
|
|
333
|
+
app.get('/api/links', (req, res) => {
|
|
334
|
+
try {
|
|
335
|
+
const project = typeof req.query.project === 'string' ? req.query.project : undefined;
|
|
336
|
+
const db = getDatabase();
|
|
337
|
+
const query = project
|
|
338
|
+
? `
|
|
339
|
+
SELECT
|
|
340
|
+
ml.*,
|
|
341
|
+
m1.title as source_title,
|
|
342
|
+
m1.category as source_category,
|
|
343
|
+
m1.type as source_type,
|
|
344
|
+
m2.title as target_title,
|
|
345
|
+
m2.category as target_category,
|
|
346
|
+
m2.type as target_type
|
|
347
|
+
FROM memory_links ml
|
|
348
|
+
JOIN memories m1 ON ml.source_id = m1.id
|
|
349
|
+
JOIN memories m2 ON ml.target_id = m2.id
|
|
350
|
+
WHERE m1.project = ? OR m2.project = ?
|
|
351
|
+
ORDER BY ml.created_at DESC
|
|
352
|
+
LIMIT 500
|
|
353
|
+
`
|
|
354
|
+
: `
|
|
355
|
+
SELECT
|
|
356
|
+
ml.*,
|
|
357
|
+
m1.title as source_title,
|
|
358
|
+
m1.category as source_category,
|
|
359
|
+
m1.type as source_type,
|
|
360
|
+
m2.title as target_title,
|
|
361
|
+
m2.category as target_category,
|
|
362
|
+
m2.type as target_type
|
|
363
|
+
FROM memory_links ml
|
|
364
|
+
JOIN memories m1 ON ml.source_id = m1.id
|
|
365
|
+
JOIN memories m2 ON ml.target_id = m2.id
|
|
366
|
+
ORDER BY ml.created_at DESC
|
|
367
|
+
LIMIT 500
|
|
368
|
+
`;
|
|
369
|
+
const links = project
|
|
370
|
+
? db.prepare(query).all(project, project)
|
|
371
|
+
: db.prepare(query).all();
|
|
372
|
+
res.json(links);
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
res.status(500).json({ error: error.message });
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
// Trigger consolidation
|
|
379
|
+
app.post('/api/consolidate', (_req, res) => {
|
|
380
|
+
try {
|
|
381
|
+
const result = consolidate();
|
|
382
|
+
res.json({
|
|
383
|
+
success: true,
|
|
384
|
+
...result,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
res.status(500).json({ error: error.message });
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
// Get context summary
|
|
392
|
+
app.get('/api/context', async (req, res) => {
|
|
393
|
+
try {
|
|
394
|
+
const project = typeof req.query.project === 'string' ? req.query.project : undefined;
|
|
395
|
+
const summary = await generateContextSummary(project);
|
|
396
|
+
const formatted = formatContextSummary(summary);
|
|
397
|
+
res.json({
|
|
398
|
+
summary,
|
|
399
|
+
formatted,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
res.status(500).json({ error: error.message });
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
// Get search suggestions (for autocomplete)
|
|
407
|
+
app.get('/api/suggestions', (req, res) => {
|
|
408
|
+
try {
|
|
409
|
+
const query = typeof req.query.q === 'string' ? req.query.q : '';
|
|
410
|
+
const limit = typeof req.query.limit === 'string' ? parseInt(req.query.limit) : 10;
|
|
411
|
+
if (!query || query.length < 2) {
|
|
412
|
+
return res.json({ suggestions: [] });
|
|
413
|
+
}
|
|
414
|
+
const db = getDatabase();
|
|
415
|
+
// Get suggestions from memory titles, categories, tags, and projects
|
|
416
|
+
const suggestions = [];
|
|
417
|
+
// Search titles that contain the query
|
|
418
|
+
const titleMatches = db.prepare(`
|
|
419
|
+
SELECT DISTINCT title, COUNT(*) as count
|
|
420
|
+
FROM memories
|
|
421
|
+
WHERE title LIKE ?
|
|
422
|
+
GROUP BY title
|
|
423
|
+
ORDER BY count DESC, last_accessed DESC
|
|
424
|
+
LIMIT ?
|
|
425
|
+
`).all(`%${query}%`, limit);
|
|
426
|
+
for (const match of titleMatches) {
|
|
427
|
+
suggestions.push({ text: match.title, type: 'title', count: match.count });
|
|
428
|
+
}
|
|
429
|
+
// Get matching categories
|
|
430
|
+
const categoryMatches = db.prepare(`
|
|
431
|
+
SELECT DISTINCT category, COUNT(*) as count
|
|
432
|
+
FROM memories
|
|
433
|
+
WHERE category LIKE ?
|
|
434
|
+
GROUP BY category
|
|
435
|
+
ORDER BY count DESC
|
|
436
|
+
LIMIT 5
|
|
437
|
+
`).all(`%${query}%`);
|
|
438
|
+
for (const match of categoryMatches) {
|
|
439
|
+
suggestions.push({ text: match.category, type: 'category', count: match.count });
|
|
440
|
+
}
|
|
441
|
+
// Get matching projects
|
|
442
|
+
const projectMatches = db.prepare(`
|
|
443
|
+
SELECT DISTINCT project, COUNT(*) as count
|
|
444
|
+
FROM memories
|
|
445
|
+
WHERE project IS NOT NULL AND project LIKE ?
|
|
446
|
+
GROUP BY project
|
|
447
|
+
ORDER BY count DESC
|
|
448
|
+
LIMIT 5
|
|
449
|
+
`).all(`%${query}%`);
|
|
450
|
+
for (const match of projectMatches) {
|
|
451
|
+
suggestions.push({ text: match.project, type: 'project', count: match.count });
|
|
452
|
+
}
|
|
453
|
+
// Sort by count and limit total results
|
|
454
|
+
suggestions.sort((a, b) => b.count - a.count);
|
|
455
|
+
const limitedSuggestions = suggestions.slice(0, limit);
|
|
456
|
+
res.json({ suggestions: limitedSuggestions });
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
res.status(500).json({ error: error.message });
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
// ============================================
|
|
463
|
+
// BRAIN WORKER (Phase 4)
|
|
464
|
+
// ============================================
|
|
465
|
+
// Create and start the background brain worker
|
|
466
|
+
const brainWorker = new BrainWorker();
|
|
467
|
+
// Worker status endpoint
|
|
468
|
+
app.get('/api/worker/status', (_req, res) => {
|
|
469
|
+
try {
|
|
470
|
+
res.json(brainWorker.getStatus());
|
|
471
|
+
}
|
|
472
|
+
catch (error) {
|
|
473
|
+
res.status(500).json({ error: error.message });
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
// Manually trigger light tick (for testing)
|
|
477
|
+
app.post('/api/worker/trigger-light', async (_req, res) => {
|
|
478
|
+
try {
|
|
479
|
+
const result = await brainWorker.triggerLightTick();
|
|
480
|
+
res.json({
|
|
481
|
+
success: true,
|
|
482
|
+
...result,
|
|
483
|
+
timestamp: result.timestamp.toISOString(),
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
res.status(500).json({ error: error.message });
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
// Manually trigger medium tick (for testing)
|
|
491
|
+
app.post('/api/worker/trigger-medium', async (_req, res) => {
|
|
492
|
+
try {
|
|
493
|
+
const result = await brainWorker.triggerMediumTick();
|
|
494
|
+
res.json({
|
|
495
|
+
success: true,
|
|
496
|
+
...result,
|
|
497
|
+
timestamp: result.timestamp.toISOString(),
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
res.status(500).json({ error: error.message });
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
// ============================================
|
|
505
|
+
// WEBSOCKET SERVER
|
|
506
|
+
// ============================================
|
|
507
|
+
const wss = new WebSocketServer({ server, path: '/ws/events' });
|
|
508
|
+
wss.on('connection', (ws) => {
|
|
509
|
+
clients.add(ws);
|
|
510
|
+
console.log(`[WS] Client connected. Total: ${clients.size}`);
|
|
511
|
+
// Send initial state
|
|
512
|
+
const stats = getMemoryStats();
|
|
513
|
+
const memories = getRecentMemories(100);
|
|
514
|
+
const memoriesWithDecay = memories.map(m => ({
|
|
515
|
+
...m,
|
|
516
|
+
decayedScore: calculateDecayedScore(m),
|
|
517
|
+
}));
|
|
518
|
+
ws.send(JSON.stringify({
|
|
519
|
+
type: 'initial_state',
|
|
520
|
+
timestamp: new Date().toISOString(),
|
|
521
|
+
data: {
|
|
522
|
+
stats,
|
|
523
|
+
memories: memoriesWithDecay,
|
|
524
|
+
},
|
|
525
|
+
}));
|
|
526
|
+
ws.on('close', () => {
|
|
527
|
+
clients.delete(ws);
|
|
528
|
+
console.log(`[WS] Client disconnected. Total: ${clients.size}`);
|
|
529
|
+
});
|
|
530
|
+
ws.on('error', (error) => {
|
|
531
|
+
console.error('[WS] Error:', error);
|
|
532
|
+
clients.delete(ws);
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
// Broadcast events to all connected clients
|
|
536
|
+
function broadcast(event) {
|
|
537
|
+
const message = JSON.stringify(event);
|
|
538
|
+
for (const client of clients) {
|
|
539
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
540
|
+
client.send(message);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
// Subscribe to memory events
|
|
545
|
+
memoryEvents.onMemoryEvent((event) => {
|
|
546
|
+
broadcast(event);
|
|
547
|
+
});
|
|
548
|
+
// Decay tick - update clients with decay changes every 30 seconds
|
|
549
|
+
let decayTickCount = 0;
|
|
550
|
+
setInterval(() => {
|
|
551
|
+
const db = getDatabase();
|
|
552
|
+
const rawRows = db.prepare('SELECT * FROM memories ORDER BY last_accessed DESC LIMIT 200').all();
|
|
553
|
+
// Convert raw DB rows to Memory objects (snake_case -> camelCase)
|
|
554
|
+
const memories = rawRows.map(rowToMemory);
|
|
555
|
+
const updates = [];
|
|
556
|
+
for (const memory of memories) {
|
|
557
|
+
const newScore = calculateDecayedScore(memory);
|
|
558
|
+
// Only include memories that have decayed significantly since last update
|
|
559
|
+
// Compare to decayedScore (not salience) to detect actual changes
|
|
560
|
+
if (Math.abs(newScore - memory.decayedScore) > 0.01) {
|
|
561
|
+
updates.push({
|
|
562
|
+
memoryId: memory.id,
|
|
563
|
+
oldScore: memory.decayedScore,
|
|
564
|
+
newScore,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (updates.length > 0) {
|
|
569
|
+
emitDecayTick(updates);
|
|
570
|
+
}
|
|
571
|
+
// Persist decay scores and checkpoint WAL every 5 minutes (10 ticks)
|
|
572
|
+
decayTickCount++;
|
|
573
|
+
if (decayTickCount >= 10) {
|
|
574
|
+
decayTickCount = 0;
|
|
575
|
+
try {
|
|
576
|
+
updateDecayScores();
|
|
577
|
+
// Checkpoint WAL to prevent file bloat and reduce contention
|
|
578
|
+
const checkpoint = checkpointWal();
|
|
579
|
+
if (checkpoint.walPages > 0) {
|
|
580
|
+
console.log(`[WAL] Checkpointed ${checkpoint.checkpointed}/${checkpoint.walPages} pages`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
catch (error) {
|
|
584
|
+
console.error('[Maintenance] Failed to persist decay scores or checkpoint:', error);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}, 30000);
|
|
588
|
+
// ============================================
|
|
589
|
+
// START SERVER
|
|
590
|
+
// ============================================
|
|
591
|
+
// Start brain worker before starting server
|
|
592
|
+
brainWorker.start();
|
|
593
|
+
// Graceful shutdown handler
|
|
594
|
+
function gracefulShutdown(signal) {
|
|
595
|
+
console.log(`\n[Server] Received ${signal}, shutting down gracefully...`);
|
|
596
|
+
// Stop the brain worker
|
|
597
|
+
brainWorker.stop();
|
|
598
|
+
// Close WebSocket connections
|
|
599
|
+
for (const client of clients) {
|
|
600
|
+
client.close();
|
|
601
|
+
}
|
|
602
|
+
clients.clear();
|
|
603
|
+
// Close the HTTP server
|
|
604
|
+
server.close(() => {
|
|
605
|
+
console.log('[Server] HTTP server closed');
|
|
606
|
+
// Checkpoint WAL before exit
|
|
607
|
+
try {
|
|
608
|
+
checkpointWal();
|
|
609
|
+
console.log('[Server] WAL checkpointed');
|
|
610
|
+
}
|
|
611
|
+
catch (e) {
|
|
612
|
+
console.error('[Server] Failed to checkpoint WAL:', e);
|
|
613
|
+
}
|
|
614
|
+
process.exit(0);
|
|
615
|
+
});
|
|
616
|
+
// Force exit after 10 seconds
|
|
617
|
+
setTimeout(() => {
|
|
618
|
+
console.error('[Server] Forced exit after timeout');
|
|
619
|
+
process.exit(1);
|
|
620
|
+
}, 10000);
|
|
621
|
+
}
|
|
622
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
623
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
624
|
+
server.listen(PORT, () => {
|
|
625
|
+
console.log(`
|
|
626
|
+
╔══════════════════════════════════════════════════════════════╗
|
|
627
|
+
║ Claude Memory Visualization Server ║
|
|
628
|
+
╠══════════════════════════════════════════════════════════════╣
|
|
629
|
+
║ REST API: http://localhost:${PORT}/api ║
|
|
630
|
+
║ WebSocket: ws://localhost:${PORT}/ws/events ║
|
|
631
|
+
║ ║
|
|
632
|
+
║ Endpoints: ║
|
|
633
|
+
║ GET /api/health - Health check ║
|
|
634
|
+
║ GET /api/memories - List memories ║
|
|
635
|
+
║ GET /api/memories/:id - Get memory ║
|
|
636
|
+
║ POST /api/memories - Create memory ║
|
|
637
|
+
║ DEL /api/memories/:id - Delete memory ║
|
|
638
|
+
║ POST /api/memories/:id/access - Reinforce memory ║
|
|
639
|
+
║ GET /api/stats - Memory statistics ║
|
|
640
|
+
║ GET /api/links - Memory relationships ║
|
|
641
|
+
║ POST /api/consolidate - Trigger consolidation ║
|
|
642
|
+
║ GET /api/context - Context summary ║
|
|
643
|
+
║ GET /api/suggestions - Search autocomplete ║
|
|
644
|
+
║ ║
|
|
645
|
+
║ Brain Worker (Phase 4): ║
|
|
646
|
+
║ GET /api/worker/status - Worker status ║
|
|
647
|
+
║ POST /api/worker/trigger-light - Trigger light tick ║
|
|
648
|
+
║ POST /api/worker/trigger-medium - Trigger medium tick ║
|
|
649
|
+
╚══════════════════════════════════════════════════════════════╝
|
|
650
|
+
`);
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
//# sourceMappingURL=visualization-server.js.map
|