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.
- package/README.md +21 -2
- 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/required-server-files.json +4 -4
- 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/9232a2d99b47b21f.js +3 -0
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/98e2c181d5c4349f.js +1 -0
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/9cb86821c1107fd6.js +9 -0
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/a56c497e02afd4ba.css +3 -0
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/a90355d73183a5e6.js +1 -0
- package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-darwin-arm64/lib/sharp-darwin-arm64.node +0 -0
- package/dashboard/.next/standalone/{node_modules/@img/sharp-linux-x64 → dashboard/node_modules/@img/sharp-darwin-arm64}/package.json +7 -13
- package/dashboard/.next/standalone/dashboard/node_modules/@img/{sharp-libvips-linux-x64 → sharp-libvips-darwin-arm64}/README.md +2 -2
- 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
- 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
- package/dashboard/.next/standalone/dashboard/node_modules/@img/{sharp-libvips-linux-x64 → sharp-libvips-darwin-arm64}/package.json +5 -11
- package/dashboard/.next/standalone/dashboard/server.js +1 -1
- package/dashboard/.next/standalone/{dashboard/node_modules/@img/sharp-linux-x64 → node_modules/@img/sharp-darwin-arm64}/package.json +7 -13
- package/dashboard/.next/standalone/node_modules/@img/{sharp-libvips-linux-x64 → sharp-libvips-darwin-arm64}/package.json +5 -11
- package/dist/api/routes/memories.js +366 -1
- package/dist/api/routes/recall.js +53 -0
- package/dist/api/routes/system.js +67 -2
- package/dist/cloud/cli.d.ts +1 -0
- package/dist/cloud/cli.js +40 -0
- package/dist/cloud/config.d.ts +10 -0
- package/dist/cloud/config.js +54 -0
- package/dist/cloud/graph-sync.d.ts +45 -0
- package/dist/cloud/graph-sync.js +260 -0
- package/dist/cloud/memory-sync.d.ts +37 -0
- package/dist/cloud/memory-sync.js +186 -0
- package/dist/cloud/sync-queue.d.ts +24 -0
- package/dist/cloud/sync-queue.js +126 -7
- package/dist/database/init.js +53 -0
- package/dist/graph/backfill.js +3 -5
- package/dist/graph/resolve.d.ts +10 -0
- package/dist/graph/resolve.js +63 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +61 -4
- package/dist/memory/search.d.ts +1 -0
- package/dist/memory/search.js +4 -0
- package/dist/memory/store.js +188 -30
- package/dist/memory/types.d.ts +33 -0
- package/dist/service/install.d.ts +1 -0
- package/dist/service/install.js +43 -1
- package/dist/tools/context.d.ts +4 -4
- package/dist/tools/forget.d.ts +6 -6
- package/dist/tools/recall.d.ts +11 -11
- package/dist/tools/remember.d.ts +19 -4
- package/dist/tools/remember.js +17 -1
- package/hooks/openclaw/cortex-memory/handler.ts +8 -0
- package/package.json +1 -1
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/313c0d327bbf244a.js +0 -9
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/3cc7e8d4f73cf5d2.js +0 -1
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/49c1cec591af1460.js +0 -3
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/ca21f348cb163905.js +0 -1
- package/dashboard/.next/standalone/dashboard/.next/static/chunks/f4ca424319f58dc7.css +0 -3
- package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-libvips-linuxmusl-x64/README.md +0 -46
- package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/glib-2.0/include/glibconfig.h +0 -221
- package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/index.js +0 -1
- package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/libvips-cpp.so.8.17.3 +0 -0
- package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json +0 -42
- package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-libvips-linuxmusl-x64/versions.json +0 -30
- package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-linux-x64/lib/sharp-linux-x64.node +0 -0
- package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-linuxmusl-x64/lib/sharp-linuxmusl-x64.node +0 -0
- package/dashboard/.next/standalone/dashboard/node_modules/@img/sharp-linuxmusl-x64/package.json +0 -46
- package/dashboard/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json +0 -42
- package/dashboard/.next/standalone/node_modules/@img/sharp-linuxmusl-x64/package.json +0 -46
- /package/dashboard/.next/standalone/dashboard/.next/static/{BEvyMAX62LQMyt5iSb-F9 → ctp9eCBcHDpTWtUYMwJK7}/_buildManifest.js +0 -0
- /package/dashboard/.next/standalone/dashboard/.next/static/{BEvyMAX62LQMyt5iSb-F9 → ctp9eCBcHDpTWtUYMwJK7}/_clientMiddlewareManifest.json +0 -0
- /package/dashboard/.next/standalone/dashboard/.next/static/{BEvyMAX62LQMyt5iSb-F9 → ctp9eCBcHDpTWtUYMwJK7}/_ssgManifest.js +0 -0
- /package/dashboard/.next/standalone/dashboard/node_modules/@img/{sharp-libvips-linux-x64 → sharp-libvips-darwin-arm64}/lib/index.js +0 -0
- /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();
|
package/dist/cloud/cli.d.ts
CHANGED
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
|
+
}
|