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.
- package/dashboard/.next/standalone/dashboard/.next/BUILD_ID +1 -1
- package/dashboard/.next/standalone/dashboard/.next/build-manifest.json +2 -2
- package/dashboard/.next/standalone/dashboard/.next/prerender-manifest.json +3 -3
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.html +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.html +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.rsc +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.html +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.rsc +3 -3
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/app/page/react-loadable-manifest.json +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/chunks/ssr/dashboard_3051539d._.js +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/pages/404.html +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/pages/500.html +2 -2
- package/dashboard/.next/standalone/dashboard/.next/server/server-reference-manifest.js +1 -1
- package/dashboard/.next/standalone/dashboard/.next/server/server-reference-manifest.json +1 -1
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/313c0d327bbf244a.js +9 -0
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/{fa5217550a8ab9a6.js → 49c1cec591af1460.js} +2 -2
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/{f69fd1c5e71fbbfd.js → ca21f348cb163905.js} +1 -1
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/f4ca424319f58dc7.css +3 -0
- package/dist/api/routes/admin.d.ts +12 -0
- package/dist/api/routes/admin.js +502 -0
- package/dist/api/routes/graph.d.ts +4 -0
- package/dist/api/routes/graph.js +333 -0
- package/dist/api/routes/incidents.d.ts +2 -0
- package/dist/api/routes/incidents.js +32 -0
- package/dist/api/routes/memories.d.ts +4 -0
- package/dist/api/routes/memories.js +659 -0
- package/dist/api/routes/recall.d.ts +4 -0
- package/dist/api/routes/recall.js +36 -0
- package/dist/api/routes/system.d.ts +9 -0
- package/dist/api/routes/system.js +201 -0
- package/dist/api/visualization-server.js +31 -1913
- package/dist/memory/search.d.ts +37 -0
- package/dist/memory/search.js +143 -0
- package/dist/memory/store.js +2 -166
- package/dist/tools/forget.d.ts +2 -2
- package/dist/tools/recall.d.ts +2 -2
- package/hooks/openclaw/cortex-memory/handler.ts +5 -141
- package/hooks/openclaw/cortex-memory/runtime.mjs +129 -0
- package/package.json +8 -4
- package/plugins/openclaw/dist/index.js +5 -39
- package/scripts/run-jest.mjs +25 -1
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/be6970da20a17c0b.js +0 -9
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/e63d2228780629dd.css +0 -3
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/_tsc.js +0 -133818
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/_tsserver.js +0 -659
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/_typingsInstaller.js +0 -222
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/cs/diagnosticMessages.generated.json +0 -2122
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/de/diagnosticMessages.generated.json +0 -2122
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/es/diagnosticMessages.generated.json +0 -2122
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/fr/diagnosticMessages.generated.json +0 -2122
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/it/diagnosticMessages.generated.json +0 -2122
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/ja/diagnosticMessages.generated.json +0 -2122
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/ko/diagnosticMessages.generated.json +0 -2122
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/pl/diagnosticMessages.generated.json +0 -2122
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/pt-br/diagnosticMessages.generated.json +0 -2122
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/ru/diagnosticMessages.generated.json +0 -2122
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/tr/diagnosticMessages.generated.json +0 -2122
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/tsc.js +0 -8
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/tsserver.js +0 -8
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/tsserverlibrary.js +0 -21
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/typesMap.json +0 -497
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/typescript.js +0 -200276
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/typingsInstaller.js +0 -8
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/watchGuard.js +0 -53
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/zh-cn/diagnosticMessages.generated.json +0 -2122
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/lib/zh-tw/diagnosticMessages.generated.json +0 -2122
- package/dashboard/.next/standalone/dashboard/node_modules/typescript/package.json +0 -120
- package/scripts/start-dashboard.sh +0 -41
- package/scripts/stop-dashboard.sh +0 -21
- /package/dashboard/.next/standalone/dashboard/.next/static/{THy6JENQ0c1sq6jQhvIDp → BEvyMAX62LQMyt5iSb-F9}/_buildManifest.js +0 -0
- /package/dashboard/.next/standalone/dashboard/.next/static/{THy6JENQ0c1sq6jQhvIDp → BEvyMAX62LQMyt5iSb-F9}/_clientMiddlewareManifest.json +0 -0
- /package/dashboard/.next/standalone/dashboard/.next/static/{THy6JENQ0c1sq6jQhvIDp → BEvyMAX62LQMyt5iSb-F9}/_ssgManifest.js +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 {
|
|
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 {
|
|
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 {
|
|
25
|
-
import {
|
|
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 {
|
|
24
|
+
import { queryAgentOperations } from '../defence/audit/queries.js';
|
|
29
25
|
import { logAudit } from '../defence/audit/index.js';
|
|
30
|
-
import { getCloudConfig,
|
|
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 {
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
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
|
-
|
|
162
|
-
app
|
|
163
|
-
|
|
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
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
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
|
-
|
|
2128
|
-
app
|
|
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
|
// ============================================
|