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.
Files changed (102) hide show
  1. package/README.md +291 -0
  2. package/dist/api/events.d.ts +134 -0
  3. package/dist/api/events.d.ts.map +1 -0
  4. package/dist/api/events.js +73 -0
  5. package/dist/api/events.js.map +1 -0
  6. package/dist/api/visualization-server.d.ts +11 -0
  7. package/dist/api/visualization-server.d.ts.map +1 -0
  8. package/dist/api/visualization-server.js +653 -0
  9. package/dist/api/visualization-server.js.map +1 -0
  10. package/dist/context/project-context.d.ts +57 -0
  11. package/dist/context/project-context.d.ts.map +1 -0
  12. package/dist/context/project-context.js +135 -0
  13. package/dist/context/project-context.js.map +1 -0
  14. package/dist/database/init.d.ts +49 -0
  15. package/dist/database/init.d.ts.map +1 -0
  16. package/dist/database/init.js +336 -0
  17. package/dist/database/init.js.map +1 -0
  18. package/dist/embeddings/generator.d.ts +20 -0
  19. package/dist/embeddings/generator.d.ts.map +1 -0
  20. package/dist/embeddings/generator.js +77 -0
  21. package/dist/embeddings/generator.js.map +1 -0
  22. package/dist/embeddings/index.d.ts +2 -0
  23. package/dist/embeddings/index.d.ts.map +1 -0
  24. package/dist/embeddings/index.js +2 -0
  25. package/dist/embeddings/index.js.map +1 -0
  26. package/dist/errors.d.ts +74 -0
  27. package/dist/errors.d.ts.map +1 -0
  28. package/dist/errors.js +131 -0
  29. package/dist/errors.js.map +1 -0
  30. package/dist/index.d.ts +16 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +83 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/memory/activation.d.ts +69 -0
  35. package/dist/memory/activation.d.ts.map +1 -0
  36. package/dist/memory/activation.js +168 -0
  37. package/dist/memory/activation.js.map +1 -0
  38. package/dist/memory/consolidate.d.ts +96 -0
  39. package/dist/memory/consolidate.d.ts.map +1 -0
  40. package/dist/memory/consolidate.js +400 -0
  41. package/dist/memory/consolidate.js.map +1 -0
  42. package/dist/memory/contradiction.d.ts +69 -0
  43. package/dist/memory/contradiction.d.ts.map +1 -0
  44. package/dist/memory/contradiction.js +286 -0
  45. package/dist/memory/contradiction.js.map +1 -0
  46. package/dist/memory/decay.d.ts +62 -0
  47. package/dist/memory/decay.d.ts.map +1 -0
  48. package/dist/memory/decay.js +184 -0
  49. package/dist/memory/decay.js.map +1 -0
  50. package/dist/memory/salience.d.ts +36 -0
  51. package/dist/memory/salience.d.ts.map +1 -0
  52. package/dist/memory/salience.js +200 -0
  53. package/dist/memory/salience.js.map +1 -0
  54. package/dist/memory/similarity.d.ts +57 -0
  55. package/dist/memory/similarity.d.ts.map +1 -0
  56. package/dist/memory/similarity.js +114 -0
  57. package/dist/memory/similarity.js.map +1 -0
  58. package/dist/memory/store.d.ts +170 -0
  59. package/dist/memory/store.d.ts.map +1 -0
  60. package/dist/memory/store.js +973 -0
  61. package/dist/memory/store.js.map +1 -0
  62. package/dist/memory/types.d.ts +91 -0
  63. package/dist/memory/types.d.ts.map +1 -0
  64. package/dist/memory/types.js +30 -0
  65. package/dist/memory/types.js.map +1 -0
  66. package/dist/server.d.ts +12 -0
  67. package/dist/server.d.ts.map +1 -0
  68. package/dist/server.js +466 -0
  69. package/dist/server.js.map +1 -0
  70. package/dist/tools/context.d.ts +135 -0
  71. package/dist/tools/context.d.ts.map +1 -0
  72. package/dist/tools/context.js +273 -0
  73. package/dist/tools/context.js.map +1 -0
  74. package/dist/tools/forget.d.ts +53 -0
  75. package/dist/tools/forget.d.ts.map +1 -0
  76. package/dist/tools/forget.js +179 -0
  77. package/dist/tools/forget.js.map +1 -0
  78. package/dist/tools/recall.d.ts +74 -0
  79. package/dist/tools/recall.d.ts.map +1 -0
  80. package/dist/tools/recall.js +140 -0
  81. package/dist/tools/recall.js.map +1 -0
  82. package/dist/tools/remember.d.ts +65 -0
  83. package/dist/tools/remember.d.ts.map +1 -0
  84. package/dist/tools/remember.js +147 -0
  85. package/dist/tools/remember.js.map +1 -0
  86. package/dist/worker/brain-worker.d.ts +100 -0
  87. package/dist/worker/brain-worker.d.ts.map +1 -0
  88. package/dist/worker/brain-worker.js +261 -0
  89. package/dist/worker/brain-worker.js.map +1 -0
  90. package/dist/worker/link-discovery.d.ts +47 -0
  91. package/dist/worker/link-discovery.d.ts.map +1 -0
  92. package/dist/worker/link-discovery.js +103 -0
  93. package/dist/worker/link-discovery.js.map +1 -0
  94. package/dist/worker/predictive-consolidation.d.ts +46 -0
  95. package/dist/worker/predictive-consolidation.d.ts.map +1 -0
  96. package/dist/worker/predictive-consolidation.js +110 -0
  97. package/dist/worker/predictive-consolidation.js.map +1 -0
  98. package/dist/worker/types.d.ts +91 -0
  99. package/dist/worker/types.d.ts.map +1 -0
  100. package/dist/worker/types.js +22 -0
  101. package/dist/worker/types.js.map +1 -0
  102. 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