shieldcortex 3.0.3 → 3.0.4

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 (91) hide show
  1. package/dashboard/.next/standalone/dashboard/.next/BUILD_ID +1 -1
  2. package/dashboard/.next/standalone/dashboard/.next/build-manifest.json +2 -2
  3. package/dashboard/.next/standalone/dashboard/.next/prerender-manifest.json +3 -3
  4. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.html +2 -2
  5. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.rsc +1 -1
  6. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  9. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  10. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  12. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.html +1 -1
  13. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.rsc +2 -2
  14. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  15. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  16. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  17. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  18. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  19. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  20. package/dashboard/.next/standalone/dashboard/.next/server/app/index.html +1 -1
  21. package/dashboard/.next/standalone/dashboard/.next/server/app/index.rsc +3 -3
  22. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  23. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_full.segment.rsc +3 -3
  24. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_head.segment.rsc +1 -1
  25. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_index.segment.rsc +2 -2
  26. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  27. package/dashboard/.next/standalone/dashboard/.next/server/app/page/react-loadable-manifest.json +1 -1
  28. package/dashboard/.next/standalone/dashboard/.next/server/app/page_client-reference-manifest.js +1 -1
  29. package/dashboard/.next/standalone/dashboard/.next/server/chunks/ssr/dashboard_3051539d._.js +1 -1
  30. package/dashboard/.next/standalone/dashboard/.next/server/pages/404.html +1 -1
  31. package/dashboard/.next/standalone/dashboard/.next/server/pages/500.html +2 -2
  32. package/dashboard/.next/standalone/dashboard/.next/server/server-reference-manifest.js +1 -1
  33. package/dashboard/.next/standalone/dashboard/.next/server/server-reference-manifest.json +1 -1
  34. package/dashboard/.next/standalone/dashboard/.next/static/chunks/313c0d327bbf244a.js +9 -0
  35. package/dashboard/.next/standalone/dashboard/.next/static/chunks/{fa5217550a8ab9a6.js → 49c1cec591af1460.js} +2 -2
  36. package/dashboard/.next/standalone/dashboard/.next/static/chunks/{f69fd1c5e71fbbfd.js → ca21f348cb163905.js} +1 -1
  37. package/dashboard/.next/standalone/dashboard/.next/static/chunks/f4ca424319f58dc7.css +3 -0
  38. package/dist/api/routes/admin.d.ts +12 -0
  39. package/dist/api/routes/admin.js +502 -0
  40. package/dist/api/routes/graph.d.ts +4 -0
  41. package/dist/api/routes/graph.js +333 -0
  42. package/dist/api/routes/incidents.d.ts +2 -0
  43. package/dist/api/routes/incidents.js +32 -0
  44. package/dist/api/routes/memories.d.ts +4 -0
  45. package/dist/api/routes/memories.js +659 -0
  46. package/dist/api/routes/recall.d.ts +4 -0
  47. package/dist/api/routes/recall.js +36 -0
  48. package/dist/api/routes/system.d.ts +9 -0
  49. package/dist/api/routes/system.js +201 -0
  50. package/dist/api/visualization-server.js +31 -1913
  51. package/dist/memory/search.d.ts +37 -0
  52. package/dist/memory/search.js +143 -0
  53. package/dist/memory/store.js +2 -166
  54. package/dist/tools/forget.d.ts +2 -2
  55. package/dist/tools/recall.d.ts +2 -2
  56. package/hooks/openclaw/cortex-memory/handler.ts +5 -141
  57. package/hooks/openclaw/cortex-memory/runtime.mjs +129 -0
  58. package/package.json +8 -4
  59. package/plugins/openclaw/dist/index.js +5 -39
  60. package/scripts/run-jest.mjs +25 -1
  61. package/dashboard/.next/standalone/dashboard/.next/static/chunks/be6970da20a17c0b.js +0 -9
  62. package/dashboard/.next/standalone/dashboard/.next/static/chunks/e63d2228780629dd.css +0 -3
  63. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/_tsc.js +0 -133818
  64. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/_tsserver.js +0 -659
  65. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/_typingsInstaller.js +0 -222
  66. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/cs/diagnosticMessages.generated.json +0 -2122
  67. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/de/diagnosticMessages.generated.json +0 -2122
  68. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/es/diagnosticMessages.generated.json +0 -2122
  69. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/fr/diagnosticMessages.generated.json +0 -2122
  70. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/it/diagnosticMessages.generated.json +0 -2122
  71. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/ja/diagnosticMessages.generated.json +0 -2122
  72. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/ko/diagnosticMessages.generated.json +0 -2122
  73. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/pl/diagnosticMessages.generated.json +0 -2122
  74. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/pt-br/diagnosticMessages.generated.json +0 -2122
  75. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/ru/diagnosticMessages.generated.json +0 -2122
  76. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/tr/diagnosticMessages.generated.json +0 -2122
  77. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/tsc.js +0 -8
  78. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/tsserver.js +0 -8
  79. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/tsserverlibrary.js +0 -21
  80. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/typesMap.json +0 -497
  81. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/typescript.js +0 -200276
  82. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/typingsInstaller.js +0 -8
  83. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/watchGuard.js +0 -53
  84. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/zh-cn/diagnosticMessages.generated.json +0 -2122
  85. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/zh-tw/diagnosticMessages.generated.json +0 -2122
  86. package/dashboard/.next/standalone/dashboard/node_modules/typescript/package.json +0 -120
  87. package/scripts/start-dashboard.sh +0 -41
  88. package/scripts/stop-dashboard.sh +0 -21
  89. /package/dashboard/.next/standalone/dashboard/.next/static/{THy6JENQ0c1sq6jQhvIDp → BEvyMAX62LQMyt5iSb-F9}/_buildManifest.js +0 -0
  90. /package/dashboard/.next/standalone/dashboard/.next/static/{THy6JENQ0c1sq6jQhvIDp → BEvyMAX62LQMyt5iSb-F9}/_clientMiddlewareManifest.json +0 -0
  91. /package/dashboard/.next/standalone/dashboard/.next/static/{THy6JENQ0c1sq6jQhvIDp → BEvyMAX62LQMyt5iSb-F9}/_ssgManifest.js +0 -0
@@ -0,0 +1,659 @@
1
+ import { getDatabase } from '../../database/init.js';
2
+ import { searchMemories, getRecentMemories, getHighPriorityMemories, getMemoryStats, getMemoryById, addMemory, deleteMemory, accessMemory, updateMemory, promoteMemory, createMemoryLink, rowToMemory, enrichMemory, } from '../../memory/store.js';
3
+ import { calculateDecayedScore } from '../../memory/decay.js';
4
+ import { consolidate, formatContextSummary, generateContextSummary, } from '../../memory/consolidate.js';
5
+ import { getActivationStats, getActiveMemories } from '../../memory/activation.js';
6
+ import { detectContradictions, getContradictionsFor } from '../../memory/contradiction.js';
7
+ import { emitConsolidation } from '../events.js';
8
+ export function registerMemoryRoutes(app, requireNotLocked) {
9
+ app.get('/api/memories', requireNotLocked, async (req, res) => {
10
+ try {
11
+ const project = typeof req.query.project === 'string' ? req.query.project : undefined;
12
+ const type = typeof req.query.type === 'string' ? req.query.type : undefined;
13
+ const category = typeof req.query.category === 'string' ? req.query.category : undefined;
14
+ const limitStr = typeof req.query.limit === 'string' ? req.query.limit : '50';
15
+ const offsetStr = typeof req.query.offset === 'string' ? req.query.offset : '0';
16
+ const mode = typeof req.query.mode === 'string' ? req.query.mode : 'recent';
17
+ const query = typeof req.query.query === 'string' ? req.query.query : undefined;
18
+ const limit = Math.min(parseInt(limitStr, 10) || 50, 1000);
19
+ const offset = parseInt(offsetStr, 10) || 0;
20
+ let memories;
21
+ if (mode === 'search' && query) {
22
+ const results = await searchMemories({
23
+ query,
24
+ project,
25
+ type: type,
26
+ category: category,
27
+ limit: limit + offset + 1,
28
+ });
29
+ memories = results.map((result) => result.memory);
30
+ }
31
+ else if (mode === 'important') {
32
+ memories = getHighPriorityMemories(limit + offset + 1, project);
33
+ }
34
+ else {
35
+ memories = getRecentMemories(limit + offset + 1, project);
36
+ }
37
+ if (type) {
38
+ memories = memories.filter((memory) => memory.type === type);
39
+ }
40
+ if (category) {
41
+ memories = memories.filter((memory) => memory.category === category);
42
+ }
43
+ const stats = getMemoryStats(project);
44
+ const total = stats.total;
45
+ const hasMore = memories.length > offset + limit;
46
+ const paginatedMemories = memories.slice(offset, offset + limit);
47
+ const memoriesWithDecay = paginatedMemories.map((memory) => ({
48
+ ...memory,
49
+ decayedScore: calculateDecayedScore(memory),
50
+ }));
51
+ res.json({
52
+ memories: memoriesWithDecay,
53
+ pagination: {
54
+ offset,
55
+ limit,
56
+ total,
57
+ hasMore,
58
+ },
59
+ });
60
+ }
61
+ catch (error) {
62
+ res.status(500).json({ error: error.message });
63
+ }
64
+ });
65
+ app.get('/api/memories/activity', requireNotLocked, (req, res) => {
66
+ try {
67
+ const project = typeof req.query.project === 'string' ? req.query.project : undefined;
68
+ const db = getDatabase();
69
+ const query = project
70
+ ? `SELECT date(created_at) as date, COUNT(*) as count
71
+ FROM memories WHERE project = ?
72
+ GROUP BY date(created_at)
73
+ ORDER BY date DESC
74
+ LIMIT 365`
75
+ : `SELECT date(created_at) as date, COUNT(*) as count
76
+ FROM memories
77
+ GROUP BY date(created_at)
78
+ ORDER BY date DESC
79
+ LIMIT 365`;
80
+ const rows = project
81
+ ? db.prepare(query).all(project)
82
+ : db.prepare(query).all();
83
+ res.json({ activity: rows });
84
+ }
85
+ catch (error) {
86
+ res.status(500).json({ error: error.message });
87
+ }
88
+ });
89
+ app.get('/api/memories/quality', requireNotLocked, (req, res) => {
90
+ try {
91
+ const project = typeof req.query.project === 'string' ? req.query.project : undefined;
92
+ const db = getDatabase();
93
+ const projectFilter = project ? 'AND project = ?' : '';
94
+ const params = project ? [project] : [];
95
+ const neverAccessed = db.prepare(`
96
+ SELECT id, title, category, type, created_at, salience
97
+ FROM memories WHERE access_count = 0 ${projectFilter}
98
+ AND created_at < datetime('now', '-1 day')
99
+ ORDER BY created_at DESC LIMIT 50
100
+ `).all(...params);
101
+ const stale = db.prepare(`
102
+ SELECT id, title, category, type, last_accessed, decayed_score, salience
103
+ FROM memories WHERE decayed_score < 0.3 ${projectFilter}
104
+ AND last_accessed < datetime('now', '-30 days')
105
+ ORDER BY decayed_score ASC LIMIT 50
106
+ `).all(...params);
107
+ const duplicates = db.prepare(`
108
+ SELECT m1.id as id1, m1.title as title_a, m2.id as id2, m2.title as title_b
109
+ FROM memories m1
110
+ JOIN memories m2 ON m1.title = m2.title AND m1.id < m2.id
111
+ ${project ? 'WHERE m1.project = ?' : ''}
112
+ LIMIT 50
113
+ `).all(...params);
114
+ res.json({
115
+ neverAccessed: { count: neverAccessed.length, items: neverAccessed },
116
+ stale: { count: stale.length, items: stale },
117
+ duplicates: { count: duplicates.length, items: duplicates },
118
+ });
119
+ }
120
+ catch (error) {
121
+ res.status(500).json({ error: error.message });
122
+ }
123
+ });
124
+ app.get('/api/memories/:id', requireNotLocked, (req, res) => {
125
+ try {
126
+ const id = parseInt(req.params.id, 10);
127
+ const memory = getMemoryById(id);
128
+ if (!memory) {
129
+ return res.status(404).json({ error: 'Memory not found' });
130
+ }
131
+ res.json({
132
+ ...memory,
133
+ decayedScore: calculateDecayedScore(memory),
134
+ });
135
+ }
136
+ catch (error) {
137
+ res.status(500).json({ error: error.message });
138
+ }
139
+ });
140
+ app.post('/api/memories', requireNotLocked, (req, res) => {
141
+ try {
142
+ const { title, content, type, category, project, tags, salience } = req.body;
143
+ if (!title || !content) {
144
+ return res.status(400).json({ error: 'Title and content required' });
145
+ }
146
+ const memory = addMemory({
147
+ title,
148
+ content,
149
+ type: type || 'short_term',
150
+ category: category || 'note',
151
+ project,
152
+ tags: tags || [],
153
+ salience,
154
+ });
155
+ res.status(201).json(memory);
156
+ }
157
+ catch (error) {
158
+ if (error.name === 'MemoryPausedError') {
159
+ return res.status(503).json({
160
+ error: 'Memory creation is paused',
161
+ paused: true,
162
+ message: 'Use the dashboard control panel to resume memory creation.',
163
+ });
164
+ }
165
+ res.status(500).json({ error: error.message });
166
+ }
167
+ });
168
+ app.delete('/api/memories/:id', requireNotLocked, (req, res) => {
169
+ try {
170
+ const id = parseInt(req.params.id, 10);
171
+ const success = deleteMemory(id);
172
+ if (!success) {
173
+ return res.status(404).json({ error: 'Memory not found' });
174
+ }
175
+ res.json({ success: true });
176
+ }
177
+ catch (error) {
178
+ res.status(500).json({ error: error.message });
179
+ }
180
+ });
181
+ app.post('/api/memories/:id/access', requireNotLocked, (req, res) => {
182
+ try {
183
+ const id = parseInt(req.params.id, 10);
184
+ const memory = accessMemory(id);
185
+ if (!memory) {
186
+ return res.status(404).json({ error: 'Memory not found' });
187
+ }
188
+ res.json({
189
+ ...memory,
190
+ decayedScore: calculateDecayedScore(memory),
191
+ });
192
+ }
193
+ catch (error) {
194
+ res.status(500).json({ error: error.message });
195
+ }
196
+ });
197
+ app.get('/api/stats', (req, res) => {
198
+ try {
199
+ const project = typeof req.query.project === 'string' ? req.query.project : undefined;
200
+ const stats = getMemoryStats(project);
201
+ const db = getDatabase();
202
+ const rawRows = db.prepare(project
203
+ ? 'SELECT * FROM memories WHERE project = ?'
204
+ : 'SELECT * FROM memories').all(project ? [project] : []);
205
+ const allMemories = rawRows.map(rowToMemory);
206
+ const decayDistribution = {
207
+ healthy: 0,
208
+ fading: 0,
209
+ critical: 0,
210
+ };
211
+ for (const memory of allMemories) {
212
+ const score = calculateDecayedScore(memory);
213
+ if (score > 0.35)
214
+ decayDistribution.healthy++;
215
+ else if (score > 0.2)
216
+ decayDistribution.fading++;
217
+ else
218
+ decayDistribution.critical++;
219
+ }
220
+ const activation = getActivationStats();
221
+ res.json({
222
+ ...stats,
223
+ decayDistribution,
224
+ activation,
225
+ timestamp: new Date().toISOString(),
226
+ });
227
+ }
228
+ catch (error) {
229
+ res.status(500).json({ error: error.message });
230
+ }
231
+ });
232
+ app.get('/api/health-score', requireNotLocked, (_req, res) => {
233
+ try {
234
+ const db = getDatabase();
235
+ const totalCount = db.prepare('SELECT COUNT(*) as count FROM memories').get().count;
236
+ const freshCount = db.prepare('SELECT COUNT(*) as count FROM memories WHERE decayed_score > 0.3').get().count;
237
+ const freshnessScore = totalCount > 0 ? Math.round((freshCount / totalCount) * 100) : 100;
238
+ const freshPct = totalCount > 0 ? Math.round((freshCount / totalCount) * 100) : 100;
239
+ const linkedCount = db.prepare('SELECT COUNT(DISTINCT memory_id) as count FROM memory_entities').get().count;
240
+ const coverageScore = totalCount > 0 ? Math.round((linkedCount / totalCount) * 100) : 0;
241
+ const contradictionCount = db.prepare("SELECT COUNT(*) as count FROM memory_links WHERE relationship = 'contradicts'").get().count;
242
+ const consistencyScore = Math.max(0, 100 - (contradictionCount * 10));
243
+ const lastConsolidated = db.prepare("SELECT created_at FROM memories WHERE type = 'long_term' AND tags LIKE '%auto-consolidated%' ORDER BY created_at DESC LIMIT 1").get();
244
+ let consolidationScore = 25;
245
+ if (lastConsolidated) {
246
+ const hoursAgo = (Date.now() - new Date(lastConsolidated.created_at).getTime()) / (1000 * 60 * 60);
247
+ if (hoursAgo <= 4)
248
+ consolidationScore = 100;
249
+ else if (hoursAgo <= 8)
250
+ consolidationScore = 75;
251
+ else if (hoursAgo <= 24)
252
+ consolidationScore = 50;
253
+ }
254
+ const overall = Math.round(freshnessScore * 0.3 +
255
+ coverageScore * 0.25 +
256
+ consistencyScore * 0.25 +
257
+ consolidationScore * 0.2);
258
+ let consolidationDetail = 'No consolidated memories found';
259
+ if (lastConsolidated) {
260
+ const hoursAgo = (Date.now() - new Date(lastConsolidated.created_at).getTime()) / (1000 * 60 * 60);
261
+ consolidationDetail = hoursAgo < 1
262
+ ? 'Last consolidated less than 1 hour ago'
263
+ : `Last consolidated ${Math.round(hoursAgo)} hours ago`;
264
+ }
265
+ res.json({
266
+ overall,
267
+ components: {
268
+ freshness: {
269
+ score: freshnessScore,
270
+ label: 'Memory Freshness',
271
+ detail: `${freshPct}% of memories above decay threshold`,
272
+ },
273
+ coverage: {
274
+ score: coverageScore,
275
+ label: 'Graph Coverage',
276
+ detail: `${coverageScore}% of memories have entity links`,
277
+ },
278
+ consistency: {
279
+ score: consistencyScore,
280
+ label: 'Consistency',
281
+ detail: `${contradictionCount} contradictions detected`,
282
+ },
283
+ consolidation: {
284
+ score: consolidationScore,
285
+ label: 'Consolidation',
286
+ detail: consolidationDetail,
287
+ },
288
+ },
289
+ });
290
+ }
291
+ catch (error) {
292
+ res.status(500).json({ error: error.message });
293
+ }
294
+ });
295
+ app.get('/api/activation', requireNotLocked, (_req, res) => {
296
+ try {
297
+ res.json({
298
+ activeMemories: getActiveMemories(),
299
+ stats: getActivationStats(),
300
+ timestamp: new Date().toISOString(),
301
+ });
302
+ }
303
+ catch (error) {
304
+ res.status(500).json({ error: error.message });
305
+ }
306
+ });
307
+ app.get('/api/contradictions', requireNotLocked, (req, res) => {
308
+ try {
309
+ const project = typeof req.query.project === 'string' ? req.query.project : undefined;
310
+ const category = typeof req.query.category === 'string' ? req.query.category : undefined;
311
+ const minScoreStr = typeof req.query.minScore === 'string' ? req.query.minScore : '0.4';
312
+ const limitStr = typeof req.query.limit === 'string' ? req.query.limit : '20';
313
+ const contradictions = detectContradictions({
314
+ project,
315
+ category: category,
316
+ minScore: parseFloat(minScoreStr) || 0.4,
317
+ limit: parseInt(limitStr, 10) || 20,
318
+ });
319
+ res.json({
320
+ contradictions: contradictions.map((item) => ({
321
+ memoryAId: item.memoryA.id,
322
+ memoryATitle: item.memoryA.title,
323
+ memoryBId: item.memoryB.id,
324
+ memoryBTitle: item.memoryB.title,
325
+ score: item.score,
326
+ reason: item.reason,
327
+ sharedTopics: item.sharedTopics,
328
+ })),
329
+ count: contradictions.length,
330
+ timestamp: new Date().toISOString(),
331
+ });
332
+ }
333
+ catch (error) {
334
+ res.status(500).json({ error: error.message });
335
+ }
336
+ });
337
+ app.get('/api/memories/:id/contradictions', requireNotLocked, (req, res) => {
338
+ try {
339
+ const id = parseInt(req.params.id, 10);
340
+ if (Number.isNaN(id)) {
341
+ return res.status(400).json({ error: 'Invalid memory ID' });
342
+ }
343
+ const contradictions = getContradictionsFor(id);
344
+ res.json({
345
+ memoryId: id,
346
+ contradictions: contradictions.map((item) => ({
347
+ contradictingMemoryId: item.memoryB.id,
348
+ contradictingMemoryTitle: item.memoryB.title,
349
+ score: item.score,
350
+ reason: item.reason,
351
+ sharedTopics: item.sharedTopics,
352
+ })),
353
+ count: contradictions.length,
354
+ });
355
+ }
356
+ catch (error) {
357
+ res.status(500).json({ error: error.message });
358
+ }
359
+ });
360
+ app.post('/api/memories/:id/enrich', requireNotLocked, (req, res) => {
361
+ try {
362
+ const id = parseInt(req.params.id, 10);
363
+ if (Number.isNaN(id)) {
364
+ return res.status(400).json({ error: 'Invalid memory ID' });
365
+ }
366
+ const { context, contextType } = req.body;
367
+ if (!context || typeof context !== 'string') {
368
+ return res.status(400).json({ error: 'Context string required in request body' });
369
+ }
370
+ const validTypes = ['search', 'access', 'related'];
371
+ const type = validTypes.includes(contextType) ? contextType : 'access';
372
+ res.json(enrichMemory(id, context, type));
373
+ }
374
+ catch (error) {
375
+ res.status(500).json({ error: error.message });
376
+ }
377
+ });
378
+ app.get('/api/projects', (_req, res) => {
379
+ try {
380
+ const db = getDatabase();
381
+ const projects = db.prepare(`
382
+ SELECT DISTINCT project, COUNT(*) as memory_count
383
+ FROM memories
384
+ WHERE project IS NOT NULL AND project != ''
385
+ GROUP BY project
386
+ ORDER BY memory_count DESC
387
+ `).all();
388
+ const totalCount = db.prepare('SELECT COUNT(*) as count FROM memories').get();
389
+ res.json({
390
+ projects: [
391
+ { project: null, memory_count: totalCount.count, label: 'All Projects' },
392
+ ...projects.map((project) => ({ ...project, label: project.project })),
393
+ ],
394
+ });
395
+ }
396
+ catch (error) {
397
+ res.status(500).json({ error: error.message });
398
+ }
399
+ });
400
+ app.get('/api/links', requireNotLocked, (req, res) => {
401
+ try {
402
+ const project = typeof req.query.project === 'string' ? req.query.project : undefined;
403
+ const db = getDatabase();
404
+ const query = project
405
+ ? `
406
+ SELECT
407
+ ml.*,
408
+ m1.title as source_title,
409
+ m1.category as source_category,
410
+ m1.type as source_type,
411
+ m2.title as target_title,
412
+ m2.category as target_category,
413
+ m2.type as target_type
414
+ FROM memory_links ml
415
+ JOIN memories m1 ON ml.source_id = m1.id
416
+ JOIN memories m2 ON ml.target_id = m2.id
417
+ WHERE m1.project = ? OR m2.project = ?
418
+ ORDER BY ml.created_at DESC
419
+ LIMIT 500
420
+ `
421
+ : `
422
+ SELECT
423
+ ml.*,
424
+ m1.title as source_title,
425
+ m1.category as source_category,
426
+ m1.type as source_type,
427
+ m2.title as target_title,
428
+ m2.category as target_category,
429
+ m2.type as target_type
430
+ FROM memory_links ml
431
+ JOIN memories m1 ON ml.source_id = m1.id
432
+ JOIN memories m2 ON ml.target_id = m2.id
433
+ ORDER BY ml.created_at DESC
434
+ LIMIT 500
435
+ `;
436
+ const links = project
437
+ ? db.prepare(query).all(project, project)
438
+ : db.prepare(query).all();
439
+ res.json(links);
440
+ }
441
+ catch (error) {
442
+ res.status(500).json({ error: error.message });
443
+ }
444
+ });
445
+ app.post('/api/consolidate', requireNotLocked, (_req, res) => {
446
+ try {
447
+ const result = consolidate();
448
+ emitConsolidation(result);
449
+ res.json({ success: true, ...result });
450
+ }
451
+ catch (error) {
452
+ res.status(500).json({ error: error.message });
453
+ }
454
+ });
455
+ app.get('/api/context', requireNotLocked, async (req, res) => {
456
+ try {
457
+ const project = typeof req.query.project === 'string' ? req.query.project : undefined;
458
+ const summary = await generateContextSummary(project);
459
+ res.json({
460
+ summary,
461
+ formatted: formatContextSummary(summary),
462
+ });
463
+ }
464
+ catch (error) {
465
+ res.status(500).json({ error: error.message });
466
+ }
467
+ });
468
+ app.get('/api/suggestions', requireNotLocked, (req, res) => {
469
+ try {
470
+ const query = typeof req.query.q === 'string' ? req.query.q : '';
471
+ const limit = typeof req.query.limit === 'string' ? parseInt(req.query.limit, 10) : 10;
472
+ if (!query || query.length < 2) {
473
+ return res.json({ suggestions: [] });
474
+ }
475
+ const db = getDatabase();
476
+ const suggestions = [];
477
+ const titleMatches = db.prepare(`
478
+ SELECT DISTINCT title, COUNT(*) as count
479
+ FROM memories
480
+ WHERE title LIKE ?
481
+ GROUP BY title
482
+ ORDER BY count DESC, last_accessed DESC
483
+ LIMIT ?
484
+ `).all(`%${query}%`, limit);
485
+ for (const match of titleMatches) {
486
+ suggestions.push({ text: match.title, type: 'title', count: match.count });
487
+ }
488
+ const categoryMatches = db.prepare(`
489
+ SELECT DISTINCT category, COUNT(*) as count
490
+ FROM memories
491
+ WHERE category LIKE ?
492
+ GROUP BY category
493
+ ORDER BY count DESC
494
+ LIMIT 5
495
+ `).all(`%${query}%`);
496
+ for (const match of categoryMatches) {
497
+ suggestions.push({ text: match.category, type: 'category', count: match.count });
498
+ }
499
+ const projectMatches = db.prepare(`
500
+ SELECT DISTINCT project, COUNT(*) as count
501
+ FROM memories
502
+ WHERE project IS NOT NULL AND project LIKE ?
503
+ GROUP BY project
504
+ ORDER BY count DESC
505
+ LIMIT 5
506
+ `).all(`%${query}%`);
507
+ for (const match of projectMatches) {
508
+ suggestions.push({ text: match.project, type: 'project', count: match.count });
509
+ }
510
+ const tagRows = db.prepare(`
511
+ SELECT tags FROM memories
512
+ WHERE tags IS NOT NULL AND tags != '[]'
513
+ LIMIT 500
514
+ `).all();
515
+ const tagCounts = new Map();
516
+ for (const row of tagRows) {
517
+ try {
518
+ const tags = JSON.parse(row.tags);
519
+ for (const tag of tags) {
520
+ if (tag.toLowerCase().includes(query.toLowerCase())) {
521
+ tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
522
+ }
523
+ }
524
+ }
525
+ catch {
526
+ // Ignore malformed tag rows.
527
+ }
528
+ }
529
+ const tagMatches = Array.from(tagCounts.entries())
530
+ .map(([text, count]) => ({ text, type: 'tag', count }))
531
+ .sort((a, b) => b.count - a.count)
532
+ .slice(0, 5);
533
+ suggestions.push(...tagMatches);
534
+ const dedupedSuggestions = suggestions.filter((suggestion, index, all) => index === all.findIndex((other) => other.text.toLowerCase() === suggestion.text.toLowerCase() && other.type === suggestion.type));
535
+ const limitedSuggestions = dedupedSuggestions
536
+ .sort((a, b) => b.count - a.count)
537
+ .slice(0, limit);
538
+ res.json({ suggestions: limitedSuggestions });
539
+ }
540
+ catch (error) {
541
+ res.status(500).json({ error: error.message });
542
+ }
543
+ });
544
+ app.post('/api/memories/:id/boost', requireNotLocked, (req, res) => {
545
+ try {
546
+ const id = parseInt(req.params.id, 10);
547
+ const memory = getMemoryById(id);
548
+ if (!memory) {
549
+ return res.status(404).json({ error: 'Memory not found' });
550
+ }
551
+ const updated = updateMemory(id, { salience: Math.min(1.0, (memory.salience ?? 0.5) + 0.15) });
552
+ res.json(updated);
553
+ }
554
+ catch (error) {
555
+ res.status(500).json({ error: error.message });
556
+ }
557
+ });
558
+ app.post('/api/memories/:id/demote', requireNotLocked, (req, res) => {
559
+ try {
560
+ const id = parseInt(req.params.id, 10);
561
+ const memory = getMemoryById(id);
562
+ if (!memory) {
563
+ return res.status(404).json({ error: 'Memory not found' });
564
+ }
565
+ const updated = updateMemory(id, { salience: Math.max(0.05, (memory.salience ?? 0.5) - 0.15) });
566
+ res.json(updated);
567
+ }
568
+ catch (error) {
569
+ res.status(500).json({ error: error.message });
570
+ }
571
+ });
572
+ app.post('/api/memories/:id/promote', requireNotLocked, (req, res) => {
573
+ try {
574
+ const id = parseInt(req.params.id, 10);
575
+ const memory = promoteMemory(id);
576
+ if (!memory) {
577
+ return res.status(404).json({ error: 'Memory not found' });
578
+ }
579
+ res.json(memory);
580
+ }
581
+ catch (error) {
582
+ res.status(500).json({ error: error.message });
583
+ }
584
+ });
585
+ app.patch('/api/memories/:id', requireNotLocked, (req, res) => {
586
+ try {
587
+ const id = parseInt(req.params.id, 10);
588
+ const { title, content, category, tags, importance } = req.body;
589
+ if (title !== undefined && (typeof title !== 'string' || title.trim().length === 0)) {
590
+ return res.status(400).json({ error: 'Title must be a non-empty string' });
591
+ }
592
+ if (content !== undefined && typeof content !== 'string') {
593
+ return res.status(400).json({ error: 'Content must be a string' });
594
+ }
595
+ const validCategories = ['architecture', 'pattern', 'preference', 'error', 'context', 'learning', 'todo', 'note', 'relationship', 'custom'];
596
+ if (category !== undefined && !validCategories.includes(category)) {
597
+ return res.status(400).json({ error: `Category must be one of: ${validCategories.join(', ')}` });
598
+ }
599
+ if (tags !== undefined && (!Array.isArray(tags) || !tags.every((tag) => typeof tag === 'string'))) {
600
+ return res.status(400).json({ error: 'Tags must be an array of strings' });
601
+ }
602
+ if (importance !== undefined && (typeof importance !== 'number' || importance < 0 || importance > 1)) {
603
+ return res.status(400).json({ error: 'Importance must be a number between 0 and 1' });
604
+ }
605
+ const updates = {};
606
+ if (title !== undefined)
607
+ updates.title = title.trim();
608
+ if (content !== undefined)
609
+ updates.content = content;
610
+ if (category !== undefined)
611
+ updates.category = category;
612
+ if (tags !== undefined)
613
+ updates.tags = tags;
614
+ if (importance !== undefined)
615
+ updates.salience = importance;
616
+ const updated = updateMemory(id, updates);
617
+ if (!updated) {
618
+ return res.status(404).json({ error: 'Memory not found' });
619
+ }
620
+ res.json(updated);
621
+ }
622
+ catch (error) {
623
+ res.status(500).json({ error: error.message });
624
+ }
625
+ });
626
+ app.post('/api/memories/:id/quarantine', requireNotLocked, (req, res) => {
627
+ try {
628
+ const id = parseInt(req.params.id, 10);
629
+ const memory = getMemoryById(id);
630
+ if (!memory) {
631
+ return res.status(404).json({ error: 'Memory not found' });
632
+ }
633
+ const db = getDatabase();
634
+ db.prepare(`INSERT INTO quarantine (original_title, original_content, source_type, source_identifier, reason, project, status, created_at)
635
+ VALUES (?, ?, ?, ?, ?, ?, 'pending', ?)`).run(memory.title, memory.content, 'dashboard', 'brain-control', req.body.reason || 'Manually quarantined from Brain dashboard', memory.project || null, new Date().toISOString());
636
+ deleteMemory(id);
637
+ res.json({ success: true, quarantined: id });
638
+ }
639
+ catch (error) {
640
+ res.status(500).json({ error: error.message });
641
+ }
642
+ });
643
+ app.post('/api/links', requireNotLocked, (req, res) => {
644
+ try {
645
+ const { sourceId, targetId, relationship, strength } = req.body;
646
+ if (!sourceId || !targetId || !relationship) {
647
+ return res.status(400).json({ error: 'sourceId, targetId, and relationship are required' });
648
+ }
649
+ const link = createMemoryLink(sourceId, targetId, relationship, strength ?? 0.5);
650
+ if (!link) {
651
+ return res.status(404).json({ error: 'One or both memories not found, or self-link attempted' });
652
+ }
653
+ res.json(link);
654
+ }
655
+ catch (error) {
656
+ res.status(500).json({ error: error.message });
657
+ }
658
+ });
659
+ }
@@ -0,0 +1,4 @@
1
+ import type { Express, Request, Response } from 'express';
2
+ type Middleware = (_req: Request, res: Response, next: (err?: unknown) => void) => void;
3
+ export declare function registerRecallRoutes(app: Express, requireNotLocked: Middleware): void;
4
+ export {};