shieldcortex 3.0.4 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +21 -2
  2. package/dashboard/.next/standalone/dashboard/.next/BUILD_ID +1 -1
  3. package/dashboard/.next/standalone/dashboard/.next/build-manifest.json +2 -2
  4. package/dashboard/.next/standalone/dashboard/.next/prerender-manifest.json +3 -3
  5. package/dashboard/.next/standalone/dashboard/.next/required-server-files.json +4 -4
  6. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.html +2 -2
  7. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.rsc +1 -1
  8. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  11. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  12. package/dashboard/.next/standalone/dashboard/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.html +1 -1
  15. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.rsc +2 -2
  16. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  17. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  18. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  19. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  20. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  21. package/dashboard/.next/standalone/dashboard/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  22. package/dashboard/.next/standalone/dashboard/.next/server/app/index.html +1 -1
  23. package/dashboard/.next/standalone/dashboard/.next/server/app/index.rsc +3 -3
  24. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  25. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_full.segment.rsc +3 -3
  26. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_head.segment.rsc +1 -1
  27. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_index.segment.rsc +2 -2
  28. package/dashboard/.next/standalone/dashboard/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  29. package/dashboard/.next/standalone/dashboard/.next/server/app/page/react-loadable-manifest.json +1 -1
  30. package/dashboard/.next/standalone/dashboard/.next/server/app/page_client-reference-manifest.js +1 -1
  31. package/dashboard/.next/standalone/dashboard/.next/server/chunks/ssr/dashboard_3051539d._.js +1 -1
  32. package/dashboard/.next/standalone/dashboard/.next/server/pages/404.html +1 -1
  33. package/dashboard/.next/standalone/dashboard/.next/server/pages/500.html +2 -2
  34. package/dashboard/.next/standalone/dashboard/.next/server/server-reference-manifest.js +1 -1
  35. package/dashboard/.next/standalone/dashboard/.next/server/server-reference-manifest.json +1 -1
  36. package/dashboard/.next/standalone/dashboard/.next/static/chunks/9232a2d99b47b21f.js +3 -0
  37. package/dashboard/.next/standalone/dashboard/.next/static/chunks/98e2c181d5c4349f.js +1 -0
  38. package/dashboard/.next/standalone/dashboard/.next/static/chunks/9cb86821c1107fd6.js +9 -0
  39. package/dashboard/.next/standalone/dashboard/.next/static/chunks/a56c497e02afd4ba.css +3 -0
  40. package/dashboard/.next/standalone/dashboard/.next/static/chunks/a90355d73183a5e6.js +1 -0
  41. package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-darwin-arm64/lib/sharp-darwin-arm64.node +0 -0
  42. package/dashboard/.next/standalone/{node_modules/@img/sharp-linux-x64 → dashboard/node_modules/@img/sharp-darwin-arm64}/package.json +7 -13
  43. package/dashboard/.next/standalone/dashboard/node_modules/@img/{sharp-libvips-linux-x64 → sharp-libvips-darwin-arm64}/README.md +2 -2
  44. package/dashboard/.next/standalone/dashboard/node_modules/@img/{sharp-libvips-linux-x64 → sharp-libvips-darwin-arm64}/lib/glib-2.0/include/glibconfig.h +8 -9
  45. package/dashboard/.next/standalone/dashboard/node_modules/@img/{sharp-libvips-linux-x64/lib/libvips-cpp.so.8.17.3 → sharp-libvips-darwin-arm64/lib/libvips-cpp.8.17.3.dylib} +0 -0
  46. package/dashboard/.next/standalone/dashboard/node_modules/@img/{sharp-libvips-linux-x64 → sharp-libvips-darwin-arm64}/package.json +5 -11
  47. package/dashboard/.next/standalone/dashboard/server.js +1 -1
  48. package/dashboard/.next/standalone/{dashboard/node_modules/@img/sharp-linux-x64 → node_modules/@img/sharp-darwin-arm64}/package.json +7 -13
  49. package/dashboard/.next/standalone/node_modules/@img/{sharp-libvips-linux-x64 → sharp-libvips-darwin-arm64}/package.json +5 -11
  50. package/dist/api/routes/memories.js +366 -1
  51. package/dist/api/routes/recall.js +53 -0
  52. package/dist/api/routes/system.js +67 -2
  53. package/dist/cloud/cli.d.ts +1 -0
  54. package/dist/cloud/cli.js +40 -0
  55. package/dist/cloud/config.d.ts +10 -0
  56. package/dist/cloud/config.js +54 -0
  57. package/dist/cloud/graph-sync.d.ts +45 -0
  58. package/dist/cloud/graph-sync.js +260 -0
  59. package/dist/cloud/memory-sync.d.ts +37 -0
  60. package/dist/cloud/memory-sync.js +186 -0
  61. package/dist/cloud/sync-queue.d.ts +24 -0
  62. package/dist/cloud/sync-queue.js +126 -7
  63. package/dist/database/init.js +53 -0
  64. package/dist/graph/backfill.js +3 -5
  65. package/dist/graph/resolve.d.ts +10 -0
  66. package/dist/graph/resolve.js +63 -1
  67. package/dist/index.d.ts +2 -0
  68. package/dist/index.js +61 -4
  69. package/dist/memory/search.d.ts +1 -0
  70. package/dist/memory/search.js +4 -0
  71. package/dist/memory/store.js +188 -30
  72. package/dist/memory/types.d.ts +33 -0
  73. package/dist/service/install.d.ts +1 -0
  74. package/dist/service/install.js +43 -1
  75. package/dist/tools/context.d.ts +4 -4
  76. package/dist/tools/forget.d.ts +6 -6
  77. package/dist/tools/recall.d.ts +11 -11
  78. package/dist/tools/remember.d.ts +19 -4
  79. package/dist/tools/remember.js +17 -1
  80. package/hooks/openclaw/cortex-memory/handler.ts +8 -0
  81. package/package.json +1 -1
  82. package/dashboard/.next/standalone/dashboard/.next/static/chunks/313c0d327bbf244a.js +0 -9
  83. package/dashboard/.next/standalone/dashboard/.next/static/chunks/3cc7e8d4f73cf5d2.js +0 -1
  84. package/dashboard/.next/standalone/dashboard/.next/static/chunks/49c1cec591af1460.js +0 -3
  85. package/dashboard/.next/standalone/dashboard/.next/static/chunks/ca21f348cb163905.js +0 -1
  86. package/dashboard/.next/standalone/dashboard/.next/static/chunks/f4ca424319f58dc7.css +0 -3
  87. package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-libvips-linuxmusl-x64/README.md +0 -46
  88. package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/glib-2.0/include/glibconfig.h +0 -221
  89. package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/index.js +0 -1
  90. package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/libvips-cpp.so.8.17.3 +0 -0
  91. package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json +0 -42
  92. package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-libvips-linuxmusl-x64/versions.json +0 -30
  93. package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-linux-x64/lib/sharp-linux-x64.node +0 -0
  94. package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-linuxmusl-x64/lib/sharp-linuxmusl-x64.node +0 -0
  95. package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-linuxmusl-x64/package.json +0 -46
  96. package/dashboard/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json +0 -42
  97. package/dashboard/.next/standalone/node_modules/@img/sharp-linuxmusl-x64/package.json +0 -46
  98. /package/dashboard/.next/standalone/dashboard/.next/static/{BEvyMAX62LQMyt5iSb-F9 → ctp9eCBcHDpTWtUYMwJK7}/_buildManifest.js +0 -0
  99. /package/dashboard/.next/standalone/dashboard/.next/static/{BEvyMAX62LQMyt5iSb-F9 → ctp9eCBcHDpTWtUYMwJK7}/_clientMiddlewareManifest.json +0 -0
  100. /package/dashboard/.next/standalone/dashboard/.next/static/{BEvyMAX62LQMyt5iSb-F9 → ctp9eCBcHDpTWtUYMwJK7}/_ssgManifest.js +0 -0
  101. /package/dashboard/.next/standalone/dashboard/node_modules/@img/{sharp-libvips-linux-x64 → sharp-libvips-darwin-arm64}/lib/index.js +0 -0
  102. /package/dashboard/.next/standalone/dashboard/node_modules/@img/{sharp-libvips-linux-x64 → sharp-libvips-darwin-arm64}/versions.json +0 -0
@@ -1,3 +1,6 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { existsSync, readdirSync, readFileSync } from 'fs';
1
4
  import { getDatabase } from '../../database/init.js';
2
5
  import { searchMemories, getRecentMemories, getHighPriorityMemories, getMemoryStats, getMemoryById, addMemory, deleteMemory, accessMemory, updateMemory, promoteMemory, createMemoryLink, rowToMemory, enrichMemory, } from '../../memory/store.js';
3
6
  import { calculateDecayedScore } from '../../memory/decay.js';
@@ -6,6 +9,185 @@ import { getActivationStats, getActiveMemories } from '../../memory/activation.j
6
9
  import { detectContradictions, getContradictionsFor } from '../../memory/contradiction.js';
7
10
  import { emitConsolidation } from '../events.js';
8
11
  export function registerMemoryRoutes(app, requireNotLocked) {
12
+ app.get('/api/capture/openclaw/sessions', requireNotLocked, (req, res) => {
13
+ try {
14
+ const limit = Math.min(parseInt(req.query.limit, 10) || 20, 100);
15
+ const auditDir = join(homedir(), '.shieldcortex', 'audit');
16
+ const db = getDatabase();
17
+ const rows = db.prepare(`
18
+ SELECT * FROM memories
19
+ WHERE source_kind IN ('hook', 'plugin') OR source LIKE 'hook:openclaw%' OR source LIKE 'agent:openclaw-plugin%'
20
+ ORDER BY updated_at DESC
21
+ LIMIT 500
22
+ `).all();
23
+ const openClawMemories = rows.map(rowToMemory);
24
+ const sessionMap = new Map();
25
+ const getSession = (sessionId) => {
26
+ let session = sessionMap.get(sessionId);
27
+ if (!session) {
28
+ session = {
29
+ sessionId,
30
+ firstSeenAt: new Date().toISOString(),
31
+ lastSeenAt: new Date(0).toISOString(),
32
+ storedMemoryCount: 0,
33
+ loggedSaved: 0,
34
+ skipped: 0,
35
+ threats: 0,
36
+ blocked: 0,
37
+ quarantined: 0,
38
+ autoExtracted: 0,
39
+ keywordTriggered: 0,
40
+ pinned: 0,
41
+ suppressed: 0,
42
+ hooks: new Set(),
43
+ models: new Set(),
44
+ agentIds: new Set(),
45
+ memoryIds: new Set(),
46
+ previews: [],
47
+ events: [],
48
+ };
49
+ sessionMap.set(sessionId, session);
50
+ }
51
+ return session;
52
+ };
53
+ for (const memory of openClawMemories) {
54
+ const sessionId = typeof memory.metadata?.sessionId === 'string'
55
+ ? memory.metadata.sessionId
56
+ : memory.source?.startsWith('agent:openclaw-plugin:')
57
+ ? memory.source.slice('agent:openclaw-plugin:'.length)
58
+ : null;
59
+ if (!sessionId)
60
+ continue;
61
+ const session = getSession(sessionId);
62
+ const createdAt = memory.createdAt.toISOString();
63
+ if (createdAt < session.firstSeenAt)
64
+ session.firstSeenAt = createdAt;
65
+ if (createdAt > session.lastSeenAt)
66
+ session.lastSeenAt = createdAt;
67
+ session.storedMemoryCount += 1;
68
+ session.memoryIds.add(memory.id);
69
+ if (typeof memory.metadata?.agentId === 'string')
70
+ session.agentIds.add(memory.metadata.agentId);
71
+ if (memory.captureMethod === 'auto' || memory.captureMethod === 'plugin' || memory.captureMethod === 'hook') {
72
+ session.autoExtracted += 1;
73
+ }
74
+ if (typeof memory.metadata?.trigger === 'string' || memory.tags.includes('keyword_trigger')) {
75
+ session.keywordTriggered += 1;
76
+ }
77
+ if (memory.pinned)
78
+ session.pinned += 1;
79
+ if (memory.status === 'suppressed')
80
+ session.suppressed += 1;
81
+ }
82
+ if (existsSync(auditDir)) {
83
+ const files = readdirSync(auditDir)
84
+ .filter((file) => /^realtime-\d{4}-\d{2}-\d{2}\.jsonl$/.test(file))
85
+ .sort()
86
+ .slice(-14);
87
+ for (const file of files) {
88
+ const raw = readFileSync(join(auditDir, file), 'utf-8');
89
+ for (const line of raw.split('\n')) {
90
+ if (!line.trim())
91
+ continue;
92
+ try {
93
+ const entry = JSON.parse(line);
94
+ const sessionId = typeof entry.sessionId === 'string' ? entry.sessionId : null;
95
+ if (!sessionId)
96
+ continue;
97
+ const session = getSession(sessionId);
98
+ const ts = typeof entry.ts === 'string' ? entry.ts : new Date().toISOString();
99
+ if (ts < session.firstSeenAt)
100
+ session.firstSeenAt = ts;
101
+ if (ts > session.lastSeenAt)
102
+ session.lastSeenAt = ts;
103
+ if (typeof entry.hook === 'string')
104
+ session.hooks.add(entry.hook);
105
+ if (typeof entry.model === 'string')
106
+ session.models.add(entry.model);
107
+ if (entry.type === 'memory') {
108
+ const count = Number(entry.count ?? 0);
109
+ const skipped = Number(entry.skipped ?? 0);
110
+ session.loggedSaved += count;
111
+ session.skipped += skipped;
112
+ session.events.push({
113
+ ts,
114
+ type: 'memory',
115
+ hook: typeof entry.hook === 'string' ? entry.hook : undefined,
116
+ model: typeof entry.model === 'string' ? entry.model : undefined,
117
+ preview: typeof entry.preview === 'string' ? entry.preview : undefined,
118
+ count,
119
+ skipped,
120
+ });
121
+ if (typeof entry.preview === 'string' && session.previews.length < 6) {
122
+ session.previews.push(entry.preview);
123
+ }
124
+ }
125
+ if (entry.type === 'threat') {
126
+ session.threats += 1;
127
+ const preview = typeof entry.preview === 'string' ? entry.preview : undefined;
128
+ const lower = preview?.toLowerCase() ?? '';
129
+ if (lower.includes('quarantine'))
130
+ session.quarantined += 1;
131
+ if (lower.includes('block'))
132
+ session.blocked += 1;
133
+ session.events.push({
134
+ ts,
135
+ type: lower.includes('quarantine') ? 'quarantine' : lower.includes('block') ? 'blocked' : 'threat',
136
+ hook: typeof entry.hook === 'string' ? entry.hook : undefined,
137
+ model: typeof entry.model === 'string' ? entry.model : undefined,
138
+ preview,
139
+ });
140
+ if (preview && session.previews.length < 6) {
141
+ session.previews.push(preview);
142
+ }
143
+ }
144
+ }
145
+ catch {
146
+ // ignore malformed lines
147
+ }
148
+ }
149
+ }
150
+ }
151
+ const sessions = Array.from(sessionMap.values())
152
+ .map((session) => ({
153
+ sessionId: session.sessionId,
154
+ firstSeenAt: session.firstSeenAt,
155
+ lastSeenAt: session.lastSeenAt,
156
+ saved: Math.max(session.storedMemoryCount, session.loggedSaved),
157
+ skipped: session.skipped,
158
+ threats: session.threats,
159
+ blocked: session.blocked,
160
+ quarantined: session.quarantined,
161
+ autoExtracted: session.autoExtracted,
162
+ keywordTriggered: session.keywordTriggered,
163
+ pinned: session.pinned,
164
+ suppressed: session.suppressed,
165
+ hooks: Array.from(session.hooks),
166
+ models: Array.from(session.models),
167
+ agentIds: Array.from(session.agentIds),
168
+ previews: session.previews,
169
+ events: session.events
170
+ .sort((a, b) => new Date(b.ts).getTime() - new Date(a.ts).getTime())
171
+ .slice(0, 24),
172
+ memories: openClawMemories
173
+ .filter((memory) => session.memoryIds.has(memory.id))
174
+ .sort((a, b) => new Date(b.updatedAt ?? b.createdAt).getTime() - new Date(a.updatedAt ?? a.createdAt).getTime())
175
+ .slice(0, 12),
176
+ }))
177
+ .sort((a, b) => new Date(b.lastSeenAt).getTime() - new Date(a.lastSeenAt).getTime())
178
+ .slice(0, limit);
179
+ const summary = {
180
+ sessions: sessions.length,
181
+ saved: sessions.reduce((sum, session) => sum + session.saved, 0),
182
+ skipped: sessions.reduce((sum, session) => sum + session.skipped, 0),
183
+ threats: sessions.reduce((sum, session) => sum + session.threats, 0),
184
+ };
185
+ res.json({ summary, sessions });
186
+ }
187
+ catch (error) {
188
+ res.status(500).json({ error: error.message });
189
+ }
190
+ });
9
191
  app.get('/api/memories', requireNotLocked, async (req, res) => {
10
192
  try {
11
193
  const project = typeof req.query.project === 'string' ? req.query.project : undefined;
@@ -110,11 +292,127 @@ export function registerMemoryRoutes(app, requireNotLocked) {
110
292
  JOIN memories m2 ON m1.title = m2.title AND m1.id < m2.id
111
293
  ${project ? 'WHERE m1.project = ?' : ''}
112
294
  LIMIT 50
295
+ `).all(...params);
296
+ const lowTrust = db.prepare(`
297
+ SELECT id, title, category, project, trust_score, source_kind, capture_method
298
+ FROM memories WHERE trust_score < 0.7 ${projectFilter}
299
+ ORDER BY trust_score ASC, updated_at DESC LIMIT 50
300
+ `).all(...params);
301
+ const noisyAutoExtracted = db.prepare(`
302
+ SELECT id, title, category, project, source_kind, capture_method, tags, trust_score
303
+ FROM memories
304
+ WHERE (capture_method = 'auto' OR tags LIKE '%auto-extracted%') ${projectFilter}
305
+ ORDER BY updated_at DESC LIMIT 50
306
+ `).all(...params);
307
+ const projectless = db.prepare(`
308
+ SELECT id, title, category, scope, source_kind, capture_method
309
+ FROM memories
310
+ WHERE (project IS NULL OR project = '') AND scope != 'global'
311
+ ORDER BY updated_at DESC LIMIT 50
312
+ `).all();
313
+ const statusCounts = db.prepare(`
314
+ SELECT status, COUNT(*) as count
315
+ FROM memories
316
+ ${project ? 'WHERE project = ?' : ''}
317
+ GROUP BY status
113
318
  `).all(...params);
114
319
  res.json({
115
320
  neverAccessed: { count: neverAccessed.length, items: neverAccessed },
116
321
  stale: { count: stale.length, items: stale },
117
322
  duplicates: { count: duplicates.length, items: duplicates },
323
+ lowTrust: { count: lowTrust.length, items: lowTrust },
324
+ noisyAutoExtracted: { count: noisyAutoExtracted.length, items: noisyAutoExtracted },
325
+ projectless: { count: projectless.length, items: projectless },
326
+ status: statusCounts.reduce((acc, row) => {
327
+ acc[row.status] = row.count;
328
+ return acc;
329
+ }, {}),
330
+ });
331
+ }
332
+ catch (error) {
333
+ res.status(500).json({ error: error.message });
334
+ }
335
+ });
336
+ app.get('/api/review/queue', requireNotLocked, (req, res) => {
337
+ try {
338
+ const project = typeof req.query.project === 'string' ? req.query.project : undefined;
339
+ const limit = Math.min(parseInt(req.query.limit, 10) || 20, 100);
340
+ const db = getDatabase();
341
+ const projectFilter = project ? 'AND project = ?' : '';
342
+ const params = project ? [project] : [];
343
+ const stale = db.prepare(`
344
+ SELECT * FROM memories
345
+ WHERE decayed_score < 0.3 ${projectFilter}
346
+ AND last_accessed < datetime('now', '-30 days')
347
+ ORDER BY decayed_score ASC LIMIT ?
348
+ `).all(...params, limit);
349
+ const neverUsed = db.prepare(`
350
+ SELECT * FROM memories
351
+ WHERE access_count = 0 ${projectFilter}
352
+ AND created_at < datetime('now', '-1 day')
353
+ ORDER BY created_at DESC LIMIT ?
354
+ `).all(...params, limit);
355
+ const lowTrust = db.prepare(`
356
+ SELECT * FROM memories
357
+ WHERE trust_score < 0.7 ${projectFilter}
358
+ ORDER BY trust_score ASC, updated_at DESC LIMIT ?
359
+ `).all(...params, limit);
360
+ const noisyAutoExtracted = db.prepare(`
361
+ SELECT * FROM memories
362
+ WHERE (capture_method = 'auto' OR tags LIKE '%auto-extracted%') ${projectFilter}
363
+ ORDER BY updated_at DESC LIMIT ?
364
+ `).all(...params, limit);
365
+ const projectless = db.prepare(`
366
+ SELECT * FROM memories
367
+ WHERE (project IS NULL OR project = '') AND scope != 'global'
368
+ ORDER BY updated_at DESC LIMIT ?
369
+ `).all(limit);
370
+ const openClawSummary = db.prepare(`
371
+ SELECT
372
+ COUNT(*) as total,
373
+ SUM(CASE WHEN capture_method = 'auto' THEN 1 ELSE 0 END) as auto_count,
374
+ SUM(CASE WHEN tags LIKE '%keyword-trigger%' THEN 1 ELSE 0 END) as keyword_count,
375
+ SUM(CASE WHEN status = 'suppressed' THEN 1 ELSE 0 END) as suppressed_count,
376
+ SUM(CASE WHEN pinned = 1 THEN 1 ELSE 0 END) as pinned_count
377
+ FROM memories
378
+ WHERE (source_kind IN ('hook', 'plugin') OR tags LIKE '%openclaw%')
379
+ ${project ? 'AND project = ?' : ''}
380
+ `).get(...params);
381
+ const contradictions = detectContradictions({
382
+ project,
383
+ minScore: 0.4,
384
+ limit,
385
+ });
386
+ res.json({
387
+ summary: {
388
+ stale: stale.length,
389
+ neverUsed: neverUsed.length,
390
+ lowTrust: lowTrust.length,
391
+ noisyAutoExtracted: noisyAutoExtracted.length,
392
+ projectless: projectless.length,
393
+ contradictions: contradictions.length,
394
+ },
395
+ openClaw: {
396
+ total: openClawSummary.total ?? 0,
397
+ autoExtracted: openClawSummary.auto_count ?? 0,
398
+ keywordTriggered: openClawSummary.keyword_count ?? 0,
399
+ suppressed: openClawSummary.suppressed_count ?? 0,
400
+ pinned: openClawSummary.pinned_count ?? 0,
401
+ },
402
+ sections: {
403
+ stale: stale.map(rowToMemory),
404
+ neverUsed: neverUsed.map(rowToMemory),
405
+ lowTrust: lowTrust.map(rowToMemory),
406
+ noisyAutoExtracted: noisyAutoExtracted.map(rowToMemory),
407
+ projectless: projectless.map(rowToMemory),
408
+ contradictions: contradictions.map((item) => ({
409
+ memoryA: item.memoryA,
410
+ memoryB: item.memoryB,
411
+ score: item.score,
412
+ reason: item.reason,
413
+ sharedTopics: item.sharedTopics,
414
+ })),
415
+ },
118
416
  });
119
417
  }
120
418
  catch (error) {
@@ -585,7 +883,7 @@ export function registerMemoryRoutes(app, requireNotLocked) {
585
883
  app.patch('/api/memories/:id', requireNotLocked, (req, res) => {
586
884
  try {
587
885
  const id = parseInt(req.params.id, 10);
588
- const { title, content, category, tags, importance } = req.body;
886
+ const { title, content, category, tags, importance, status, pinned, reviewedBy, cloudExcluded, scope, project } = req.body;
589
887
  if (title !== undefined && (typeof title !== 'string' || title.trim().length === 0)) {
590
888
  return res.status(400).json({ error: 'Title must be a non-empty string' });
591
889
  }
@@ -602,6 +900,24 @@ export function registerMemoryRoutes(app, requireNotLocked) {
602
900
  if (importance !== undefined && (typeof importance !== 'number' || importance < 0 || importance > 1)) {
603
901
  return res.status(400).json({ error: 'Importance must be a number between 0 and 1' });
604
902
  }
903
+ if (status !== undefined && !['active', 'archived', 'suppressed', 'canonical'].includes(status)) {
904
+ return res.status(400).json({ error: 'Invalid review status' });
905
+ }
906
+ if (pinned !== undefined && typeof pinned !== 'boolean') {
907
+ return res.status(400).json({ error: 'Pinned must be boolean' });
908
+ }
909
+ if (cloudExcluded !== undefined && typeof cloudExcluded !== 'boolean') {
910
+ return res.status(400).json({ error: 'cloudExcluded must be boolean' });
911
+ }
912
+ if (reviewedBy !== undefined && reviewedBy !== null && typeof reviewedBy !== 'string') {
913
+ return res.status(400).json({ error: 'reviewedBy must be string or null' });
914
+ }
915
+ if (scope !== undefined && !['project', 'global'].includes(scope)) {
916
+ return res.status(400).json({ error: 'scope must be project or global' });
917
+ }
918
+ if (project !== undefined && project !== null && typeof project !== 'string') {
919
+ return res.status(400).json({ error: 'project must be string or null' });
920
+ }
605
921
  const updates = {};
606
922
  if (title !== undefined)
607
923
  updates.title = title.trim();
@@ -613,6 +929,55 @@ export function registerMemoryRoutes(app, requireNotLocked) {
613
929
  updates.tags = tags;
614
930
  if (importance !== undefined)
615
931
  updates.salience = importance;
932
+ if (status !== undefined)
933
+ updates.status = status;
934
+ if (pinned !== undefined)
935
+ updates.pinned = pinned;
936
+ if (reviewedBy !== undefined)
937
+ updates.reviewedBy = reviewedBy;
938
+ if (cloudExcluded !== undefined)
939
+ updates.cloudExcluded = cloudExcluded;
940
+ if (scope !== undefined)
941
+ updates.scope = scope;
942
+ if (project !== undefined)
943
+ updates.project = project;
944
+ const updated = updateMemory(id, updates);
945
+ if (!updated) {
946
+ return res.status(404).json({ error: 'Memory not found' });
947
+ }
948
+ res.json(updated);
949
+ }
950
+ catch (error) {
951
+ res.status(500).json({ error: error.message });
952
+ }
953
+ });
954
+ app.patch('/api/memories/:id/review', requireNotLocked, (req, res) => {
955
+ try {
956
+ const id = parseInt(req.params.id, 10);
957
+ const { action, reviewedBy, project, scope } = req.body;
958
+ if (Number.isNaN(id)) {
959
+ return res.status(400).json({ error: 'Invalid memory ID' });
960
+ }
961
+ const reviewActor = typeof reviewedBy === 'string' && reviewedBy.trim() ? reviewedBy.trim() : 'dashboard';
962
+ const actionMap = {
963
+ archive: { status: 'archived', reviewedBy: reviewActor },
964
+ suppress: { status: 'suppressed', reviewedBy: reviewActor },
965
+ restore: { status: 'active', reviewedBy: reviewActor },
966
+ pin: { pinned: true, reviewedBy: reviewActor },
967
+ unpin: { pinned: false, reviewedBy: reviewActor },
968
+ canonicalize: { status: 'canonical', pinned: true, reviewedBy: reviewActor },
969
+ excludeCloud: { cloudExcluded: true, reviewedBy: reviewActor },
970
+ includeCloud: { cloudExcluded: false, reviewedBy: reviewActor },
971
+ rescopeProject: { scope: 'project', project: project ?? null, reviewedBy: reviewActor },
972
+ rescopeGlobal: { scope: 'global', project: null, reviewedBy: reviewActor },
973
+ };
974
+ if (!action || !actionMap[action]) {
975
+ return res.status(400).json({ error: 'Unsupported review action' });
976
+ }
977
+ const updates = {
978
+ ...actionMap[action],
979
+ ...(scope ? { scope } : {}),
980
+ };
616
981
  const updated = updateMemory(id, updates);
617
982
  if (!updated) {
618
983
  return res.status(404).json({ error: 'Memory not found' });
@@ -1,4 +1,5 @@
1
1
  import { searchMemoriesExplained } from '../../memory/store.js';
2
+ import { getMemoryById, getRecentMemories } from '../../memory/store.js';
2
3
  export function registerRecallRoutes(app, requireNotLocked) {
3
4
  app.get('/api/recall/explain', requireNotLocked, async (req, res) => {
4
5
  try {
@@ -12,6 +13,9 @@ export function registerRecallRoutes(app, requireNotLocked) {
12
13
  const limit = Math.min(parseInt(req.query.limit, 10) || 10, 50);
13
14
  const includeDecayed = req.query.includeDecayed === 'true';
14
15
  const includeGlobal = req.query.includeGlobal !== 'false';
16
+ const includeArchived = req.query.includeArchived === 'true';
17
+ const includeSuppressed = req.query.includeSuppressed === 'true';
18
+ const expectedId = typeof req.query.expectedId === 'string' ? parseInt(req.query.expectedId, 10) : null;
15
19
  const results = await searchMemoriesExplained({
16
20
  query,
17
21
  project,
@@ -20,13 +24,62 @@ export function registerRecallRoutes(app, requireNotLocked) {
20
24
  limit,
21
25
  includeDecayed,
22
26
  includeGlobal,
27
+ includeArchived,
28
+ includeSuppressed,
23
29
  });
30
+ let expectedMemory = null;
31
+ if (expectedId && Number.isFinite(expectedId)) {
32
+ const memory = getMemoryById(expectedId);
33
+ if (memory) {
34
+ const foundIndex = results.findIndex((result) => result.memory.id === expectedId);
35
+ expectedMemory = {
36
+ id: memory.id,
37
+ title: memory.title,
38
+ status: memory.status,
39
+ pinned: memory.pinned,
40
+ cloudExcluded: memory.cloudExcluded,
41
+ trustScore: memory.trustScore,
42
+ captureMethod: memory.captureMethod,
43
+ sourceKind: memory.sourceKind,
44
+ rank: foundIndex >= 0 ? foundIndex + 1 : null,
45
+ eligible: memory.status !== 'archived' && memory.status !== 'suppressed',
46
+ reasons: [
47
+ ...(memory.status === 'archived' ? ['Archived memories are excluded from normal recall'] : []),
48
+ ...(memory.status === 'suppressed' ? ['Suppressed memories are excluded from normal recall'] : []),
49
+ ...(memory.trustScore < 0.7 ? [`Low trust source (${memory.trustScore.toFixed(2)})`] : []),
50
+ ...(memory.cloudExcluded ? ['Excluded from cloud sync'] : []),
51
+ ...(foundIndex === -1 ? ['Did not rank in the current result window'] : []),
52
+ ],
53
+ };
54
+ }
55
+ }
56
+ const misses = getRecentMemories(200, project).filter((memory) => {
57
+ if (results.some((result) => result.memory.id === memory.id))
58
+ return false;
59
+ if (memory.status === 'archived' || memory.status === 'suppressed')
60
+ return false;
61
+ return memory.salience >= 0.65 || memory.pinned;
62
+ }).slice(0, 5).map((memory) => ({
63
+ id: memory.id,
64
+ title: memory.title,
65
+ status: memory.status,
66
+ salience: memory.salience,
67
+ captureMethod: memory.captureMethod,
68
+ sourceKind: memory.sourceKind,
69
+ whyNotRecalled: [
70
+ 'Lower relevance for this query than the returned set',
71
+ ...(memory.pinned ? ['Pinned memories are still query-sensitive, not guaranteed results'] : []),
72
+ ...(memory.trustScore < 0.7 ? [`Low trust source (${memory.trustScore.toFixed(2)})`] : []),
73
+ ],
74
+ }));
24
75
  res.json({
25
76
  query,
26
77
  project: project ?? null,
27
78
  total: results.length,
28
79
  sideEffects: 'disabled',
29
80
  results,
81
+ expectedMemory,
82
+ misses,
30
83
  });
31
84
  }
32
85
  catch (error) {
@@ -1,6 +1,8 @@
1
1
  import { WebSocket } from 'ws';
2
- import { getCloudConfig, getDefenceMode, getOpenClawMemoryConfig, isConfigTampered, readRawConfig, setCloudConfig, setDefenceMode, setOpenClawMemoryConfig, } from '../../cloud/config.js';
2
+ import { getCloudConfig, getCloudSyncControls, getDeviceId, getDeviceName, getDefenceMode, getOpenClawMemoryConfig, isConfigTampered, readRawConfig, setCloudConfig, setCloudSyncControls, setDefenceMode, setOpenClawMemoryConfig, } from '../../cloud/config.js';
3
3
  import { getQueueStats } from '../../cloud/sync-queue.js';
4
+ import { getDatabase } from '../../database/init.js';
5
+ import { getRequiredTier, isFeatureEnabled } from '../../license/gate.js';
4
6
  import { getControlStatus, isKillSwitchActive, pause, resume } from '../control.js';
5
7
  import { checkForUpdates, getCurrentVersion, getRunningVersion, performUpdate, scheduleRestart, } from '../version.js';
6
8
  export function registerSystemRoutes(app, deps) {
@@ -44,6 +46,7 @@ export function registerSystemRoutes(app, deps) {
44
46
  enabled: config.cloudEnabled,
45
47
  apiKeySet: !!config.cloudApiKey,
46
48
  baseUrl: config.cloudBaseUrl,
49
+ syncControls: getCloudSyncControls(),
47
50
  openclawMemory: getOpenClawMemoryConfig(),
48
51
  });
49
52
  }
@@ -53,7 +56,7 @@ export function registerSystemRoutes(app, deps) {
53
56
  });
54
57
  app.post('/api/cloud/config', (req, res) => {
55
58
  try {
56
- const { cloudApiKey, cloudEnabled, cloudBaseUrl, openclawAutoMemory, openclawAutoMemoryDedupe, openclawAutoMemoryNoveltyThreshold, openclawAutoMemoryMaxRecent, } = req.body;
59
+ const { cloudApiKey, cloudEnabled, cloudBaseUrl, cloudSyncProjectMode, cloudSyncProjects, cloudSyncContentMode, cloudSyncExcludeSensitive, openclawAutoMemory, openclawAutoMemoryDedupe, openclawAutoMemoryNoveltyThreshold, openclawAutoMemoryMaxRecent, } = req.body;
57
60
  if (openclawAutoMemory !== undefined && typeof openclawAutoMemory !== 'boolean') {
58
61
  return res.status(400).json({ error: 'openclawAutoMemory must be a boolean' });
59
62
  }
@@ -68,11 +71,36 @@ export function registerSystemRoutes(app, deps) {
68
71
  (typeof openclawAutoMemoryMaxRecent !== 'number' || Number.isNaN(openclawAutoMemoryMaxRecent))) {
69
72
  return res.status(400).json({ error: 'openclawAutoMemoryMaxRecent must be a number' });
70
73
  }
74
+ if (cloudSyncProjectMode !== undefined &&
75
+ cloudSyncProjectMode !== 'all' &&
76
+ cloudSyncProjectMode !== 'include' &&
77
+ cloudSyncProjectMode !== 'exclude') {
78
+ return res.status(400).json({ error: 'cloudSyncProjectMode must be all, include, or exclude' });
79
+ }
80
+ if (cloudSyncContentMode !== undefined &&
81
+ cloudSyncContentMode !== 'full' &&
82
+ cloudSyncContentMode !== 'metadata') {
83
+ return res.status(400).json({ error: 'cloudSyncContentMode must be full or metadata' });
84
+ }
85
+ if (cloudSyncExcludeSensitive !== undefined &&
86
+ typeof cloudSyncExcludeSensitive !== 'boolean') {
87
+ return res.status(400).json({ error: 'cloudSyncExcludeSensitive must be a boolean' });
88
+ }
89
+ if (cloudSyncProjects !== undefined &&
90
+ (!Array.isArray(cloudSyncProjects) || cloudSyncProjects.some((value) => typeof value !== 'string'))) {
91
+ return res.status(400).json({ error: 'cloudSyncProjects must be an array of strings' });
92
+ }
71
93
  setCloudConfig({
72
94
  ...(cloudApiKey !== undefined && { cloudApiKey }),
73
95
  ...(cloudEnabled !== undefined && { cloudEnabled }),
74
96
  ...(cloudBaseUrl !== undefined && { cloudBaseUrl }),
75
97
  });
98
+ setCloudSyncControls({
99
+ ...(cloudSyncProjectMode !== undefined && { projectMode: cloudSyncProjectMode }),
100
+ ...(cloudSyncProjects !== undefined && { projects: cloudSyncProjects }),
101
+ ...(cloudSyncContentMode !== undefined && { contentMode: cloudSyncContentMode }),
102
+ ...(cloudSyncExcludeSensitive !== undefined && { excludeSensitive: cloudSyncExcludeSensitive }),
103
+ });
76
104
  setOpenClawMemoryConfig({
77
105
  ...(openclawAutoMemory !== undefined && { autoMemory: openclawAutoMemory }),
78
106
  ...(openclawAutoMemoryDedupe !== undefined && { dedupe: openclawAutoMemoryDedupe }),
@@ -85,6 +113,7 @@ export function registerSystemRoutes(app, deps) {
85
113
  enabled: updated.cloudEnabled,
86
114
  apiKeySet: !!updated.cloudApiKey,
87
115
  baseUrl: updated.cloudBaseUrl,
116
+ syncControls: getCloudSyncControls(),
88
117
  openclawMemory: getOpenClawMemoryConfig(),
89
118
  });
90
119
  }
@@ -122,10 +151,25 @@ export function registerSystemRoutes(app, deps) {
122
151
  res.json({
123
152
  enabled: config.cloudEnabled,
124
153
  apiKeySet: !!config.cloudApiKey,
154
+ baseUrl: config.cloudBaseUrl,
155
+ featureEnabled: isFeatureEnabled('cloud_sync'),
156
+ requiredTier: getRequiredTier('cloud_sync'),
157
+ controls: getCloudSyncControls(),
125
158
  lastSyncAt: (typeof raw.lastSyncAt === 'string' ? raw.lastSyncAt : null),
159
+ device: {
160
+ id: getDeviceId(),
161
+ name: getDeviceName(),
162
+ platform: `${process.platform}/${process.arch}`,
163
+ },
126
164
  queue: {
127
165
  pending: queue.pending,
128
166
  failed: queue.failed,
167
+ synced: queue.synced,
168
+ byKind: queue.byKind,
169
+ oldestPendingAt: queue.oldestPendingAt,
170
+ nextRetryAt: queue.nextRetryAt,
171
+ lastError: queue.lastError,
172
+ lastErrorKind: queue.lastErrorKind,
129
173
  },
130
174
  });
131
175
  }
@@ -133,6 +177,27 @@ export function registerSystemRoutes(app, deps) {
133
177
  res.status(500).json({ error: error.message });
134
178
  }
135
179
  });
180
+ app.get('/api/cloud/projects', (_req, res) => {
181
+ try {
182
+ const db = getDatabase();
183
+ const rows = db.prepare(`
184
+ SELECT project, COUNT(*) as count
185
+ FROM memories
186
+ WHERE project IS NOT NULL AND TRIM(project) != ''
187
+ GROUP BY project
188
+ ORDER BY count DESC, project ASC
189
+ `).all();
190
+ res.json({
191
+ projects: rows.map((row) => ({
192
+ project: row.project,
193
+ count: Number(row.count ?? 0),
194
+ })),
195
+ });
196
+ }
197
+ catch (error) {
198
+ res.status(500).json({ error: error.message });
199
+ }
200
+ });
136
201
  app.get('/api/version', (_req, res) => {
137
202
  try {
138
203
  const version = getCurrentVersion();
@@ -1 +1,2 @@
1
1
  export declare function handleCloudConfig(args: string[]): void;
2
+ export declare function handleCloudCommand(args: string[]): Promise<void>;
package/dist/cloud/cli.js CHANGED
@@ -1,4 +1,9 @@
1
1
  import { getCloudConfig, setCloudConfig, getDefenceMode, setDefenceMode, getVerifyConfig, setVerifyConfig, getOpenClawAutoMemory, setOpenClawAutoMemory, } from './config.js';
2
+ import { syncAllGraphToCloud } from './graph-sync.js';
3
+ import { syncAllMemoriesToCloud } from './memory-sync.js';
4
+ import { isFeatureEnabled } from '../license/gate.js';
5
+ import { initDatabase } from '../database/init.js';
6
+ import { reconcileSyncQueue } from './sync-queue.js';
2
7
  const VALID_MODES = ['strict', 'balanced', 'permissive'];
3
8
  const VALID_VERIFY_MODES = ['advisory', 'enforce'];
4
9
  export function handleCloudConfig(args) {
@@ -128,3 +133,38 @@ export function handleCloudConfig(args) {
128
133
  console.log(' --verify-timeout <ms> Set verify timeout in ms (1000-30000)');
129
134
  }
130
135
  }
136
+ export async function handleCloudCommand(args) {
137
+ const action = args[0];
138
+ if (action === 'sync' && args.includes('--full')) {
139
+ const config = getCloudConfig();
140
+ if (!config.cloudEnabled || !config.cloudApiKey) {
141
+ console.error('Cloud sync is not configured. Set an API key and enable cloud sync first.');
142
+ process.exit(1);
143
+ }
144
+ if (!isFeatureEnabled('cloud_sync')) {
145
+ console.error('Cloud memory sync requires a Team or higher licence.');
146
+ process.exit(1);
147
+ }
148
+ initDatabase();
149
+ console.log('Syncing local memories and graph to ShieldCortex Cloud...');
150
+ const memoryResult = await syncAllMemoriesToCloud();
151
+ const graphResult = await syncAllGraphToCloud();
152
+ let reconciled = 0;
153
+ if (memoryResult.failed === 0 && graphResult.failedBatches === 0) {
154
+ reconciled = reconcileSyncQueue({
155
+ kinds: ['memory', 'graph'],
156
+ statuses: ['pending', 'failed'],
157
+ maxCreatedAt: new Date().toISOString(),
158
+ }).removed;
159
+ }
160
+ console.log(`Finished. ${memoryResult.synced}/${memoryResult.total} memories synced` +
161
+ `${memoryResult.failed > 0 ? `, ${memoryResult.failed} queued for retry` : ''}.`);
162
+ console.log(`Graph replica: ${graphResult.entities} entities, ${graphResult.triples} relationships, ${graphResult.memoryEntities} memory links` +
163
+ `${graphResult.failedBatches > 0 ? `, ${graphResult.failedBatches} batch${graphResult.failedBatches === 1 ? '' : 'es'} queued for retry` : ''}.`);
164
+ if (reconciled > 0) {
165
+ console.log(`Reconciled ${reconciled} stale sync queue entr${reconciled === 1 ? 'y' : 'ies'}.`);
166
+ }
167
+ return;
168
+ }
169
+ console.log('Usage: shieldcortex cloud sync --full');
170
+ }