shieldcortex 3.0.3 → 3.1.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 (135) hide show
  1. package/README.md +5 -2
  2. package/dashboard/.next/standalone/dashboard/.next/BUILD_ID +1 -1
  3. package/dashboard/.next/standalone/dashboard/.next/build-manifest.json +2 -2
  4. package/dashboard/.next/standalone/dashboard/.next/prerender-manifest.json +3 -3
  5. package/dashboard/.next/standalone/dashboard/.next/required-server-files.json +4 -4
  6. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.html +2 -2
  7. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.rsc +1 -1
  8. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  11. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  12. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.html +1 -1
  15. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.rsc +2 -2
  16. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  17. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  18. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  19. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  20. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  21. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  22. package/dashboard/.next/standalone/dashboard/.next/server/app/index.html +1 -1
  23. package/dashboard/.next/standalone/dashboard/.next/server/app/index.rsc +3 -3
  24. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  25. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_full.segment.rsc +3 -3
  26. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_head.segment.rsc +1 -1
  27. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_index.segment.rsc +2 -2
  28. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  29. package/dashboard/.next/standalone/dashboard/.next/server/app/page/react-loadable-manifest.json +1 -1
  30. package/dashboard/.next/standalone/dashboard/.next/server/app/page_client-reference-manifest.js +1 -1
  31. package/dashboard/.next/standalone/dashboard/.next/server/chunks/ssr/dashboard_3051539d._.js +1 -1
  32. package/dashboard/.next/standalone/dashboard/.next/server/pages/404.html +1 -1
  33. package/dashboard/.next/standalone/dashboard/.next/server/pages/500.html +2 -2
  34. package/dashboard/.next/standalone/dashboard/.next/server/server-reference-manifest.js +1 -1
  35. package/dashboard/.next/standalone/dashboard/.next/server/server-reference-manifest.json +1 -1
  36. package/dashboard/.next/standalone/dashboard/.next/static/chunks/0a69eb25d08447ee.js +1 -0
  37. package/dashboard/.next/standalone/dashboard/.next/static/chunks/9232a2d99b47b21f.js +3 -0
  38. package/dashboard/.next/standalone/dashboard/.next/static/chunks/97537d3db46c8467.css +3 -0
  39. package/dashboard/.next/standalone/dashboard/.next/static/chunks/aa6e9b8a52353969.js +9 -0
  40. package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-darwin-arm64/lib/sharp-darwin-arm64.node +0 -0
  41. package/dashboard/.next/standalone/{node_modules/@img/sharp-linux-x64 → dashboard/node_modules/@img/sharp-darwin-arm64}/package.json +7 -13
  42. package/dashboard/.next/standalone/dashboard/node_modules/@img/{sharp-libvips-linux-x64 → sharp-libvips-darwin-arm64}/README.md +2 -2
  43. package/dashboard/.next/standalone/dashboard/node_modules/@img/{sharp-libvips-linux-x64 → sharp-libvips-darwin-arm64}/lib/glib-2.0/include/glibconfig.h +8 -9
  44. package/dashboard/.next/standalone/dashboard/node_modules/@img/{sharp-libvips-linux-x64/lib/libvips-cpp.so.8.17.3 → sharp-libvips-darwin-arm64/lib/libvips-cpp.8.17.3.dylib} +0 -0
  45. package/dashboard/.next/standalone/dashboard/node_modules/@img/{sharp-libvips-linux-x64 → sharp-libvips-darwin-arm64}/package.json +5 -11
  46. package/dashboard/.next/standalone/dashboard/server.js +1 -1
  47. package/dashboard/.next/standalone/{dashboard/node_modules/@img/sharp-linux-x64 → node_modules/@img/sharp-darwin-arm64}/package.json +7 -13
  48. package/dashboard/.next/standalone/node_modules/@img/{sharp-libvips-linux-x64 → sharp-libvips-darwin-arm64}/package.json +5 -11
  49. package/dist/api/routes/admin.d.ts +12 -0
  50. package/dist/api/routes/admin.js +502 -0
  51. package/dist/api/routes/graph.d.ts +4 -0
  52. package/dist/api/routes/graph.js +333 -0
  53. package/dist/api/routes/incidents.d.ts +2 -0
  54. package/dist/api/routes/incidents.js +32 -0
  55. package/dist/api/routes/memories.d.ts +4 -0
  56. package/dist/api/routes/memories.js +659 -0
  57. package/dist/api/routes/recall.d.ts +4 -0
  58. package/dist/api/routes/recall.js +36 -0
  59. package/dist/api/routes/system.d.ts +9 -0
  60. package/dist/api/routes/system.js +266 -0
  61. package/dist/api/visualization-server.js +31 -1913
  62. package/dist/cloud/cli.d.ts +1 -0
  63. package/dist/cloud/cli.js +40 -0
  64. package/dist/cloud/config.d.ts +10 -0
  65. package/dist/cloud/config.js +54 -0
  66. package/dist/cloud/graph-sync.d.ts +45 -0
  67. package/dist/cloud/graph-sync.js +257 -0
  68. package/dist/cloud/memory-sync.d.ts +36 -0
  69. package/dist/cloud/memory-sync.js +183 -0
  70. package/dist/cloud/sync-queue.d.ts +24 -0
  71. package/dist/cloud/sync-queue.js +126 -7
  72. package/dist/database/init.js +24 -0
  73. package/dist/graph/backfill.js +3 -5
  74. package/dist/graph/resolve.d.ts +10 -0
  75. package/dist/graph/resolve.js +63 -1
  76. package/dist/index.d.ts +2 -0
  77. package/dist/index.js +61 -4
  78. package/dist/memory/search.d.ts +37 -0
  79. package/dist/memory/search.js +143 -0
  80. package/dist/memory/store.js +47 -171
  81. package/dist/memory/types.d.ts +2 -0
  82. package/dist/service/install.d.ts +1 -0
  83. package/dist/service/install.js +43 -1
  84. package/dist/tools/recall.d.ts +1 -1
  85. package/hooks/openclaw/cortex-memory/handler.ts +5 -141
  86. package/hooks/openclaw/cortex-memory/runtime.mjs +129 -0
  87. package/package.json +8 -4
  88. package/plugins/openclaw/dist/index.js +5 -39
  89. package/scripts/run-jest.mjs +25 -1
  90. package/dashboard/.next/standalone/dashboard/.next/static/chunks/be6970da20a17c0b.js +0 -9
  91. package/dashboard/.next/standalone/dashboard/.next/static/chunks/e63d2228780629dd.css +0 -3
  92. package/dashboard/.next/standalone/dashboard/.next/static/chunks/f69fd1c5e71fbbfd.js +0 -1
  93. package/dashboard/.next/standalone/dashboard/.next/static/chunks/fa5217550a8ab9a6.js +0 -3
  94. package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-libvips-linuxmusl-x64/README.md +0 -46
  95. package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/glib-2.0/include/glibconfig.h +0 -221
  96. package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/index.js +0 -1
  97. package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/libvips-cpp.so.8.17.3 +0 -0
  98. package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json +0 -42
  99. package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-libvips-linuxmusl-x64/versions.json +0 -30
  100. package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-linux-x64/lib/sharp-linux-x64.node +0 -0
  101. package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-linuxmusl-x64/lib/sharp-linuxmusl-x64.node +0 -0
  102. package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-linuxmusl-x64/package.json +0 -46
  103. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/_tsc.js +0 -133818
  104. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/_tsserver.js +0 -659
  105. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/_typingsInstaller.js +0 -222
  106. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/cs/diagnosticMessages.generated.json +0 -2122
  107. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/de/diagnosticMessages.generated.json +0 -2122
  108. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/es/diagnosticMessages.generated.json +0 -2122
  109. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/fr/diagnosticMessages.generated.json +0 -2122
  110. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/it/diagnosticMessages.generated.json +0 -2122
  111. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/ja/diagnosticMessages.generated.json +0 -2122
  112. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/ko/diagnosticMessages.generated.json +0 -2122
  113. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/pl/diagnosticMessages.generated.json +0 -2122
  114. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/pt-br/diagnosticMessages.generated.json +0 -2122
  115. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/ru/diagnosticMessages.generated.json +0 -2122
  116. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/tr/diagnosticMessages.generated.json +0 -2122
  117. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/tsc.js +0 -8
  118. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/tsserver.js +0 -8
  119. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/tsserverlibrary.js +0 -21
  120. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/typesMap.json +0 -497
  121. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/typescript.js +0 -200276
  122. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/typingsInstaller.js +0 -8
  123. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/watchGuard.js +0 -53
  124. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/zh-cn/diagnosticMessages.generated.json +0 -2122
  125. package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/zh-tw/diagnosticMessages.generated.json +0 -2122
  126. package/dashboard/.next/standalone/dashboard/node_modules/typescript/package.json +0 -120
  127. package/dashboard/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json +0 -42
  128. package/dashboard/.next/standalone/node_modules/@img/sharp-linuxmusl-x64/package.json +0 -46
  129. package/scripts/start-dashboard.sh +0 -41
  130. package/scripts/stop-dashboard.sh +0 -21
  131. /package/dashboard/.next/standalone/dashboard/.next/static/{THy6JENQ0c1sq6jQhvIDp → RnvqrTXo_jN8SuMdaNcIj}/_buildManifest.js +0 -0
  132. /package/dashboard/.next/standalone/dashboard/.next/static/{THy6JENQ0c1sq6jQhvIDp → RnvqrTXo_jN8SuMdaNcIj}/_clientMiddlewareManifest.json +0 -0
  133. /package/dashboard/.next/standalone/dashboard/.next/static/{THy6JENQ0c1sq6jQhvIDp → RnvqrTXo_jN8SuMdaNcIj}/_ssgManifest.js +0 -0
  134. /package/dashboard/.next/standalone/dashboard/node_modules/@img/{sharp-libvips-linux-x64 → sharp-libvips-darwin-arm64}/lib/index.js +0 -0
  135. /package/dashboard/.next/standalone/dashboard/node_modules/@img/{sharp-libvips-linux-x64 → sharp-libvips-darwin-arm64}/versions.json +0 -0
@@ -13,27 +13,26 @@ import { generateSessionToken, cleanupSessionToken, validateSessionToken, getSes
13
13
  import { WebSocketServer, WebSocket } from 'ws';
14
14
  import { getDatabase, initDatabase, checkpointWal } from '../database/init.js';
15
15
  import { DEFAULT_CONFIG } from '../memory/types.js';
16
- import { searchMemories, searchMemoriesExplained, getRecentMemories, getHighPriorityMemories, getMemoryStats, getMemoryById, addMemory, deleteMemory, accessMemory, updateDecayScores, updateMemory, promoteMemory, createMemoryLink, rowToMemory, } from '../memory/store.js';
17
- import { consolidate, generateContextSummary, formatContextSummary, } from '../memory/consolidate.js';
16
+ import { getRecentMemories, getMemoryStats, rowToMemory, updateDecayScores } from '../memory/store.js';
18
17
  import { calculateDecayedScore } from '../memory/decay.js';
19
- import { getActivationStats, getActiveMemories } from '../memory/activation.js';
20
- import { detectContradictions, getContradictionsFor } from '../memory/contradiction.js';
21
- import { enrichMemory } from '../memory/store.js';
22
- import { memoryEvents, emitDecayTick, emitConsolidation, getUnprocessedEvents, markEventsProcessed, cleanupOldEvents, } from './events.js';
18
+ import { memoryEvents, emitDecayTick, getUnprocessedEvents, markEventsProcessed, cleanupOldEvents, } from './events.js';
23
19
  import { BrainWorker } from '../worker/brain-worker.js';
24
- import { pause, resume, getControlStatus, isKillSwitchActive, getKillSwitchMeta, activateKillSwitch, deactivateKillSwitch } from './control.js';
25
- import { getCurrentVersion, getRunningVersion, checkForUpdates, performUpdate, scheduleRestart } from './version.js';
20
+ import { isKillSwitchActive, getKillSwitchMeta, activateKillSwitch, deactivateKillSwitch } from './control.js';
21
+ import { getRunningVersion } from './version.js';
26
22
  import { runDefencePipeline } from '../defence/pipeline.js';
27
23
  import { DEFAULT_DEFENCE_CONFIG } from '../defence/types.js';
28
- import { queryAuditLogs, getAuditStats, queryAgentRegistry, queryAgentTimeline, queryAgentOperations, queryIncidentReplay } from '../defence/audit/queries.js';
24
+ import { queryAgentOperations } from '../defence/audit/queries.js';
29
25
  import { logAudit } from '../defence/audit/index.js';
30
- import { getCloudConfig, setCloudConfig, readRawConfig, getTrustedSkills, addTrustedSkill, removeTrustedSkill, getDeviceId, getDeviceName, getDefenceMode, setDefenceMode, isConfigTampered, getOpenClawMemoryConfig, setOpenClawMemoryConfig } from '../cloud/config.js';
31
- import { getQueueStats } from '../cloud/sync-queue.js';
26
+ import { getCloudConfig, getTrustedSkills, addTrustedSkill, removeTrustedSkill, getDeviceId, getDeviceName, getDefenceMode } from '../cloud/config.js';
32
27
  import { scanSkill, scanSkillContent, discoverSkillFiles } from '../defence/skill-scanner/index.js';
33
28
  import { getIronDomeStatus, activateIronDome, deactivateIronDome, scanForInjection } from '../defence/iron-dome/index.js';
34
- import { getLicense, activateLicense, deactivateLicense } from '../license/store.js';
35
- import { listFeatures, requireFeature, FeatureGatedError } from '../license/gate.js';
36
- import { validateOnceNow } from '../license/validate.js';
29
+ import { requireFeature, FeatureGatedError } from '../license/gate.js';
30
+ import { registerAdminRoutes } from './routes/admin.js';
31
+ import { registerGraphRoutes } from './routes/graph.js';
32
+ import { registerIncidentRoutes } from './routes/incidents.js';
33
+ import { registerMemoryRoutes } from './routes/memories.js';
34
+ import { registerRecallRoutes } from './routes/recall.js';
35
+ import { registerSystemRoutes } from './routes/system.js';
37
36
  const PORT = process.env.PORT || 3001;
38
37
  /**
39
38
  * In-memory counters for FEATURE_GATED (403) responses per feature.
@@ -158,751 +157,9 @@ export function startVisualizationServer(dbPath) {
158
157
  const total = Object.values(gatedCounters).reduce((sum, n) => sum + n, 0);
159
158
  res.json({ total, byFeature: { ...gatedCounters } });
160
159
  });
161
- // Get all memories with filters and pagination
162
- app.get('/api/memories', requireNotLocked, async (req, res) => {
163
- try {
164
- // Extract query params as strings
165
- const project = typeof req.query.project === 'string' ? req.query.project : undefined;
166
- const type = typeof req.query.type === 'string' ? req.query.type : undefined;
167
- const category = typeof req.query.category === 'string' ? req.query.category : undefined;
168
- const limitStr = typeof req.query.limit === 'string' ? req.query.limit : '50';
169
- const offsetStr = typeof req.query.offset === 'string' ? req.query.offset : '0';
170
- const mode = typeof req.query.mode === 'string' ? req.query.mode : 'recent';
171
- const query = typeof req.query.query === 'string' ? req.query.query : undefined;
172
- const limit = Math.min(parseInt(limitStr, 10) || 50, 1000); // Cap at 1000, default 50
173
- const offset = parseInt(offsetStr, 10) || 0; // Default 0
174
- let memories;
175
- if (mode === 'search' && query) {
176
- const results = await searchMemories({
177
- query,
178
- project,
179
- type: type,
180
- category: category,
181
- limit: limit + offset + 1, // Fetch extra to check hasMore
182
- });
183
- memories = results.map(r => r.memory);
184
- }
185
- else if (mode === 'important') {
186
- memories = getHighPriorityMemories(limit + offset + 1, project);
187
- }
188
- else {
189
- memories = getRecentMemories(limit + offset + 1, project);
190
- }
191
- // Filter by type and category if provided
192
- if (type) {
193
- memories = memories.filter(m => m.type === type);
194
- }
195
- if (category) {
196
- memories = memories.filter(m => m.category === category);
197
- }
198
- // Get total count for pagination
199
- const stats = getMemoryStats(project);
200
- const total = stats.total;
201
- // Apply pagination
202
- const hasMore = memories.length > offset + limit;
203
- const paginatedMemories = memories.slice(offset, offset + limit);
204
- // Add computed decayed score to each memory
205
- const memoriesWithDecay = paginatedMemories.map(m => ({
206
- ...m,
207
- decayedScore: calculateDecayedScore(m),
208
- }));
209
- res.json({
210
- memories: memoriesWithDecay,
211
- pagination: {
212
- offset,
213
- limit,
214
- total,
215
- hasMore,
216
- },
217
- });
218
- }
219
- catch (error) {
220
- res.status(500).json({ error: error.message });
221
- }
222
- });
223
- // Activity data for heatmap (must be before :id route)
224
- app.get('/api/memories/activity', requireNotLocked, (req, res) => {
225
- try {
226
- const project = typeof req.query.project === 'string' ? req.query.project : undefined;
227
- const db = getDatabase();
228
- const query = project
229
- ? `SELECT date(created_at) as date, COUNT(*) as count
230
- FROM memories WHERE project = ?
231
- GROUP BY date(created_at)
232
- ORDER BY date DESC
233
- LIMIT 365`
234
- : `SELECT date(created_at) as date, COUNT(*) as count
235
- FROM memories
236
- GROUP BY date(created_at)
237
- ORDER BY date DESC
238
- LIMIT 365`;
239
- const rows = project
240
- ? db.prepare(query).all(project)
241
- : db.prepare(query).all();
242
- res.json({ activity: rows });
243
- }
244
- catch (error) {
245
- res.status(500).json({ error: error.message });
246
- }
247
- });
248
- // Memory quality analysis (must be before :id route)
249
- app.get('/api/memories/quality', requireNotLocked, (req, res) => {
250
- try {
251
- const project = typeof req.query.project === 'string' ? req.query.project : undefined;
252
- const db = getDatabase();
253
- const projectFilter = project ? 'AND project = ?' : '';
254
- const params = project ? [project] : [];
255
- const neverAccessed = db.prepare(`
256
- SELECT id, title, category, type, created_at, salience
257
- FROM memories WHERE access_count = 0 ${projectFilter}
258
- AND created_at < datetime('now', '-1 day')
259
- ORDER BY created_at DESC LIMIT 50
260
- `).all(...params);
261
- const stale = db.prepare(`
262
- SELECT id, title, category, type, last_accessed, decayed_score, salience
263
- FROM memories WHERE decayed_score < 0.3 ${projectFilter}
264
- AND last_accessed < datetime('now', '-30 days')
265
- ORDER BY decayed_score ASC LIMIT 50
266
- `).all(...params);
267
- const duplicates = db.prepare(`
268
- SELECT m1.id as id1, m1.title as title_a, m2.id as id2, m2.title as title_b
269
- FROM memories m1
270
- JOIN memories m2 ON m1.title = m2.title AND m1.id < m2.id
271
- ${project ? 'WHERE m1.project = ?' : ''}
272
- LIMIT 50
273
- `).all(...params);
274
- res.json({
275
- neverAccessed: { count: neverAccessed.length, items: neverAccessed },
276
- stale: { count: stale.length, items: stale },
277
- duplicates: { count: duplicates.length, items: duplicates },
278
- });
279
- }
280
- catch (error) {
281
- res.status(500).json({ error: error.message });
282
- }
283
- });
284
- // Explain why memories ranked for a recall query.
285
- // Read-only: uses the same scoring logic as search without reinforcement side effects.
286
- app.get('/api/recall/explain', requireNotLocked, async (req, res) => {
287
- try {
288
- const query = typeof req.query.query === 'string' ? req.query.query.trim() : '';
289
- if (!query) {
290
- return res.status(400).json({ error: 'query is required' });
291
- }
292
- const project = typeof req.query.project === 'string' ? req.query.project : undefined;
293
- const type = typeof req.query.type === 'string' ? req.query.type : undefined;
294
- const category = typeof req.query.category === 'string' ? req.query.category : undefined;
295
- const limit = Math.min(parseInt(req.query.limit, 10) || 10, 50);
296
- const includeDecayed = req.query.includeDecayed === 'true';
297
- const includeGlobal = req.query.includeGlobal !== 'false';
298
- const results = await searchMemoriesExplained({
299
- query,
300
- project,
301
- type: type,
302
- category: category,
303
- limit,
304
- includeDecayed,
305
- includeGlobal,
306
- });
307
- res.json({
308
- query,
309
- project: project ?? null,
310
- total: results.length,
311
- sideEffects: 'disabled',
312
- results,
313
- });
314
- }
315
- catch (error) {
316
- res.status(500).json({ error: error.message });
317
- }
318
- });
319
- // Get single memory by ID
320
- app.get('/api/memories/:id', requireNotLocked, (req, res) => {
321
- try {
322
- const id = parseInt(req.params.id);
323
- const memory = getMemoryById(id);
324
- if (!memory) {
325
- return res.status(404).json({ error: 'Memory not found' });
326
- }
327
- res.json({
328
- ...memory,
329
- decayedScore: calculateDecayedScore(memory),
330
- });
331
- }
332
- catch (error) {
333
- res.status(500).json({ error: error.message });
334
- }
335
- });
336
- // Create memory
337
- app.post('/api/memories', requireNotLocked, (req, res) => {
338
- try {
339
- const { title, content, type, category, project, tags, salience } = req.body;
340
- if (!title || !content) {
341
- return res.status(400).json({ error: 'Title and content required' });
342
- }
343
- const memory = addMemory({
344
- title,
345
- content,
346
- type: type || 'short_term',
347
- category: category || 'note',
348
- project,
349
- tags: tags || [],
350
- salience,
351
- });
352
- res.status(201).json(memory);
353
- }
354
- catch (error) {
355
- // Handle paused state gracefully
356
- if (error.name === 'MemoryPausedError') {
357
- return res.status(503).json({
358
- error: 'Memory creation is paused',
359
- paused: true,
360
- message: 'Use the dashboard control panel to resume memory creation.',
361
- });
362
- }
363
- res.status(500).json({ error: error.message });
364
- }
365
- });
366
- // Delete memory
367
- app.delete('/api/memories/:id', requireNotLocked, (req, res) => {
368
- try {
369
- const id = parseInt(req.params.id);
370
- const success = deleteMemory(id);
371
- if (!success) {
372
- return res.status(404).json({ error: 'Memory not found' });
373
- }
374
- res.json({ success: true });
375
- }
376
- catch (error) {
377
- res.status(500).json({ error: error.message });
378
- }
379
- });
380
- // Access/reinforce memory
381
- app.post('/api/memories/:id/access', requireNotLocked, (req, res) => {
382
- try {
383
- const id = parseInt(req.params.id);
384
- const memory = accessMemory(id);
385
- if (!memory) {
386
- return res.status(404).json({ error: 'Memory not found' });
387
- }
388
- res.json({
389
- ...memory,
390
- decayedScore: calculateDecayedScore(memory),
391
- });
392
- }
393
- catch (error) {
394
- res.status(500).json({ error: error.message });
395
- }
396
- });
397
- // Get statistics
398
- app.get('/api/stats', (req, res) => {
399
- try {
400
- const project = typeof req.query.project === 'string' ? req.query.project : undefined;
401
- const stats = getMemoryStats(project);
402
- // Add decay distribution
403
- const db = getDatabase();
404
- const rawRows = db.prepare(project
405
- ? 'SELECT * FROM memories WHERE project = ?'
406
- : 'SELECT * FROM memories').all(project ? [project] : []);
407
- // Convert raw DB rows to Memory objects (snake_case -> camelCase)
408
- const allMemories = rawRows.map(rowToMemory);
409
- const decayDistribution = {
410
- healthy: 0, // > 0.35 (realistic given base salience 0.25 + access bonus)
411
- fading: 0, // 0.2 - 0.35
412
- critical: 0, // < 0.2 (approaching deletion threshold)
413
- };
414
- for (const m of allMemories) {
415
- const score = calculateDecayedScore(m);
416
- if (score > 0.35)
417
- decayDistribution.healthy++;
418
- else if (score > 0.2)
419
- decayDistribution.fading++;
420
- else
421
- decayDistribution.critical++;
422
- }
423
- // Get spreading activation stats (Phase 2 organic feature)
424
- const activationStats = getActivationStats();
425
- res.json({
426
- ...stats,
427
- decayDistribution,
428
- activation: activationStats,
429
- timestamp: new Date().toISOString(),
430
- });
431
- }
432
- catch (error) {
433
- res.status(500).json({ error: error.message });
434
- }
435
- });
436
- // Memory health score (composite metric)
437
- app.get('/api/health-score', requireNotLocked, (_req, res) => {
438
- try {
439
- const db = getDatabase();
440
- // ── Freshness ──
441
- const totalCount = db.prepare('SELECT COUNT(*) as count FROM memories').get().count;
442
- const freshCount = db.prepare('SELECT COUNT(*) as count FROM memories WHERE decayed_score > 0.3').get().count;
443
- const freshnessScore = totalCount > 0 ? Math.round((freshCount / totalCount) * 100) : 100;
444
- const freshPct = totalCount > 0 ? Math.round((freshCount / totalCount) * 100) : 100;
445
- // ── Coverage ──
446
- const linkedCount = db.prepare('SELECT COUNT(DISTINCT memory_id) as count FROM memory_entities').get().count;
447
- const coverageScore = totalCount > 0 ? Math.round((linkedCount / totalCount) * 100) : 0;
448
- // ── Consistency ──
449
- const contradictionCount = db.prepare("SELECT COUNT(*) as count FROM memory_links WHERE relationship = 'contradicts'").get().count;
450
- const consistencyScore = Math.max(0, 100 - (contradictionCount * 10));
451
- // ── Consolidation ──
452
- 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();
453
- let consolidationScore = 25;
454
- if (lastConsolidated) {
455
- const hoursAgo = (Date.now() - new Date(lastConsolidated.created_at).getTime()) / (1000 * 60 * 60);
456
- if (hoursAgo <= 4)
457
- consolidationScore = 100;
458
- else if (hoursAgo <= 8)
459
- consolidationScore = 75;
460
- else if (hoursAgo <= 24)
461
- consolidationScore = 50;
462
- else
463
- consolidationScore = 25;
464
- }
465
- // ── Overall (weighted average) ──
466
- const overall = Math.round(freshnessScore * 0.3 +
467
- coverageScore * 0.25 +
468
- consistencyScore * 0.25 +
469
- consolidationScore * 0.2);
470
- // ── Consolidation detail text ──
471
- let consolidationDetail = 'No consolidated memories found';
472
- if (lastConsolidated) {
473
- const hoursAgo = (Date.now() - new Date(lastConsolidated.created_at).getTime()) / (1000 * 60 * 60);
474
- if (hoursAgo < 1)
475
- consolidationDetail = 'Last consolidated less than 1 hour ago';
476
- else
477
- consolidationDetail = `Last consolidated ${Math.round(hoursAgo)} hours ago`;
478
- }
479
- res.json({
480
- overall,
481
- components: {
482
- freshness: {
483
- score: freshnessScore,
484
- label: 'Memory Freshness',
485
- detail: `${freshPct}% of memories above decay threshold`,
486
- },
487
- coverage: {
488
- score: coverageScore,
489
- label: 'Graph Coverage',
490
- detail: `${coverageScore}% of memories have entity links`,
491
- },
492
- consistency: {
493
- score: consistencyScore,
494
- label: 'Consistency',
495
- detail: `${contradictionCount} contradictions detected`,
496
- },
497
- consolidation: {
498
- score: consolidationScore,
499
- label: 'Consolidation',
500
- detail: consolidationDetail,
501
- },
502
- },
503
- });
504
- }
505
- catch (error) {
506
- res.status(500).json({ error: error.message });
507
- }
508
- });
509
- // Get currently activated memories (spreading activation)
510
- app.get('/api/activation', requireNotLocked, (_req, res) => {
511
- try {
512
- const activeMemories = getActiveMemories();
513
- const stats = getActivationStats();
514
- res.json({
515
- activeMemories,
516
- stats,
517
- timestamp: new Date().toISOString(),
518
- });
519
- }
520
- catch (error) {
521
- res.status(500).json({ error: error.message });
522
- }
523
- });
524
- // ============================================
525
- // ORGANIC BRAIN ENDPOINTS (Phase 3)
526
- // ============================================
527
- // Get detected contradictions
528
- app.get('/api/contradictions', requireNotLocked, (req, res) => {
529
- try {
530
- const project = typeof req.query.project === 'string' ? req.query.project : undefined;
531
- const category = typeof req.query.category === 'string' ? req.query.category : undefined;
532
- const minScoreStr = typeof req.query.minScore === 'string' ? req.query.minScore : '0.4';
533
- const limitStr = typeof req.query.limit === 'string' ? req.query.limit : '20';
534
- const minScore = parseFloat(minScoreStr) || 0.4; // Default 0.4
535
- const limit = parseInt(limitStr, 10) || 20; // Default 20
536
- const contradictions = detectContradictions({
537
- project,
538
- category: category,
539
- minScore,
540
- limit,
541
- });
542
- res.json({
543
- contradictions: contradictions.map(c => ({
544
- memoryAId: c.memoryA.id,
545
- memoryATitle: c.memoryA.title,
546
- memoryBId: c.memoryB.id,
547
- memoryBTitle: c.memoryB.title,
548
- score: c.score,
549
- reason: c.reason,
550
- sharedTopics: c.sharedTopics,
551
- })),
552
- count: contradictions.length,
553
- timestamp: new Date().toISOString(),
554
- });
555
- }
556
- catch (error) {
557
- res.status(500).json({ error: error.message });
558
- }
559
- });
560
- // Get contradictions for a specific memory
561
- app.get('/api/memories/:id/contradictions', requireNotLocked, (req, res) => {
562
- try {
563
- const id = parseInt(req.params.id);
564
- if (isNaN(id)) {
565
- return res.status(400).json({ error: 'Invalid memory ID' });
566
- }
567
- const contradictions = getContradictionsFor(id);
568
- res.json({
569
- memoryId: id,
570
- contradictions: contradictions.map(c => ({
571
- contradictingMemoryId: c.memoryB.id,
572
- contradictingMemoryTitle: c.memoryB.title,
573
- score: c.score,
574
- reason: c.reason,
575
- sharedTopics: c.sharedTopics,
576
- })),
577
- count: contradictions.length,
578
- });
579
- }
580
- catch (error) {
581
- res.status(500).json({ error: error.message });
582
- }
583
- });
584
- // Manually enrich a memory with new context
585
- app.post('/api/memories/:id/enrich', requireNotLocked, (req, res) => {
586
- try {
587
- const id = parseInt(req.params.id);
588
- if (isNaN(id)) {
589
- return res.status(400).json({ error: 'Invalid memory ID' });
590
- }
591
- const { context, contextType } = req.body;
592
- if (!context || typeof context !== 'string') {
593
- return res.status(400).json({ error: 'Context string required in request body' });
594
- }
595
- const validTypes = ['search', 'access', 'related'];
596
- const type = validTypes.includes(contextType) ? contextType : 'access';
597
- const result = enrichMemory(id, context, type);
598
- res.json(result);
599
- }
600
- catch (error) {
601
- res.status(500).json({ error: error.message });
602
- }
603
- });
604
- // Get list of all projects
605
- app.get('/api/projects', (_req, res) => {
606
- try {
607
- const db = getDatabase();
608
- const projects = db.prepare(`
609
- SELECT DISTINCT project, COUNT(*) as memory_count
610
- FROM memories
611
- WHERE project IS NOT NULL AND project != ''
612
- GROUP BY project
613
- ORDER BY memory_count DESC
614
- `).all();
615
- // Add "All Projects" option with total count
616
- const totalCount = db.prepare('SELECT COUNT(*) as count FROM memories').get();
617
- res.json({
618
- projects: [
619
- { project: null, memory_count: totalCount.count, label: 'All Projects' },
620
- ...projects.map(p => ({ ...p, label: p.project })),
621
- ],
622
- });
623
- }
624
- catch (error) {
625
- res.status(500).json({ error: error.message });
626
- }
627
- });
628
- // ============================================
629
- // CONTROL ENDPOINTS
630
- // ============================================
631
- // Get control status
632
- app.get('/api/control/status', (_req, res) => {
633
- try {
634
- const status = getControlStatus();
635
- res.json(status);
636
- }
637
- catch (error) {
638
- res.status(500).json({ error: error.message });
639
- }
640
- });
641
- // Pause memory creation (soft pause — kill switch takes precedence)
642
- app.post('/api/control/pause', (_req, res) => {
643
- try {
644
- if (isKillSwitchActive()) {
645
- return res.status(409).json({ error: 'Kill switch is active — use /api/iron-dome/resume to deactivate first', code: 'KILL_SWITCH_ACTIVE' });
646
- }
647
- pause();
648
- res.json({ paused: true, message: 'Memory creation paused' });
649
- }
650
- catch (error) {
651
- res.status(500).json({ error: error.message });
652
- }
653
- });
654
- // Resume memory creation (soft resume — cannot override kill switch)
655
- app.post('/api/control/resume', (_req, res) => {
656
- try {
657
- if (isKillSwitchActive()) {
658
- return res.status(409).json({ error: 'Kill switch is active — use /api/iron-dome/resume to deactivate first', code: 'KILL_SWITCH_ACTIVE' });
659
- }
660
- resume();
661
- res.json({ paused: false, message: 'Memory creation resumed' });
662
- }
663
- catch (error) {
664
- res.status(500).json({ error: error.message });
665
- }
666
- });
667
- // ============================================
668
- // CLOUD CONFIG ENDPOINTS
669
- // ============================================
670
- // Get cloud configuration status
671
- app.get('/api/cloud/config', (_req, res) => {
672
- try {
673
- const config = getCloudConfig();
674
- const openclawMemory = getOpenClawMemoryConfig();
675
- res.json({
676
- enabled: config.cloudEnabled,
677
- apiKeySet: !!config.cloudApiKey,
678
- baseUrl: config.cloudBaseUrl,
679
- openclawMemory,
680
- });
681
- }
682
- catch (error) {
683
- res.status(500).json({ error: error.message });
684
- }
685
- });
686
- // Update cloud configuration
687
- app.post('/api/cloud/config', (req, res) => {
688
- try {
689
- const { cloudApiKey, cloudEnabled, cloudBaseUrl, openclawAutoMemory, openclawAutoMemoryDedupe, openclawAutoMemoryNoveltyThreshold, openclawAutoMemoryMaxRecent, } = req.body;
690
- if (openclawAutoMemory !== undefined && typeof openclawAutoMemory !== 'boolean') {
691
- res.status(400).json({ error: 'openclawAutoMemory must be a boolean' });
692
- return;
693
- }
694
- if (openclawAutoMemoryDedupe !== undefined && typeof openclawAutoMemoryDedupe !== 'boolean') {
695
- res.status(400).json({ error: 'openclawAutoMemoryDedupe must be a boolean' });
696
- return;
697
- }
698
- if (openclawAutoMemoryNoveltyThreshold !== undefined &&
699
- (typeof openclawAutoMemoryNoveltyThreshold !== 'number' || Number.isNaN(openclawAutoMemoryNoveltyThreshold))) {
700
- res.status(400).json({ error: 'openclawAutoMemoryNoveltyThreshold must be a number' });
701
- return;
702
- }
703
- if (openclawAutoMemoryMaxRecent !== undefined &&
704
- (typeof openclawAutoMemoryMaxRecent !== 'number' || Number.isNaN(openclawAutoMemoryMaxRecent))) {
705
- res.status(400).json({ error: 'openclawAutoMemoryMaxRecent must be a number' });
706
- return;
707
- }
708
- setCloudConfig({
709
- ...(cloudApiKey !== undefined && { cloudApiKey }),
710
- ...(cloudEnabled !== undefined && { cloudEnabled }),
711
- ...(cloudBaseUrl !== undefined && { cloudBaseUrl }),
712
- });
713
- setOpenClawMemoryConfig({
714
- ...(openclawAutoMemory !== undefined && { autoMemory: openclawAutoMemory }),
715
- ...(openclawAutoMemoryDedupe !== undefined && { dedupe: openclawAutoMemoryDedupe }),
716
- ...(openclawAutoMemoryNoveltyThreshold !== undefined && { noveltyThreshold: openclawAutoMemoryNoveltyThreshold }),
717
- ...(openclawAutoMemoryMaxRecent !== undefined && { maxRecent: openclawAutoMemoryMaxRecent }),
718
- });
719
- const updated = getCloudConfig();
720
- const openclawMemory = getOpenClawMemoryConfig();
721
- res.json({
722
- success: true,
723
- enabled: updated.cloudEnabled,
724
- apiKeySet: !!updated.cloudApiKey,
725
- baseUrl: updated.cloudBaseUrl,
726
- openclawMemory,
727
- });
728
- }
729
- catch (error) {
730
- res.status(500).json({ error: error.message });
731
- }
732
- });
733
- // ============================================
734
- // DEFENCE CONFIG ENDPOINTS
735
- // ============================================
736
- // Get defence configuration (firewall mode + integrity status)
737
- app.get('/api/defence/config', (_req, res) => {
738
- try {
739
- res.json({ mode: getDefenceMode(), tampered: isConfigTampered() });
740
- }
741
- catch (error) {
742
- res.status(500).json({ error: error.message });
743
- }
744
- });
745
- // Update defence configuration (firewall mode)
746
- app.post('/api/defence/config', (req, res) => {
747
- try {
748
- const { mode } = req.body;
749
- const validModes = ['strict', 'balanced', 'permissive'];
750
- if (!mode || !validModes.includes(mode)) {
751
- res.status(400).json({ error: `Invalid mode. Must be one of: ${validModes.join(', ')}` });
752
- return;
753
- }
754
- setDefenceMode(mode);
755
- res.json({ success: true, mode });
756
- }
757
- catch (error) {
758
- res.status(500).json({ error: error.message });
759
- }
760
- });
761
- // Get cloud sync status (queue stats + config)
762
- app.get('/api/cloud/sync-status', (_req, res) => {
763
- try {
764
- const config = getCloudConfig();
765
- const raw = readRawConfig();
766
- const queue = getQueueStats();
767
- res.json({
768
- enabled: config.cloudEnabled,
769
- apiKeySet: !!config.cloudApiKey,
770
- lastSyncAt: (typeof raw.lastSyncAt === 'string' ? raw.lastSyncAt : null),
771
- queue: {
772
- pending: queue.pending,
773
- failed: queue.failed,
774
- },
775
- });
776
- }
777
- catch (error) {
778
- res.status(500).json({ error: error.message });
779
- }
780
- });
781
- // ============================================
782
- // VERSION ENDPOINTS
783
- // ============================================
784
- // Get current version (with stale detection)
785
- app.get('/api/version', (_req, res) => {
786
- try {
787
- const version = getCurrentVersion();
788
- const runningVersion = getRunningVersion();
789
- res.json({ version, runningVersion, stale: runningVersion !== version });
790
- }
791
- catch (error) {
792
- res.status(500).json({ error: error.message });
793
- }
794
- });
795
- // Check for updates
796
- app.get('/api/version/check', async (req, res) => {
797
- try {
798
- const forceRefresh = req.query.force === 'true';
799
- const versionInfo = await checkForUpdates(forceRefresh);
800
- res.json(versionInfo);
801
- }
802
- catch (error) {
803
- res.status(500).json({ error: error.message });
804
- }
805
- });
806
- // Perform update
807
- app.post('/api/version/update', async (_req, res) => {
808
- try {
809
- // Notify clients that update is starting
810
- broadcast({
811
- type: 'update_started',
812
- timestamp: new Date().toISOString(),
813
- data: { message: 'Update in progress...' },
814
- });
815
- const result = await performUpdate();
816
- // Notify clients of result
817
- broadcast({
818
- type: result.success ? 'update_complete' : 'update_failed',
819
- timestamp: new Date().toISOString(),
820
- data: result,
821
- });
822
- res.json(result);
823
- }
824
- catch (error) {
825
- res.status(500).json({ error: error.message });
826
- }
827
- });
828
- // Restart server
829
- app.post('/api/version/restart', (_req, res) => {
830
- try {
831
- // Notify all WebSocket clients
832
- broadcast({
833
- type: 'server_restarting',
834
- timestamp: new Date().toISOString(),
835
- data: { message: 'Server restarting in 3 seconds...' },
836
- });
837
- // Close WebSocket connections gracefully
838
- for (const client of clients) {
839
- try {
840
- if (client.readyState === WebSocket.OPEN) {
841
- client.send(JSON.stringify({
842
- type: 'server_restarting',
843
- timestamp: new Date().toISOString(),
844
- data: { reconnectIn: 5000 },
845
- }));
846
- }
847
- }
848
- catch (e) {
849
- console.error('[shieldcortex] WebSocket send failed during restart:', e);
850
- }
851
- }
852
- // Schedule restart after response is sent
853
- res.json({ success: true, message: 'Server will restart in 3 seconds' });
854
- scheduleRestart(3000);
855
- }
856
- catch (error) {
857
- res.status(500).json({ error: error.message });
858
- }
859
- });
860
- // Get memory links/relationships
861
- app.get('/api/links', requireNotLocked, (req, res) => {
862
- try {
863
- const project = typeof req.query.project === 'string' ? req.query.project : undefined;
864
- const db = getDatabase();
865
- const query = project
866
- ? `
867
- SELECT
868
- ml.*,
869
- m1.title as source_title,
870
- m1.category as source_category,
871
- m1.type as source_type,
872
- m2.title as target_title,
873
- m2.category as target_category,
874
- m2.type as target_type
875
- FROM memory_links ml
876
- JOIN memories m1 ON ml.source_id = m1.id
877
- JOIN memories m2 ON ml.target_id = m2.id
878
- WHERE m1.project = ? OR m2.project = ?
879
- ORDER BY ml.created_at DESC
880
- LIMIT 500
881
- `
882
- : `
883
- SELECT
884
- ml.*,
885
- m1.title as source_title,
886
- m1.category as source_category,
887
- m1.type as source_type,
888
- m2.title as target_title,
889
- m2.category as target_category,
890
- m2.type as target_type
891
- FROM memory_links ml
892
- JOIN memories m1 ON ml.source_id = m1.id
893
- JOIN memories m2 ON ml.target_id = m2.id
894
- ORDER BY ml.created_at DESC
895
- LIMIT 500
896
- `;
897
- const links = project
898
- ? db.prepare(query).all(project, project)
899
- : db.prepare(query).all();
900
- res.json(links);
901
- }
902
- catch (error) {
903
- res.status(500).json({ error: error.message });
904
- }
905
- });
160
+ registerMemoryRoutes(app, requireNotLocked);
161
+ registerRecallRoutes(app, requireNotLocked);
162
+ registerSystemRoutes(app, { broadcast, clients });
906
163
  // ============================================
907
164
  // INSIGHTS ENDPOINTS
908
165
  // ============================================
@@ -948,589 +205,26 @@ export function startVisualizationServer(dbPath) {
948
205
  columns,
949
206
  rows,
950
207
  rowCount: rows.length,
951
- executionTime,
952
- });
953
- }
954
- else {
955
- // Write operation
956
- const result = db.prepare(query).run();
957
- const executionTime = Date.now() - startTime;
958
- res.json({
959
- columns: ['changes', 'lastInsertRowid'],
960
- rows: [{ changes: result.changes, lastInsertRowid: result.lastInsertRowid }],
961
- rowCount: 1,
962
- executionTime,
963
- });
964
- }
965
- }
966
- catch (error) {
967
- res.status(500).json({ error: error.message });
968
- }
969
- });
970
- // Trigger consolidation
971
- app.post('/api/consolidate', requireNotLocked, (_req, res) => {
972
- try {
973
- const result = consolidate();
974
- // Emit event for Activity log
975
- emitConsolidation(result);
976
- res.json({
977
- success: true,
978
- ...result,
979
- });
980
- }
981
- catch (error) {
982
- res.status(500).json({ error: error.message });
983
- }
984
- });
985
- // Get context summary
986
- app.get('/api/context', requireNotLocked, async (req, res) => {
987
- try {
988
- const project = typeof req.query.project === 'string' ? req.query.project : undefined;
989
- const summary = await generateContextSummary(project);
990
- const formatted = formatContextSummary(summary);
991
- res.json({
992
- summary,
993
- formatted,
994
- });
995
- }
996
- catch (error) {
997
- res.status(500).json({ error: error.message });
998
- }
999
- });
1000
- // Get search suggestions (for autocomplete)
1001
- app.get('/api/suggestions', requireNotLocked, (req, res) => {
1002
- try {
1003
- const query = typeof req.query.q === 'string' ? req.query.q : '';
1004
- const limit = typeof req.query.limit === 'string' ? parseInt(req.query.limit) : 10;
1005
- if (!query || query.length < 2) {
1006
- return res.json({ suggestions: [] });
1007
- }
1008
- const db = getDatabase();
1009
- // Get suggestions from memory titles, categories, tags, and projects
1010
- const suggestions = [];
1011
- // Search titles that contain the query
1012
- const titleMatches = db.prepare(`
1013
- SELECT DISTINCT title, COUNT(*) as count
1014
- FROM memories
1015
- WHERE title LIKE ?
1016
- GROUP BY title
1017
- ORDER BY count DESC, last_accessed DESC
1018
- LIMIT ?
1019
- `).all(`%${query}%`, limit);
1020
- for (const match of titleMatches) {
1021
- suggestions.push({ text: match.title, type: 'title', count: match.count });
1022
- }
1023
- // Get matching categories
1024
- const categoryMatches = db.prepare(`
1025
- SELECT DISTINCT category, COUNT(*) as count
1026
- FROM memories
1027
- WHERE category LIKE ?
1028
- GROUP BY category
1029
- ORDER BY count DESC
1030
- LIMIT 5
1031
- `).all(`%${query}%`);
1032
- for (const match of categoryMatches) {
1033
- suggestions.push({ text: match.category, type: 'category', count: match.count });
1034
- }
1035
- // Get matching projects
1036
- const projectMatches = db.prepare(`
1037
- SELECT DISTINCT project, COUNT(*) as count
1038
- FROM memories
1039
- WHERE project IS NOT NULL AND project LIKE ?
1040
- GROUP BY project
1041
- ORDER BY count DESC
1042
- LIMIT 5
1043
- `).all(`%${query}%`);
1044
- for (const match of projectMatches) {
1045
- suggestions.push({ text: match.project, type: 'project', count: match.count });
1046
- }
1047
- // Sort by count and limit total results
1048
- suggestions.sort((a, b) => b.count - a.count);
1049
- const limitedSuggestions = suggestions.slice(0, limit);
1050
- res.json({ suggestions: limitedSuggestions });
1051
- }
1052
- catch (error) {
1053
- res.status(500).json({ error: error.message });
1054
- }
1055
- });
1056
- // ============================================
1057
- // GRAPH / ONTOLOGY ENDPOINTS
1058
- // ============================================
1059
- // List entities with optional filters and pagination
1060
- app.get('/api/graph/entities', requireNotLocked, (req, res) => {
1061
- try {
1062
- const db = getDatabase();
1063
- const type = typeof req.query.type === 'string' ? req.query.type : undefined;
1064
- const minMentions = typeof req.query.minMentions === 'string' ? parseInt(req.query.minMentions) : 0;
1065
- const limit = typeof req.query.limit === 'string' ? Math.min(parseInt(req.query.limit), 500) : 100;
1066
- const offset = typeof req.query.offset === 'string' ? parseInt(req.query.offset) : 0;
1067
- let whereClause = 'WHERE 1=1';
1068
- const params = [];
1069
- if (type) {
1070
- whereClause += ' AND type = ?';
1071
- params.push(type);
1072
- }
1073
- if (minMentions > 0) {
1074
- whereClause += ' AND memory_count >= ?';
1075
- params.push(minMentions);
1076
- }
1077
- const totalRow = db.prepare(`SELECT COUNT(*) as count FROM entities ${whereClause}`).get(...params);
1078
- const total = totalRow.count;
1079
- const rows = db.prepare(`SELECT * FROM entities ${whereClause} ORDER BY memory_count DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
1080
- const entities = rows.map((r) => {
1081
- let aliases = [];
1082
- try {
1083
- aliases = JSON.parse(r.aliases || '[]');
1084
- }
1085
- catch {
1086
- aliases = [];
1087
- }
1088
- return {
1089
- id: r.id,
1090
- name: r.name,
1091
- type: r.type,
1092
- memoryCount: r.memory_count ?? 0,
1093
- aliases,
1094
- createdAt: r.created_at,
1095
- updatedAt: r.updated_at,
1096
- };
1097
- });
1098
- res.json({ entities, total, offset, limit, hasMore: offset + limit < total });
1099
- }
1100
- catch (error) {
1101
- res.status(500).json({ error: error.message });
1102
- }
1103
- });
1104
- // Get triples for a specific entity
1105
- app.get('/api/graph/entities/:id/triples', requireNotLocked, (req, res) => {
1106
- try {
1107
- const db = getDatabase();
1108
- const id = parseInt(req.params.id);
1109
- if (isNaN(id)) {
1110
- return res.status(400).json({ error: 'Invalid entity ID' });
1111
- }
1112
- const rows = db.prepare(`
1113
- SELECT t.*, s.name as subject_name, s.type as subject_type,
1114
- o.name as object_name, o.type as object_type
1115
- FROM triples t
1116
- JOIN entities s ON s.id = t.subject_id
1117
- JOIN entities o ON o.id = t.object_id
1118
- WHERE t.subject_id = ? OR t.object_id = ?
1119
- ORDER BY t.created_at DESC
1120
- `).all(id, id);
1121
- res.json({ triples: rows });
1122
- }
1123
- catch (error) {
1124
- res.status(500).json({ error: error.message });
1125
- }
1126
- });
1127
- // Get memories linked to a specific entity
1128
- app.get('/api/graph/entities/:id/memories', requireNotLocked, (req, res) => {
1129
- try {
1130
- const db = getDatabase();
1131
- const id = parseInt(req.params.id);
1132
- if (isNaN(id)) {
1133
- return res.status(400).json({ error: 'Invalid entity ID' });
1134
- }
1135
- const rows = db.prepare(`
1136
- SELECT m.id, m.title, m.type, m.category, m.salience, m.created_at
1137
- FROM memories m
1138
- JOIN memory_entities me ON me.memory_id = m.id
1139
- WHERE me.entity_id = ?
1140
- ORDER BY m.salience DESC, m.created_at DESC
1141
- LIMIT 50
1142
- `).all(id);
1143
- res.json({ memories: rows });
1144
- }
1145
- catch (error) {
1146
- res.status(500).json({ error: error.message });
1147
- }
1148
- });
1149
- // Get neighbourhood of an entity: the entity, its direct neighbours, and connecting triples
1150
- app.get('/api/graph/entities/:id/neighbourhood', requireNotLocked, (req, res) => {
1151
- try {
1152
- const db = getDatabase();
1153
- const id = parseInt(req.params.id);
1154
- if (isNaN(id)) {
1155
- return res.status(400).json({ error: 'Invalid entity ID' });
1156
- }
1157
- // Get the focal entity
1158
- const focal = db.prepare('SELECT id, name, type, memory_count as memoryCount, aliases FROM entities WHERE id = ?').get(id);
1159
- if (!focal) {
1160
- return res.status(404).json({ error: 'Entity not found' });
1161
- }
1162
- focal.aliases = JSON.parse(focal.aliases || '[]');
1163
- // Get all triples involving this entity (exclude related_to noise — only meaningful predicates)
1164
- const triplesAll = db.prepare(`
1165
- SELECT t.id, t.subject_id, t.object_id, t.predicate,
1166
- s.name as subject_name, s.type as subject_type, s.memory_count as subject_count,
1167
- o.name as object_name, o.type as object_type, o.memory_count as object_count
1168
- FROM triples t
1169
- JOIN entities s ON s.id = t.subject_id
1170
- JOIN entities o ON o.id = t.object_id
1171
- WHERE (t.subject_id = ? OR t.object_id = ?)
1172
- ORDER BY
1173
- CASE WHEN t.predicate != 'related_to' THEN 0 ELSE 1 END,
1174
- CASE WHEN t.subject_id = ? THEN o.memory_count ELSE s.memory_count END DESC
1175
- `).all(id, id, id);
1176
- // Collect unique neighbour IDs, prioritising meaningful predicates
1177
- const neighbourIds = new Map();
1178
- const meaningfulTriples = [];
1179
- const relatedToTriples = [];
1180
- for (const t of triplesAll) {
1181
- const neighbourId = t.subject_id === id ? t.object_id : t.subject_id;
1182
- const count = t.subject_id === id ? t.object_count : t.subject_count;
1183
- if (neighbourId === id)
1184
- continue;
1185
- if (t.predicate !== 'related_to') {
1186
- meaningfulTriples.push(t);
1187
- if (!neighbourIds.has(neighbourId)) {
1188
- neighbourIds.set(neighbourId, { predicate: t.predicate, count });
1189
- }
1190
- }
1191
- else {
1192
- relatedToTriples.push(t);
1193
- }
1194
- }
1195
- // Add related_to neighbours up to a cap (prefer high memory count)
1196
- for (const t of relatedToTriples) {
1197
- if (neighbourIds.size >= 25)
1198
- break;
1199
- const neighbourId = t.subject_id === id ? t.object_id : t.subject_id;
1200
- const count = t.subject_id === id ? t.object_count : t.subject_count;
1201
- if (!neighbourIds.has(neighbourId)) {
1202
- neighbourIds.set(neighbourId, { predicate: 'related_to', count });
1203
- }
1204
- }
1205
- // Build triples list (only for included neighbours)
1206
- const includedTriples = [
1207
- ...meaningfulTriples.filter(t => {
1208
- const nid = t.subject_id === id ? t.object_id : t.subject_id;
1209
- return neighbourIds.has(nid);
1210
- }),
1211
- ...relatedToTriples.filter(t => {
1212
- const nid = t.subject_id === id ? t.object_id : t.subject_id;
1213
- return neighbourIds.has(nid);
1214
- }),
1215
- ];
1216
- // Deduplicate triples by id
1217
- const seenTriples = new Set();
1218
- const uniqueTriples = includedTriples.filter(t => {
1219
- if (seenTriples.has(t.id))
1220
- return false;
1221
- seenTriples.add(t.id);
1222
- return true;
1223
- });
1224
- // Fetch neighbour entities
1225
- const neighbourEntities = [];
1226
- if (neighbourIds.size > 0) {
1227
- const ids = [...neighbourIds.keys()];
1228
- const placeholders = ids.map(() => '?').join(',');
1229
- const rows = db.prepare(`
1230
- SELECT id, name, type, memory_count as memoryCount, aliases
1231
- FROM entities WHERE id IN (${placeholders})
1232
- `).all(...ids);
1233
- for (const r of rows) {
1234
- r.aliases = JSON.parse(r.aliases || '[]');
1235
- neighbourEntities.push(r);
1236
- }
1237
- }
1238
- res.json({
1239
- focal,
1240
- neighbours: neighbourEntities,
1241
- triples: uniqueTriples,
1242
- totalConnections: triplesAll.length,
1243
- });
1244
- }
1245
- catch (error) {
1246
- res.status(500).json({ error: error.message });
1247
- }
1248
- });
1249
- // List triples with optional predicate filter and pagination
1250
- app.get('/api/graph/triples', requireNotLocked, (req, res) => {
1251
- try {
1252
- const db = getDatabase();
1253
- const predicate = typeof req.query.predicate === 'string' ? req.query.predicate : undefined;
1254
- const limit = typeof req.query.limit === 'string' ? Math.min(parseInt(req.query.limit), 10000) : 100;
1255
- const offset = typeof req.query.offset === 'string' ? parseInt(req.query.offset) : 0;
1256
- let whereClause = '';
1257
- const params = [];
1258
- if (predicate) {
1259
- whereClause = 'WHERE t.predicate = ?';
1260
- params.push(predicate);
1261
- }
1262
- // For large triple sets, only return triples involving the top 500 entities
1263
- // to keep response sizes manageable
1264
- const totalRow = db.prepare(`SELECT COUNT(*) as count FROM triples t ${whereClause}`).get(...params);
1265
- const total = totalRow.count;
1266
- const rows = db.prepare(`
1267
- SELECT t.*, s.name as subject_name, s.type as subject_type,
1268
- o.name as object_name, o.type as object_type
1269
- FROM triples t
1270
- JOIN entities s ON s.id = t.subject_id
1271
- JOIN entities o ON o.id = t.object_id
1272
- ${whereClause}
1273
- ORDER BY t.created_at DESC
1274
- LIMIT ? OFFSET ?
1275
- `).all(...params, limit, offset);
1276
- res.json({ triples: rows, total, offset, limit, hasMore: offset + limit < total });
1277
- }
1278
- catch (error) {
1279
- res.status(500).json({ error: error.message });
1280
- }
1281
- });
1282
- // Search entities by name
1283
- app.get('/api/graph/search', requireNotLocked, (req, res) => {
1284
- try {
1285
- const db = getDatabase();
1286
- const q = typeof req.query.q === 'string' ? req.query.q : '';
1287
- if (!q) {
1288
- return res.status(400).json({ error: 'Query parameter "q" is required' });
1289
- }
1290
- const rows = db.prepare(`SELECT * FROM entities WHERE LOWER(name) LIKE ? ORDER BY memory_count DESC LIMIT 20`).all(`%${q.toLowerCase()}%`);
1291
- const entities = rows.map((r) => {
1292
- let aliases = [];
1293
- try {
1294
- aliases = JSON.parse(r.aliases || '[]');
1295
- }
1296
- catch {
1297
- aliases = [];
1298
- }
1299
- return {
1300
- id: r.id,
1301
- name: r.name,
1302
- type: r.type,
1303
- memoryCount: r.memory_count ?? 0,
1304
- aliases,
1305
- };
1306
- });
1307
- res.json({ entities });
1308
- }
1309
- catch (error) {
1310
- res.status(500).json({ error: error.message });
1311
- }
1312
- });
1313
- // Find path between two entities using BFS
1314
- app.get('/api/graph/paths', requireNotLocked, (req, res) => {
1315
- try {
1316
- const db = getDatabase();
1317
- const fromName = typeof req.query.from === 'string' ? req.query.from : '';
1318
- const toName = typeof req.query.to === 'string' ? req.query.to : '';
1319
- if (!fromName || !toName) {
1320
- return res.status(400).json({ error: 'Both "from" and "to" query parameters are required' });
1321
- }
1322
- const fromRow = db.prepare('SELECT * FROM entities WHERE LOWER(name) = LOWER(?)').get(fromName);
1323
- if (!fromRow) {
1324
- return res.status(404).json({ error: `Entity "${fromName}" not found` });
1325
- }
1326
- const toRow = db.prepare('SELECT * FROM entities WHERE LOWER(name) = LOWER(?)').get(toName);
1327
- if (!toRow) {
1328
- return res.status(404).json({ error: `Entity "${toName}" not found` });
1329
- }
1330
- if (fromRow.id === toRow.id) {
1331
- return res.json({ path: [{ entity: fromRow.name, predicate: '(self)' }], sourceMemories: [] });
1332
- }
1333
- // BFS
1334
- const maxDepth = 4;
1335
- const visited = new Map();
1336
- visited.set(fromRow.id, { id: fromRow.id, name: fromRow.name, parentId: null, predicate: '', sourceMemoryId: null });
1337
- let frontier = [fromRow.id];
1338
- let found = false;
1339
- for (let d = 0; d < maxDepth && !found; d++) {
1340
- const nextFrontier = [];
1341
- for (const nodeId of frontier) {
1342
- const outgoing = db.prepare('SELECT t.object_id as next_id, t.predicate, t.source_memory_id, e.name FROM triples t JOIN entities e ON e.id = t.object_id WHERE t.subject_id = ?').all(nodeId);
1343
- for (const row of outgoing) {
1344
- if (!visited.has(row.next_id)) {
1345
- visited.set(row.next_id, { id: row.next_id, name: row.name, parentId: nodeId, predicate: row.predicate, sourceMemoryId: row.source_memory_id });
1346
- nextFrontier.push(row.next_id);
1347
- if (row.next_id === toRow.id) {
1348
- found = true;
1349
- break;
1350
- }
1351
- }
1352
- }
1353
- if (found)
1354
- break;
1355
- const incoming = db.prepare('SELECT t.subject_id as next_id, t.predicate, t.source_memory_id, e.name FROM triples t JOIN entities e ON e.id = t.subject_id WHERE t.object_id = ?').all(nodeId);
1356
- for (const row of incoming) {
1357
- if (!visited.has(row.next_id)) {
1358
- visited.set(row.next_id, { id: row.next_id, name: row.name, parentId: nodeId, predicate: `~${row.predicate}`, sourceMemoryId: row.source_memory_id });
1359
- nextFrontier.push(row.next_id);
1360
- if (row.next_id === toRow.id) {
1361
- found = true;
1362
- break;
1363
- }
1364
- }
1365
- }
1366
- if (found)
1367
- break;
1368
- }
1369
- frontier = nextFrontier;
1370
- if (frontier.length === 0)
1371
- break;
1372
- }
1373
- if (!found) {
1374
- return res.json({ path: [], sourceMemories: [], message: 'No path found' });
1375
- }
1376
- // Reconstruct path
1377
- const path = [];
1378
- const sourceMemoryIds = [];
1379
- let current = visited.get(toRow.id);
1380
- while (current) {
1381
- path.unshift({ entity: current.name, predicate: current.predicate });
1382
- if (current.sourceMemoryId)
1383
- sourceMemoryIds.push(current.sourceMemoryId);
1384
- current = current.parentId !== null ? visited.get(current.parentId) : undefined;
1385
- }
1386
- // Fetch source memories
1387
- const sourceMemories = sourceMemoryIds.length > 0
1388
- ? db.prepare(`SELECT id, title FROM memories WHERE id IN (${sourceMemoryIds.map(() => '?').join(',')})`).all(...sourceMemoryIds)
1389
- : [];
1390
- res.json({ path, sourceMemories });
1391
- }
1392
- catch (error) {
1393
- res.status(500).json({ error: error.message });
1394
- }
1395
- });
1396
- // ============================================
1397
- // BRAIN CONTROL CENTRE
1398
- // ============================================
1399
- // Boost memory salience (+0.15, capped at 1.0)
1400
- app.post('/api/memories/:id/boost', requireNotLocked, (req, res) => {
1401
- try {
1402
- const id = parseInt(req.params.id);
1403
- const memory = getMemoryById(id);
1404
- if (!memory) {
1405
- return res.status(404).json({ error: 'Memory not found' });
1406
- }
1407
- const newSalience = Math.min(1.0, (memory.salience ?? 0.5) + 0.15);
1408
- const updated = updateMemory(id, { salience: newSalience });
1409
- res.json(updated);
1410
- }
1411
- catch (error) {
1412
- res.status(500).json({ error: error.message });
1413
- }
1414
- });
1415
- // Demote memory salience (-0.15, floor at 0.05)
1416
- app.post('/api/memories/:id/demote', requireNotLocked, (req, res) => {
1417
- try {
1418
- const id = parseInt(req.params.id);
1419
- const memory = getMemoryById(id);
1420
- if (!memory) {
1421
- return res.status(404).json({ error: 'Memory not found' });
1422
- }
1423
- const newSalience = Math.max(0.05, (memory.salience ?? 0.5) - 0.15);
1424
- const updated = updateMemory(id, { salience: newSalience });
1425
- res.json(updated);
1426
- }
1427
- catch (error) {
1428
- res.status(500).json({ error: error.message });
1429
- }
1430
- });
1431
- // Promote memory from STM to LTM
1432
- app.post('/api/memories/:id/promote', requireNotLocked, (req, res) => {
1433
- try {
1434
- const id = parseInt(req.params.id);
1435
- const memory = promoteMemory(id);
1436
- if (!memory) {
1437
- return res.status(404).json({ error: 'Memory not found' });
1438
- }
1439
- res.json(memory);
1440
- }
1441
- catch (error) {
1442
- res.status(500).json({ error: error.message });
1443
- }
1444
- });
1445
- // Update memory (partial: title, content, tags, category, importance/salience)
1446
- app.patch('/api/memories/:id', requireNotLocked, (req, res) => {
1447
- try {
1448
- const id = parseInt(req.params.id);
1449
- const { title, content, category, tags, importance } = req.body;
1450
- // Validate provided fields
1451
- if (title !== undefined) {
1452
- if (typeof title !== 'string' || title.trim().length === 0) {
1453
- return res.status(400).json({ error: 'Title must be a non-empty string' });
1454
- }
1455
- }
1456
- if (content !== undefined) {
1457
- if (typeof content !== 'string') {
1458
- return res.status(400).json({ error: 'Content must be a string' });
1459
- }
1460
- }
1461
- const validCategories = ['architecture', 'pattern', 'preference', 'error', 'context', 'learning', 'todo', 'note', 'relationship', 'custom'];
1462
- if (category !== undefined) {
1463
- if (!validCategories.includes(category)) {
1464
- return res.status(400).json({ error: `Category must be one of: ${validCategories.join(', ')}` });
1465
- }
1466
- }
1467
- if (tags !== undefined) {
1468
- if (!Array.isArray(tags) || !tags.every((t) => typeof t === 'string')) {
1469
- return res.status(400).json({ error: 'Tags must be an array of strings' });
1470
- }
1471
- }
1472
- if (importance !== undefined) {
1473
- if (typeof importance !== 'number' || importance < 0 || importance > 1) {
1474
- return res.status(400).json({ error: 'Importance must be a number between 0 and 1' });
1475
- }
1476
- }
1477
- // Build clean updates object (map importance → salience)
1478
- const updates = {};
1479
- if (title !== undefined)
1480
- updates.title = title.trim();
1481
- if (content !== undefined)
1482
- updates.content = content;
1483
- if (category !== undefined)
1484
- updates.category = category;
1485
- if (tags !== undefined)
1486
- updates.tags = tags;
1487
- if (importance !== undefined)
1488
- updates.salience = importance;
1489
- const updated = updateMemory(id, updates);
1490
- if (!updated) {
1491
- return res.status(404).json({ error: 'Memory not found' });
1492
- }
1493
- res.json(updated);
1494
- }
1495
- catch (error) {
1496
- res.status(500).json({ error: error.message });
1497
- }
1498
- });
1499
- // Quarantine a memory (move to quarantine table, delete original)
1500
- app.post('/api/memories/:id/quarantine', requireNotLocked, (req, res) => {
1501
- try {
1502
- const id = parseInt(req.params.id);
1503
- const memory = getMemoryById(id);
1504
- if (!memory) {
1505
- return res.status(404).json({ error: 'Memory not found' });
1506
- }
1507
- const db = getDatabase();
1508
- db.prepare(`INSERT INTO quarantine (original_title, original_content, source_type, source_identifier, reason, project, status, created_at)
1509
- VALUES (?, ?, ?, ?, ?, ?, 'pending', ?)`).run(memory.title, memory.content, 'dashboard', 'brain-control', req.body.reason || 'Manually quarantined from Brain dashboard', memory.project || null, new Date().toISOString());
1510
- deleteMemory(id);
1511
- res.json({ success: true, quarantined: id });
1512
- }
1513
- catch (error) {
1514
- res.status(500).json({ error: error.message });
1515
- }
1516
- });
1517
- // Create a manual link between two memories
1518
- app.post('/api/links', requireNotLocked, (req, res) => {
1519
- try {
1520
- const { sourceId, targetId, relationship, strength } = req.body;
1521
- if (!sourceId || !targetId || !relationship) {
1522
- return res.status(400).json({ error: 'sourceId, targetId, and relationship are required' });
208
+ executionTime,
209
+ });
1523
210
  }
1524
- const link = createMemoryLink(sourceId, targetId, relationship, strength ?? 0.5);
1525
- if (!link) {
1526
- return res.status(404).json({ error: 'One or both memories not found, or self-link attempted' });
211
+ else {
212
+ // Write operation
213
+ const result = db.prepare(query).run();
214
+ const executionTime = Date.now() - startTime;
215
+ res.json({
216
+ columns: ['changes', 'lastInsertRowid'],
217
+ rows: [{ changes: result.changes, lastInsertRowid: result.lastInsertRowid }],
218
+ rowCount: 1,
219
+ executionTime,
220
+ });
1527
221
  }
1528
- res.json(link);
1529
222
  }
1530
223
  catch (error) {
1531
224
  res.status(500).json({ error: error.message });
1532
225
  }
1533
226
  });
227
+ registerGraphRoutes(app, requireNotLocked);
1534
228
  // ============================================
1535
229
  // SKILL SCANNER
1536
230
  // ============================================
@@ -1848,585 +542,9 @@ export function startVisualizationServer(dbPath) {
1848
542
  res.status(500).json({ error: error.message });
1849
543
  }
1850
544
  });
1851
- // Query audit logs
1852
- app.get('/api/v1/audit', (req, res) => {
1853
- try {
1854
- const options = {};
1855
- if (req.query.startTime)
1856
- options.startTime = req.query.startTime;
1857
- if (req.query.endTime)
1858
- options.endTime = req.query.endTime;
1859
- if (req.query.source)
1860
- options.source = req.query.source;
1861
- if (req.query.firewallResult)
1862
- options.firewallResult = req.query.firewallResult;
1863
- if (req.query.limit)
1864
- options.limit = parseInt(req.query.limit, 10);
1865
- if (req.query.project)
1866
- options.project = req.query.project;
1867
- const logs = queryAuditLogs(options);
1868
- res.json({ logs, total: logs.length });
1869
- }
1870
- catch (error) {
1871
- res.status(500).json({ error: error.message });
1872
- }
1873
- });
1874
- // Audit statistics
1875
- app.get('/api/v1/audit/stats', (req, res) => {
1876
- try {
1877
- const timeRange = req.query.timeRange ?? '24h';
1878
- const project = req.query.project;
1879
- const stats = getAuditStats(timeRange, project);
1880
- res.json(stats);
1881
- }
1882
- catch (error) {
1883
- res.status(500).json({ error: error.message });
1884
- }
1885
- });
1886
- // Incident replay — reconstruct a best-effort timeline from audit, quarantine, and persisted events.
1887
- app.get('/api/v1/incidents/replay', (req, res) => {
1888
- try {
1889
- const limit = Math.min(parseInt(req.query.limit, 10) || 200, 500);
1890
- const startTime = typeof req.query.startTime === 'string' ? req.query.startTime : undefined;
1891
- const endTime = typeof req.query.endTime === 'string' ? req.query.endTime : undefined;
1892
- const project = typeof req.query.project === 'string' ? req.query.project : undefined;
1893
- const sourceIdentifier = typeof req.query.sourceIdentifier === 'string' ? req.query.sourceIdentifier : undefined;
1894
- const memoryId = req.query.memoryId ? parseInt(req.query.memoryId, 10) : undefined;
1895
- const events = queryIncidentReplay({
1896
- startTime,
1897
- endTime,
1898
- project,
1899
- sourceIdentifier,
1900
- memoryId,
1901
- limit,
1902
- });
1903
- res.json({
1904
- events,
1905
- total: events.length,
1906
- coverage: {
1907
- sources: ['defence_audit', 'quarantine', 'events'],
1908
- note: 'Replay is best-effort. Durable audit and quarantine history is complete; generic event coverage depends on the retained events table window.',
1909
- },
1910
- });
1911
- }
1912
- catch (error) {
1913
- res.status(500).json({ error: error.message });
1914
- }
1915
- });
1916
- // Agent registry — distinct agents aggregated from audit logs
1917
- app.get('/api/v1/agents', (req, res) => {
1918
- try {
1919
- const timeRange = req.query.timeRange ?? '24h';
1920
- const project = req.query.project;
1921
- const agents = queryAgentRegistry(timeRange, project);
1922
- res.json({ agents });
1923
- }
1924
- catch (error) {
1925
- res.status(500).json({ error: error.message });
1926
- }
1927
- });
1928
- // Agent trust score timeline
1929
- app.get('/api/v1/agents/:identifier/timeline', (req, res) => {
1930
- try {
1931
- const identifier = decodeURIComponent(req.params.identifier);
1932
- const timeRange = req.query.timeRange ?? '24h';
1933
- const project = req.query.project;
1934
- const points = queryAgentTimeline(identifier, timeRange, project);
1935
- res.json({ points });
1936
- }
1937
- catch (error) {
1938
- res.status(500).json({ error: error.message });
1939
- }
1940
- });
1941
- // Agent operations — paginated audit entries for one agent
1942
- app.get('/api/v1/agents/:identifier/operations', (req, res) => {
1943
- try {
1944
- const identifier = decodeURIComponent(req.params.identifier);
1945
- const limit = parseInt(req.query.limit, 10) || 50;
1946
- const offset = parseInt(req.query.offset, 10) || 0;
1947
- const firewallResult = req.query.firewallResult;
1948
- const project = req.query.project;
1949
- const entries = queryAgentOperations(identifier, {
1950
- limit, offset, project,
1951
- firewallResult: firewallResult,
1952
- });
1953
- res.json({ entries, limit, offset });
1954
- }
1955
- catch (error) {
1956
- res.status(500).json({ error: error.message });
1957
- }
1958
- });
1959
- // List quarantined items
1960
- app.get('/api/v1/quarantine', (req, res) => {
1961
- try {
1962
- const db = getDatabase();
1963
- const status = req.query.status ?? 'pending';
1964
- const limit = parseInt(req.query.limit, 10) || 50;
1965
- const project = req.query.project;
1966
- const sql = project
1967
- ? 'SELECT * FROM quarantine WHERE status = ? AND project = ? ORDER BY created_at DESC LIMIT ?'
1968
- : 'SELECT * FROM quarantine WHERE status = ? ORDER BY created_at DESC LIMIT ?';
1969
- const params = project ? [status, project, limit] : [status, limit];
1970
- const rows = db.prepare(sql).all(...params);
1971
- const items = rows.map((r) => ({
1972
- ...r,
1973
- title: r.original_title,
1974
- content: r.original_content,
1975
- }));
1976
- res.json({ items, total: items.length });
1977
- }
1978
- catch (error) {
1979
- res.status(500).json({ error: error.message });
1980
- }
1981
- });
1982
- // Approve quarantined item
1983
- app.post('/api/v1/quarantine/:id/approve', requireNotLocked, (req, res) => {
1984
- try {
1985
- const db = getDatabase();
1986
- const id = parseInt(req.params.id, 10);
1987
- const reviewedBy = req.body?.reviewedBy ?? 'api';
1988
- const result = db.prepare('UPDATE quarantine SET status = ?, reviewed_at = ?, reviewed_by = ? WHERE id = ? AND status = ?').run('approved', new Date().toISOString(), reviewedBy, id, 'pending');
1989
- if (result.changes === 0) {
1990
- return res.status(404).json({ error: 'Quarantine entry not found or already reviewed' });
1991
- }
1992
- res.json({ success: true, id, status: 'approved' });
1993
- }
1994
- catch (error) {
1995
- res.status(500).json({ error: error.message });
1996
- }
1997
- });
1998
- // Reject quarantined item
1999
- app.post('/api/v1/quarantine/:id/reject', requireNotLocked, (req, res) => {
2000
- try {
2001
- const db = getDatabase();
2002
- const id = parseInt(req.params.id, 10);
2003
- const reviewedBy = req.body?.reviewedBy ?? 'api';
2004
- const notes = req.body?.notes ?? null;
2005
- const result = db.prepare('UPDATE quarantine SET status = ?, reviewed_at = ?, reviewed_by = ? WHERE id = ? AND status = ?').run('rejected', new Date().toISOString(), reviewedBy, id, 'pending');
2006
- if (result.changes === 0) {
2007
- return res.status(404).json({ error: 'Quarantine entry not found or already reviewed' });
2008
- }
2009
- res.json({ success: true, id, status: 'rejected' });
2010
- }
2011
- catch (error) {
2012
- res.status(500).json({ error: error.message });
2013
- }
2014
- });
2015
- // Bulk approve quarantined items
2016
- app.post('/api/v1/quarantine/bulk-approve', requireNotLocked, (req, res) => {
2017
- try {
2018
- const db = getDatabase();
2019
- const ids = req.body?.ids;
2020
- if (!Array.isArray(ids) || ids.length === 0) {
2021
- return res.status(400).json({ error: 'ids must be a non-empty array of numbers' });
2022
- }
2023
- const reviewedBy = req.body?.reviewedBy ?? 'dashboard';
2024
- const now = new Date().toISOString();
2025
- const stmt = db.prepare('UPDATE quarantine SET status = ?, reviewed_at = ?, reviewed_by = ? WHERE id = ? AND status = ?');
2026
- let updated = 0;
2027
- const txn = db.transaction(() => {
2028
- for (const id of ids) {
2029
- const result = stmt.run('approved', now, reviewedBy, id, 'pending');
2030
- updated += result.changes;
2031
- }
2032
- });
2033
- txn();
2034
- res.json({ success: true, updated, total: ids.length });
2035
- }
2036
- catch (error) {
2037
- res.status(500).json({ error: error.message });
2038
- }
2039
- });
2040
- // Bulk reject quarantined items
2041
- app.post('/api/v1/quarantine/bulk-reject', requireNotLocked, (req, res) => {
2042
- try {
2043
- const db = getDatabase();
2044
- const ids = req.body?.ids;
2045
- if (!Array.isArray(ids) || ids.length === 0) {
2046
- return res.status(400).json({ error: 'ids must be a non-empty array of numbers' });
2047
- }
2048
- const reviewedBy = req.body?.reviewedBy ?? 'dashboard';
2049
- const now = new Date().toISOString();
2050
- const stmt = db.prepare('UPDATE quarantine SET status = ?, reviewed_at = ?, reviewed_by = ? WHERE id = ? AND status = ?');
2051
- let updated = 0;
2052
- const txn = db.transaction(() => {
2053
- for (const id of ids) {
2054
- const result = stmt.run('rejected', now, reviewedBy, id, 'pending');
2055
- updated += result.changes;
2056
- }
2057
- });
2058
- txn();
2059
- res.json({ success: true, updated, total: ids.length });
2060
- }
2061
- catch (error) {
2062
- res.status(500).json({ error: error.message });
2063
- }
2064
- });
2065
- // Retroactive sync: push existing quarantine items to cloud
2066
- app.post('/api/quarantine/sync-to-cloud', requireNotLocked, async (_req, res) => {
2067
- try {
2068
- const config = getCloudConfig();
2069
- if (!config.cloudEnabled || !config.cloudApiKey) {
2070
- return res.status(400).json({ error: 'Cloud not configured. Enable cloud sync first.' });
2071
- }
2072
- const db = getDatabase();
2073
- const rows = db.prepare('SELECT * FROM quarantine WHERE status = ? ORDER BY created_at ASC').all('pending');
2074
- if (rows.length === 0) {
2075
- return res.json({ synced: 0, message: 'No pending quarantine items to sync.' });
2076
- }
2077
- let synced = 0;
2078
- const errors = [];
2079
- for (const row of rows) {
2080
- try {
2081
- const indicators = (() => {
2082
- try {
2083
- return JSON.parse(row.threat_indicators ?? '[]');
2084
- }
2085
- catch {
2086
- return [];
2087
- }
2088
- })();
2089
- const resp = await fetch(`${config.cloudBaseUrl}/v1/quarantine/ingest`, {
2090
- method: 'POST',
2091
- headers: {
2092
- 'Content-Type': 'application/json',
2093
- Authorization: `Bearer ${config.cloudApiKey}`,
2094
- },
2095
- body: JSON.stringify({
2096
- original_content: row.original_content,
2097
- original_title: row.original_title ?? undefined,
2098
- source_type: row.source_type ?? 'unknown',
2099
- source_identifier: row.source_identifier ?? 'unknown',
2100
- reason: row.reason ?? 'Unknown reason',
2101
- threat_indicators: indicators,
2102
- anomaly_score: row.anomaly_score ?? 0,
2103
- firewall_result: row.firewall_result ?? 'QUARANTINE',
2104
- }),
2105
- signal: AbortSignal.timeout(10_000),
2106
- });
2107
- if (resp.ok) {
2108
- synced++;
2109
- }
2110
- else {
2111
- const body = await resp.text().catch(() => '');
2112
- errors.push(`Item ${row.id}: ${resp.status} ${body.substring(0, 100)}`);
2113
- }
2114
- }
2115
- catch (e) {
2116
- errors.push(`Item ${row.id}: ${e.message}`);
2117
- }
2118
- }
2119
- res.json({ synced, total: rows.length, errors: errors.length > 0 ? errors : undefined });
2120
- }
2121
- catch (error) {
2122
- res.status(500).json({ error: error.message });
2123
- }
2124
- });
2125
- // Create and start the background brain worker
2126
545
  const brainWorker = new BrainWorker();
2127
- // Worker status endpoint
2128
- app.get('/api/worker/status', (_req, res) => {
2129
- try {
2130
- res.json(brainWorker.getStatus());
2131
- }
2132
- catch (error) {
2133
- res.status(500).json({ error: error.message });
2134
- }
2135
- });
2136
- // Manually trigger light tick (for testing)
2137
- app.post('/api/worker/trigger-light', requireNotLocked, async (_req, res) => {
2138
- try {
2139
- const result = await brainWorker.triggerLightTick();
2140
- res.json({
2141
- success: true,
2142
- ...result,
2143
- timestamp: result.timestamp.toISOString(),
2144
- });
2145
- }
2146
- catch (error) {
2147
- res.status(500).json({ error: error.message });
2148
- }
2149
- });
2150
- // Manually trigger medium tick (for testing)
2151
- app.post('/api/worker/trigger-medium', requireNotLocked, async (_req, res) => {
2152
- try {
2153
- const result = await brainWorker.triggerMediumTick();
2154
- res.json({
2155
- success: true,
2156
- ...result,
2157
- timestamp: result.timestamp.toISOString(),
2158
- });
2159
- }
2160
- catch (error) {
2161
- res.status(500).json({ error: error.message });
2162
- }
2163
- });
2164
- // ============================================
2165
- // LICENSE
2166
- // ============================================
2167
- app.get('/api/license/status', (_req, res) => {
2168
- try {
2169
- const info = getLicense();
2170
- const features = listFeatures();
2171
- res.json({
2172
- tier: info.tier,
2173
- valid: info.valid,
2174
- email: info.email,
2175
- expiresAt: info.expiresAt?.toISOString() ?? null,
2176
- daysUntilExpiry: info.daysUntilExpiry,
2177
- teamId: info.teamId,
2178
- features,
2179
- });
2180
- }
2181
- catch (error) {
2182
- res.status(500).json({ error: error.message });
2183
- }
2184
- });
2185
- app.post('/api/license/activate', async (req, res) => {
2186
- try {
2187
- const { key } = req.body;
2188
- if (!key || typeof key !== 'string') {
2189
- return res.status(400).json({ error: 'License key is required' });
2190
- }
2191
- const info = activateLicense(key.trim());
2192
- // Fire online validation (non-blocking but wait briefly for immediate feedback)
2193
- const validationStatus = await validateOnceNow();
2194
- const features = listFeatures();
2195
- res.json({
2196
- success: true,
2197
- tier: info.tier,
2198
- valid: info.valid,
2199
- email: info.email,
2200
- expiresAt: info.expiresAt?.toISOString() ?? null,
2201
- daysUntilExpiry: info.daysUntilExpiry,
2202
- validationStatus,
2203
- features,
2204
- });
2205
- }
2206
- catch (error) {
2207
- res.status(400).json({ error: error.message });
2208
- }
2209
- });
2210
- app.post('/api/license/deactivate', (_req, res) => {
2211
- try {
2212
- deactivateLicense();
2213
- const features = listFeatures();
2214
- res.json({
2215
- success: true,
2216
- tier: 'free',
2217
- features,
2218
- });
2219
- }
2220
- catch (error) {
2221
- res.status(500).json({ error: error.message });
2222
- }
2223
- });
2224
- // ============================================
2225
- // PRO FEATURE ENDPOINTS
2226
- // ============================================
2227
- // ── Custom Firewall Rules ────────────────────
2228
- app.get('/api/firewall-rules', requireProFeature('custom_firewall_rules'), (_req, res) => {
2229
- try {
2230
- const { listFirewallRules } = require('../defence/custom-rules/store.js');
2231
- const rules = listFirewallRules();
2232
- res.json({ rules, total: rules.length });
2233
- }
2234
- catch (error) {
2235
- res.status(500).json({ error: error.message });
2236
- }
2237
- });
2238
- app.post('/api/firewall-rules', requireProFeature('custom_firewall_rules'), (req, res) => {
2239
- try {
2240
- const { createFirewallRule } = require('../defence/custom-rules/store.js');
2241
- const { name, priority, condition_type, condition_value, action } = req.body;
2242
- if (!name || !condition_type || !condition_value || !action) {
2243
- return res.status(400).json({ error: 'name, condition_type, condition_value, and action are required' });
2244
- }
2245
- const rule = createFirewallRule({ name, priority: priority ?? 100, condition_type, condition_value, action });
2246
- res.status(201).json(rule);
2247
- }
2248
- catch (error) {
2249
- const msg = error.message;
2250
- const status = msg.includes('Maximum') ? 400 : 500;
2251
- res.status(status).json({ error: msg });
2252
- }
2253
- });
2254
- app.patch('/api/firewall-rules/:id', requireProFeature('custom_firewall_rules'), (req, res) => {
2255
- try {
2256
- const { updateFirewallRule } = require('../defence/custom-rules/store.js');
2257
- const rule = updateFirewallRule(Number(req.params.id), req.body);
2258
- if (!rule)
2259
- return res.status(404).json({ error: 'Rule not found' });
2260
- res.json(rule);
2261
- }
2262
- catch (error) {
2263
- res.status(500).json({ error: error.message });
2264
- }
2265
- });
2266
- app.delete('/api/firewall-rules/:id', requireProFeature('custom_firewall_rules'), (req, res) => {
2267
- try {
2268
- const { deleteFirewallRule } = require('../defence/custom-rules/store.js');
2269
- const deleted = deleteFirewallRule(Number(req.params.id));
2270
- if (!deleted)
2271
- return res.status(404).json({ error: 'Rule not found' });
2272
- res.json({ success: true, id: Number(req.params.id) });
2273
- }
2274
- catch (error) {
2275
- res.status(500).json({ error: error.message });
2276
- }
2277
- });
2278
- // ── Custom Injection Patterns ────────────────
2279
- app.get('/api/patterns', requireProFeature('custom_injection_patterns'), (_req, res) => {
2280
- try {
2281
- const { listCustomPatterns } = require('../defence/custom-patterns/store.js');
2282
- const patterns = listCustomPatterns();
2283
- res.json({ patterns, total: patterns.length });
2284
- }
2285
- catch (error) {
2286
- res.status(500).json({ error: error.message });
2287
- }
2288
- });
2289
- app.post('/api/patterns', requireProFeature('custom_injection_patterns'), (req, res) => {
2290
- try {
2291
- const { createCustomPattern, validateRegex } = require('../defence/custom-patterns/store.js');
2292
- const { name, category, severity, regex, description } = req.body;
2293
- if (!name || !regex) {
2294
- return res.status(400).json({ error: 'name and regex are required' });
2295
- }
2296
- // Validate regex safety before creating
2297
- const validation = validateRegex(regex);
2298
- if (!validation.valid) {
2299
- return res.status(400).json({ error: validation.error });
2300
- }
2301
- const pattern = createCustomPattern({
2302
- name,
2303
- category: category || 'custom',
2304
- severity: severity || 'medium',
2305
- regex,
2306
- description,
2307
- });
2308
- res.status(201).json(pattern);
2309
- }
2310
- catch (error) {
2311
- const msg = error.message;
2312
- const status = msg.includes('Maximum') || msg.includes('Invalid') || msg.includes('rejected') ? 400 : 500;
2313
- res.status(status).json({ error: msg });
2314
- }
2315
- });
2316
- app.delete('/api/patterns/:id', requireProFeature('custom_injection_patterns'), (req, res) => {
2317
- try {
2318
- const { deleteCustomPattern } = require('../defence/custom-patterns/store.js');
2319
- const deleted = deleteCustomPattern(Number(req.params.id));
2320
- if (!deleted)
2321
- return res.status(404).json({ error: 'Pattern not found' });
2322
- res.json({ success: true, id: Number(req.params.id) });
2323
- }
2324
- catch (error) {
2325
- res.status(500).json({ error: error.message });
2326
- }
2327
- });
2328
- app.post('/api/patterns/:id/test', requireProFeature('custom_injection_patterns'), (req, res) => {
2329
- try {
2330
- const { testPattern } = require('../defence/custom-patterns/store.js');
2331
- const { text } = req.body;
2332
- if (!text)
2333
- return res.status(400).json({ error: 'text is required' });
2334
- const result = testPattern(Number(req.params.id), text);
2335
- res.json(result);
2336
- }
2337
- catch (error) {
2338
- res.status(500).json({ error: error.message });
2339
- }
2340
- });
2341
- // ── Custom Iron Dome Policies ────────────────
2342
- app.get('/api/iron-dome/policies', requireProFeature('custom_iron_dome_policies'), (_req, res) => {
2343
- try {
2344
- const { listIronDomePolicies } = require('../defence/iron-dome/custom-policies.js');
2345
- const policies = listIronDomePolicies();
2346
- res.json({ policies, total: policies.length });
2347
- }
2348
- catch (error) {
2349
- res.status(500).json({ error: error.message });
2350
- }
2351
- });
2352
- app.post('/api/iron-dome/policies', requireProFeature('custom_iron_dome_policies'), (req, res) => {
2353
- try {
2354
- const { createIronDomePolicy } = require('../defence/iron-dome/custom-policies.js');
2355
- const { name, description, config } = req.body;
2356
- if (!name)
2357
- return res.status(400).json({ error: 'name is required' });
2358
- const policy = createIronDomePolicy({ name, description, config: config || {} });
2359
- res.status(201).json(policy);
2360
- }
2361
- catch (error) {
2362
- const msg = error.message;
2363
- const status = msg.includes('Maximum') ? 400 : 500;
2364
- res.status(status).json({ error: msg });
2365
- }
2366
- });
2367
- app.delete('/api/iron-dome/policies/:id', requireProFeature('custom_iron_dome_policies'), (req, res) => {
2368
- try {
2369
- const { deleteIronDomePolicy } = require('../defence/iron-dome/custom-policies.js');
2370
- const deleted = deleteIronDomePolicy(Number(req.params.id));
2371
- if (!deleted)
2372
- return res.status(404).json({ error: 'Policy not found' });
2373
- res.json({ success: true, id: Number(req.params.id) });
2374
- }
2375
- catch (error) {
2376
- res.status(500).json({ error: error.message });
2377
- }
2378
- });
2379
- app.put('/api/iron-dome/policies/:id/activate', requireProFeature('custom_iron_dome_policies'), (req, res) => {
2380
- try {
2381
- const { activateIronDomePolicy } = require('../defence/iron-dome/custom-policies.js');
2382
- const policy = activateIronDomePolicy(Number(req.params.id));
2383
- if (!policy)
2384
- return res.status(404).json({ error: 'Policy not found' });
2385
- res.json(policy);
2386
- }
2387
- catch (error) {
2388
- res.status(500).json({ error: error.message });
2389
- }
2390
- });
2391
- // ── Audit Export ─────────────────────────────
2392
- app.get('/api/audit/export', requireProFeature('audit_export'), (req, res) => {
2393
- try {
2394
- const { exportAuditJSON, exportAuditCSV } = require('../defence/audit/export.js');
2395
- const format = req.query.format || 'json';
2396
- const startTime = req.query.startTime;
2397
- const endTime = req.query.endTime;
2398
- if (format === 'csv') {
2399
- const csv = exportAuditCSV(startTime, endTime);
2400
- res.setHeader('Content-Type', 'text/csv');
2401
- res.setHeader('Content-Disposition', `attachment; filename="shieldcortex-audit-${Date.now()}.csv"`);
2402
- res.send(csv);
2403
- }
2404
- else {
2405
- const json = exportAuditJSON(startTime, endTime);
2406
- res.setHeader('Content-Type', 'application/json');
2407
- res.setHeader('Content-Disposition', `attachment; filename="shieldcortex-audit-${Date.now()}.json"`);
2408
- res.send(json);
2409
- }
2410
- }
2411
- catch (error) {
2412
- res.status(500).json({ error: error.message });
2413
- }
2414
- });
2415
- // ── Deep Skill Scanner ───────────────────────
2416
- app.post('/api/skills/deep-scan', requireProFeature('skill_scanner_deep'), async (req, res) => {
2417
- try {
2418
- const { runDeepScan } = require('../defence/skill-scanner/deep-scan.js');
2419
- const { files } = req.body;
2420
- if (!files || !Array.isArray(files) || files.length === 0) {
2421
- return res.status(400).json({ error: 'files array is required (each with name and content)' });
2422
- }
2423
- const result = await runDeepScan(files);
2424
- res.json(result);
2425
- }
2426
- catch (error) {
2427
- res.status(500).json({ error: error.message });
2428
- }
2429
- });
546
+ registerIncidentRoutes(app);
547
+ registerAdminRoutes(app, { brainWorker, requireNotLocked, requireProFeature });
2430
548
  // ============================================
2431
549
  // WEBSOCKET SERVER
2432
550
  // ============================================