total-recall-brain 3.3.0 → 3.5.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 +1 -1
- package/bin/antigravity.mjs +19 -8
- package/extension/background.js +13 -1
- package/extension/content-script.js +18 -1
- package/extension/lib/brain-client.js +18 -5
- package/extension/options/options.js +9 -4
- package/frontend/index.html +8 -0
- package/frontend/src/api.ts +20 -1
- package/frontend/src/pages/ChatPage.tsx +9 -6
- package/frontend/src/pages/HealthPage.tsx +179 -7
- package/frontend/src/pages/IntegrationsPage.tsx +4 -2
- package/frontend/src/pages/SettingsPage.tsx +197 -2
- package/frontend/src/types.ts +16 -0
- package/package.json +2 -2
- package/scaffold/.agent/skills/total-recall/SKILL.md +55 -6
- package/scaffold/.agent/skills/total-recall/memory-vault/invariants/operating-instructions.md +2 -2
- package/scaffold/.agent/skills/total-recall/research-queue.jsonl +1 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/agents.yml +1 -6
- package/scaffold/.agent/skills/total-recall/skills/ssss/SKILL.md +19 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/references/ssss-spec.md +33 -5
- package/src/cli/forget.mjs +18 -12
- package/src/cli/ingest/index.mjs +3 -2
- package/src/cli/map.mjs +2 -2
- package/src/cli/rebuild.mjs +1 -1
- package/src/cli/remember.mjs +21 -11
- package/src/cli/share.mjs +7 -2
- package/src/core/append-log.mjs +131 -0
- package/src/core/clarity-rewriter.mjs +10 -6
- package/src/core/conclusion-writer.mjs +3 -2
- package/src/core/config.mjs +6 -0
- package/src/core/conflict-detector.mjs +4 -2
- package/src/core/context-compiler.mjs +412 -0
- package/src/core/daemon-loop.mjs +15 -0
- package/src/core/dream.mjs +4 -4
- package/src/core/drift-detector.mjs +3 -3
- package/src/core/embeddings.mjs +60 -10
- package/src/core/fact-seeker.mjs +70 -44
- package/src/core/import-rules.mjs +3 -2
- package/src/core/inference-engine.mjs +5 -4
- package/src/core/migrate.mjs +4 -3
- package/src/core/optimizer.mjs +6 -5
- package/src/core/parallel-context.mjs +288 -0
- package/src/core/post-mortem.mjs +4 -3
- package/src/core/research-queue.mjs +4 -3
- package/src/core/runtime.mjs +30 -7
- package/src/core/sandbox.mjs +56 -26
- package/src/core/scheduler.mjs +6 -5
- package/src/core/schema.mjs +483 -2
- package/src/core/search.mjs +167 -101
- package/src/core/surface.mjs +203 -53
- package/src/core/validated-write.mjs +100 -0
- package/src/core/vault-cache.mjs +46 -18
- package/src/core/vault.mjs +17 -29
- package/src/core/vector-field.mjs +659 -0
- package/src/core/vector-store.mjs +85 -0
- package/src/server/api.mjs +4 -3
- package/src/server/index.mjs +13 -61
- package/src/server/rest.mjs +341 -63
- package/src/server/route-manifest.json +352 -77
- package/src/server/routes/memory.mjs +85 -39
- package/src/server/routes/sessions.mjs +6 -2
- package/src/server/routes/share.mjs +9 -2
package/src/server/rest.mjs
CHANGED
|
@@ -52,10 +52,11 @@ import crypto from 'node:crypto';
|
|
|
52
52
|
import { spawn, spawnSync } from 'node:child_process';
|
|
53
53
|
import matter from 'gray-matter';
|
|
54
54
|
import { fileURLToPath } from 'node:url';
|
|
55
|
+
import yaml from 'yaml';
|
|
55
56
|
import { loadRuntimeConfig } from '../core/runtime.mjs';
|
|
56
57
|
|
|
57
58
|
|
|
58
|
-
import {
|
|
59
|
+
import { writeNode, deleteNode, safeStringify } from '../core/vault.mjs';
|
|
59
60
|
import { getNodes, invalidate } from '../core/vault-cache.mjs';
|
|
60
61
|
import { compileSurface } from '../core/surface.mjs';
|
|
61
62
|
import { runInSandbox } from '../core/sandbox.mjs';
|
|
@@ -231,6 +232,161 @@ router.post('/api/vault/compile', requireAuth, requireScope('memory:recompile'),
|
|
|
231
232
|
}
|
|
232
233
|
});
|
|
233
234
|
|
|
235
|
+
/**
|
|
236
|
+
* POST /api/vault/compact
|
|
237
|
+
* Compact all active append-only log files (dedup, remove tombstones).
|
|
238
|
+
*/
|
|
239
|
+
router.post('/api/vault/compact', requireAuth, requireScope('memory:write'), async (req, res) => {
|
|
240
|
+
try {
|
|
241
|
+
const { compactAppendLogs } = await import('../core/append-log.mjs');
|
|
242
|
+
const result = compactAppendLogs();
|
|
243
|
+
res.json({ compacted: true, ...result });
|
|
244
|
+
} catch (err) {
|
|
245
|
+
serverError(res, err);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* POST /api/context
|
|
251
|
+
* Dynamic streaming context compilation.
|
|
252
|
+
* Assembles a query-aware, temporally-scored, budget-constrained context
|
|
253
|
+
* document from the vault in real-time. Every token earns its place.
|
|
254
|
+
*
|
|
255
|
+
* Body: { query: string, budget?: object, momentum_slugs?: string[] }
|
|
256
|
+
* Returns: { context: string, stats: object }
|
|
257
|
+
*/
|
|
258
|
+
router.post('/api/context', requireAuth, requireScope('memory:read'), async (req, res) => {
|
|
259
|
+
try {
|
|
260
|
+
const { compileContext } = await import('../core/context-compiler.mjs');
|
|
261
|
+
const { query, budget, momentum_slugs } = req.body || {};
|
|
262
|
+
const result = await compileContext({
|
|
263
|
+
query: query || '',
|
|
264
|
+
vaultDir: VAULT_DIR,
|
|
265
|
+
derivedDir: DERIVED_DIR,
|
|
266
|
+
budget: budget || {},
|
|
267
|
+
consumer: 'api',
|
|
268
|
+
momentumSlugs: momentum_slugs || [],
|
|
269
|
+
});
|
|
270
|
+
res.json(result);
|
|
271
|
+
} catch (err) {
|
|
272
|
+
serverError(res, err);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* GET /api/context/preview
|
|
278
|
+
* Lightweight context preview — returns temporally-scored candidates
|
|
279
|
+
* and budget allocation without computing embeddings.
|
|
280
|
+
*/
|
|
281
|
+
router.get('/api/context/preview', requireAuth, requireScope('memory:read'), async (req, res) => {
|
|
282
|
+
try {
|
|
283
|
+
const { previewContext } = await import('../core/context-compiler.mjs');
|
|
284
|
+
const result = previewContext({ vaultDir: VAULT_DIR });
|
|
285
|
+
res.json(result);
|
|
286
|
+
} catch (err) {
|
|
287
|
+
serverError(res, err);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* POST /api/context/stream
|
|
293
|
+
* Massively parallel context streaming via Flash fan-out.
|
|
294
|
+
* Chunks the entire vault, dispatches parallel Flash calls to score
|
|
295
|
+
* every node against the query simultaneously, merges into budget.
|
|
296
|
+
*
|
|
297
|
+
* Body: { query: string, budget_tokens?: number, batch_size?: number,
|
|
298
|
+
* concurrency?: number, min_score?: number }
|
|
299
|
+
* Returns: { context: string, stats: object, scored: array }
|
|
300
|
+
*/
|
|
301
|
+
router.post('/api/context/stream', requireAuth, requireScope('memory:read'), async (req, res) => {
|
|
302
|
+
try {
|
|
303
|
+
const { streamParallelContext } = await import('../core/parallel-context.mjs');
|
|
304
|
+
const { query, budget_tokens, batch_size, concurrency, min_score } = req.body || {};
|
|
305
|
+
if (!query) return badRequest(res, 'query is required');
|
|
306
|
+
const result = await streamParallelContext({
|
|
307
|
+
query,
|
|
308
|
+
vaultDir: VAULT_DIR,
|
|
309
|
+
budgetTokens: budget_tokens,
|
|
310
|
+
batchSize: batch_size,
|
|
311
|
+
concurrency,
|
|
312
|
+
minScore: min_score,
|
|
313
|
+
});
|
|
314
|
+
res.json(result);
|
|
315
|
+
} catch (err) {
|
|
316
|
+
serverError(res, err);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* GET /api/context/flash/health
|
|
322
|
+
* Check Flash API connectivity for parallel context streaming.
|
|
323
|
+
*/
|
|
324
|
+
router.get('/api/context/flash/health', requireAuth, requireScope('memory:read'), async (req, res) => {
|
|
325
|
+
try {
|
|
326
|
+
const { checkFlashHealth } = await import('../core/parallel-context.mjs');
|
|
327
|
+
const result = await checkFlashHealth();
|
|
328
|
+
res.json(result);
|
|
329
|
+
} catch (err) {
|
|
330
|
+
serverError(res, err);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// ─── Vector Field ─────────────────────────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* POST /api/field/compile
|
|
338
|
+
* Compile the full vector field: embed all nodes, compute N×N covariance
|
|
339
|
+
* matrix, track velocities, persist to disk.
|
|
340
|
+
*/
|
|
341
|
+
router.post('/api/field/compile', requireAuth, requireScope('memory:recompile'), async (req, res) => {
|
|
342
|
+
try {
|
|
343
|
+
const { compileField } = await import('../core/vector-field.mjs');
|
|
344
|
+
const result = await compileField({ vaultDir: VAULT_DIR, derivedDir: DERIVED_DIR });
|
|
345
|
+
res.json({ compiled: true, ...result.meta });
|
|
346
|
+
} catch (err) {
|
|
347
|
+
serverError(res, err);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* POST /api/field/sample
|
|
353
|
+
* Sample the vector field at a query point.
|
|
354
|
+
* Returns ranked nodes with direct similarity, entanglement boost, and velocity.
|
|
355
|
+
*
|
|
356
|
+
* Body: { query: string, top_k?: number, entanglement_boost?: number, velocity_weight?: number }
|
|
357
|
+
*/
|
|
358
|
+
router.post('/api/field/sample', requireAuth, requireScope('memory:read'), async (req, res) => {
|
|
359
|
+
try {
|
|
360
|
+
const { sampleField } = await import('../core/vector-field.mjs');
|
|
361
|
+
const { query, top_k, entanglement_boost, velocity_weight } = req.body || {};
|
|
362
|
+
if (!query) return badRequest(res, 'query is required');
|
|
363
|
+
const result = await sampleField({
|
|
364
|
+
query,
|
|
365
|
+
topK: top_k,
|
|
366
|
+
entanglementBoost: entanglement_boost,
|
|
367
|
+
velocityWeight: velocity_weight,
|
|
368
|
+
derivedDir: DERIVED_DIR,
|
|
369
|
+
});
|
|
370
|
+
res.json(result);
|
|
371
|
+
} catch (err) {
|
|
372
|
+
serverError(res, err);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* GET /api/field/stats
|
|
378
|
+
* Vector field statistics: point count, velocity distribution,
|
|
379
|
+
* strongest couplings, compilation metadata.
|
|
380
|
+
*/
|
|
381
|
+
router.get('/api/field/stats', requireAuth, requireScope('memory:read'), async (req, res) => {
|
|
382
|
+
try {
|
|
383
|
+
const { fieldStats } = await import('../core/vector-field.mjs');
|
|
384
|
+
res.json(fieldStats(DERIVED_DIR));
|
|
385
|
+
} catch (err) {
|
|
386
|
+
serverError(res, err);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
234
390
|
/**
|
|
235
391
|
* POST /api/dream
|
|
236
392
|
*/
|
|
@@ -253,24 +409,39 @@ router.post('/api/dream', requireAuth, requireScope('memory:recompile'), async (
|
|
|
253
409
|
}
|
|
254
410
|
});
|
|
255
411
|
|
|
412
|
+
/**
|
|
413
|
+
* GET /api/vault/hash
|
|
414
|
+
* Lightweight endpoint for PWA cache invalidation.
|
|
415
|
+
* Returns only the vault content hash — no expensive I/O.
|
|
416
|
+
*/
|
|
417
|
+
router.get('/api/vault/hash', requireAuth, requireScope('memory:read'), (req, res) => {
|
|
418
|
+
try {
|
|
419
|
+
const hashFile = path.join(DERIVED_DIR, 'vault-hash.txt');
|
|
420
|
+
const hash = fs.existsSync(hashFile) ? fs.readFileSync(hashFile, 'utf8').trim() : null;
|
|
421
|
+
res.json({ vault_hash: hash });
|
|
422
|
+
} catch (err) {
|
|
423
|
+
serverError(res, err);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
256
427
|
/**
|
|
257
428
|
* GET /api/vault/status
|
|
258
429
|
*/
|
|
259
430
|
router.get('/api/vault/status', requireAuth, requireScope('memory:read'), async (req, res) => {
|
|
260
431
|
try {
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
|
|
432
|
+
// Use vault-cache instead of scanning every .md file from disk
|
|
433
|
+
const nodeCount = getNodes(VAULT_DIR).length;
|
|
434
|
+
const skillCount = fs.existsSync(SKILLS_DIR) ? fs.readdirSync(SKILLS_DIR, { withFileTypes: true }).filter(d =>
|
|
435
|
+
d.isDirectory() && fs.existsSync(path.join(SKILLS_DIR, d.name, 'SKILL.md'))).length : 0;
|
|
264
436
|
const lastCompile = fs.existsSync(INSTRUCTIONS)
|
|
265
437
|
? fs.statSync(INSTRUCTIONS).mtime.toISOString()
|
|
266
438
|
: null;
|
|
267
439
|
const derivedFiles = fs.existsSync(DERIVED_DIR)
|
|
268
440
|
? fs.readdirSync(DERIVED_DIR).length : 0;
|
|
269
441
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const sessionEmbedCount =
|
|
273
|
-
? Object.keys(JSON.parse(fs.readFileSync(path.join(DERIVED_DIR, 'session-embeddings.json'), 'utf8') || '{}')).length : 0;
|
|
442
|
+
// Use mtime-cached loaders instead of raw readFile + JSON.parse
|
|
443
|
+
const vaultEmbedCount = Object.keys(loadEmbeddingsIndex(DERIVED_DIR)).length;
|
|
444
|
+
const sessionEmbedCount = Object.keys(loadSessionEmbeddingsIndex(DERIVED_DIR)).length;
|
|
274
445
|
|
|
275
446
|
// CLI agent availability (best-effort)
|
|
276
447
|
const { findBinaryInPath } = await import('../core/runtime.mjs');
|
|
@@ -471,6 +642,7 @@ router.get('/api', (req, res) => {
|
|
|
471
642
|
},
|
|
472
643
|
vault: {
|
|
473
644
|
'POST /api/vault/compile': 'Recompile SSSS surface (INSTRUCTIONS.md)',
|
|
645
|
+
'POST /api/vault/compact': 'Compact all active append-only log files',
|
|
474
646
|
'GET /api/vault/status': 'Node count, skill count, last compile time',
|
|
475
647
|
},
|
|
476
648
|
keys: {
|
|
@@ -728,17 +900,7 @@ router.get('/api/brain/export', requireAuth, requireScope('brain:export'), async
|
|
|
728
900
|
|
|
729
901
|
// ─── Chrome Extension Download ───────────────────────────────────────────────
|
|
730
902
|
|
|
731
|
-
router.get('/api/extension/download', async (_req, res) => {
|
|
732
|
-
let preconfiguredPath = '';
|
|
733
|
-
let originalContent = '';
|
|
734
|
-
const cleanup = () => {
|
|
735
|
-
if (preconfiguredPath && originalContent) {
|
|
736
|
-
try {
|
|
737
|
-
fs.writeFileSync(preconfiguredPath, originalContent, 'utf8');
|
|
738
|
-
} catch {}
|
|
739
|
-
}
|
|
740
|
-
};
|
|
741
|
-
|
|
903
|
+
router.get('/api/extension/download', requireAuth, requireScope('config:read'), async (_req, res) => {
|
|
742
904
|
try {
|
|
743
905
|
// Extension lives at <package-root>/extension/
|
|
744
906
|
const extDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../extension');
|
|
@@ -746,30 +908,8 @@ router.get('/api/extension/download', async (_req, res) => {
|
|
|
746
908
|
return res.status(404).json({ error: 'Chrome extension not found in this installation.' });
|
|
747
909
|
}
|
|
748
910
|
|
|
749
|
-
//
|
|
750
|
-
|
|
751
|
-
let brainUrl = '';
|
|
752
|
-
let patToken = '';
|
|
753
|
-
if (fs.existsSync(brainPath)) {
|
|
754
|
-
try {
|
|
755
|
-
const brain = JSON.parse(fs.readFileSync(brainPath, 'utf8'));
|
|
756
|
-
brainUrl = brain.url || '';
|
|
757
|
-
patToken = brain.token || '';
|
|
758
|
-
} catch {}
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
// Overwrite preconfigured.js temporarily with injected values
|
|
762
|
-
preconfiguredPath = path.join(extDir, 'lib', 'preconfigured.js');
|
|
763
|
-
if (fs.existsSync(preconfiguredPath)) {
|
|
764
|
-
originalContent = fs.readFileSync(preconfiguredPath, 'utf8');
|
|
765
|
-
const injectedContent = `// Exposes default configuration dynamically injected by the server
|
|
766
|
-
self.PreConfigured = {
|
|
767
|
-
brainUrl: "${brainUrl}",
|
|
768
|
-
pat: "${patToken}"
|
|
769
|
-
};
|
|
770
|
-
`;
|
|
771
|
-
fs.writeFileSync(preconfiguredPath, injectedContent, 'utf8');
|
|
772
|
-
}
|
|
911
|
+
// Never inject PATs into packaged extension source. Pair from the extension
|
|
912
|
+
// options page so secrets stay in extension-local storage.
|
|
773
913
|
|
|
774
914
|
res.setHeader('Content-Type', 'application/zip');
|
|
775
915
|
res.setHeader('Content-Disposition', 'attachment; filename="total-recall-extension.zip"');
|
|
@@ -784,18 +924,14 @@ self.PreConfigured = {
|
|
|
784
924
|
res.setHeader('Content-Disposition', 'attachment; filename="total-recall-extension.tar.gz"');
|
|
785
925
|
const tar = spawn('tar', ['czf', '-', '-C', path.dirname(extDir), 'extension'], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
786
926
|
tar.stdout.pipe(res);
|
|
787
|
-
tar.on('error', err => {
|
|
788
|
-
tar.on('close', () => {
|
|
789
|
-
} else {
|
|
790
|
-
cleanup();
|
|
927
|
+
tar.on('error', err => { if (!res.headersSent) serverError(res, err); });
|
|
928
|
+
tar.on('close', () => { if (!res.writableEnded) res.end(); });
|
|
791
929
|
}
|
|
792
930
|
});
|
|
793
931
|
zip.on('close', code => {
|
|
794
|
-
cleanup();
|
|
795
932
|
if (code !== 0 && !res.writableEnded) res.end();
|
|
796
933
|
});
|
|
797
934
|
} catch (err) {
|
|
798
|
-
cleanup();
|
|
799
935
|
serverError(res, err);
|
|
800
936
|
}
|
|
801
937
|
});
|
|
@@ -804,7 +940,7 @@ self.PreConfigured = {
|
|
|
804
940
|
* GET /api/extension/status
|
|
805
941
|
* Returns whether the extension is available (packaged) and connected (has sent captures).
|
|
806
942
|
*/
|
|
807
|
-
router.get('/api/extension/status', async (_req, res) => {
|
|
943
|
+
router.get('/api/extension/status', requireAuth, requireScope('config:read'), async (_req, res) => {
|
|
808
944
|
try {
|
|
809
945
|
const extDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../extension');
|
|
810
946
|
const available = fs.existsSync(path.join(extDir, 'manifest.json'));
|
|
@@ -1104,7 +1240,7 @@ router.put("/api/scripts/:name", requireAuth, requireScope("files:write"), (req,
|
|
|
1104
1240
|
}
|
|
1105
1241
|
});
|
|
1106
1242
|
|
|
1107
|
-
router.post("/api/scripts/:name/run", requireAuth, requireScope("sandbox:run"), (req, res) => {
|
|
1243
|
+
router.post("/api/scripts/:name/run", sandboxRateLimiter(), requireAuth, requireScope("sandbox:run"), requireSandboxEnabled, async (req, res) => {
|
|
1108
1244
|
try {
|
|
1109
1245
|
const { name } = req.params;
|
|
1110
1246
|
if (name.includes("..") || name.includes("/") || name.includes("\\")) {
|
|
@@ -1115,15 +1251,21 @@ router.post("/api/scripts/:name/run", requireAuth, requireScope("sandbox:run"),
|
|
|
1115
1251
|
return res.status(404).json({ error: `Script "${name}" not found` });
|
|
1116
1252
|
}
|
|
1117
1253
|
|
|
1118
|
-
const
|
|
1119
|
-
|
|
1120
|
-
|
|
1254
|
+
const isNodeScript = name.endsWith(".js") || name.endsWith(".mjs") || name.endsWith(".cjs");
|
|
1255
|
+
if (!isNodeScript) {
|
|
1256
|
+
return res.status(400).json({
|
|
1257
|
+
error: "Only Node.js scripts can be run through the sandbox endpoint."
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1121
1260
|
|
|
1122
|
-
const result =
|
|
1261
|
+
const result = await runInSandbox(scriptPath, 10000, {
|
|
1262
|
+
allowNetwork: req.body?.allowNetwork === true
|
|
1263
|
+
});
|
|
1123
1264
|
res.json({
|
|
1124
|
-
success: result.
|
|
1125
|
-
output: result.
|
|
1126
|
-
exitCode: result.
|
|
1265
|
+
success: result.success,
|
|
1266
|
+
output: result.output || "(no output)",
|
|
1267
|
+
exitCode: result.code ?? null,
|
|
1268
|
+
signal: result.signal ?? null
|
|
1127
1269
|
});
|
|
1128
1270
|
} catch (err) {
|
|
1129
1271
|
serverError(res, err);
|
|
@@ -1229,10 +1371,12 @@ router.get('/api/config-json', requireAuth, requireScope('config:read'), (req, r
|
|
|
1229
1371
|
const securityPath = path.join(CONFIG_DIR, 'security.yml');
|
|
1230
1372
|
const budgetPath = path.join(CONFIG_DIR, 'budget.yml');
|
|
1231
1373
|
const brainPath = path.join(CONFIG_DIR, 'brain.json');
|
|
1374
|
+
const secretsPath = path.join(AGENT_DIR, 'secrets.enc');
|
|
1232
1375
|
|
|
1233
1376
|
let security = {};
|
|
1234
1377
|
let budget = {};
|
|
1235
1378
|
let brain = {};
|
|
1379
|
+
let secrets = {};
|
|
1236
1380
|
|
|
1237
1381
|
if (fs.existsSync(securityPath)) {
|
|
1238
1382
|
try {
|
|
@@ -1249,17 +1393,37 @@ router.get('/api/config-json', requireAuth, requireScope('config:read'), (req, r
|
|
|
1249
1393
|
brain = JSON.parse(fs.readFileSync(brainPath, 'utf8')) || {};
|
|
1250
1394
|
} catch {}
|
|
1251
1395
|
}
|
|
1396
|
+
if (fs.existsSync(secretsPath)) {
|
|
1397
|
+
try {
|
|
1398
|
+
secrets = JSON.parse(fs.readFileSync(secretsPath, 'utf8')) || {};
|
|
1399
|
+
} catch {}
|
|
1400
|
+
}
|
|
1252
1401
|
|
|
1253
|
-
|
|
1402
|
+
const safeBrain = { ...brain };
|
|
1403
|
+
if (safeBrain.token) {
|
|
1404
|
+
safeBrain.has_token = true;
|
|
1405
|
+
delete safeBrain.token;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
const allowedKeys = ['google_api_key', 'anthropic_api_key', 'openai_api_key', 'tavily_api_key', 'brave_api_key', 'exa_api_key', 'serper_api_key', 'github_token'];
|
|
1409
|
+
const safeSecrets = {};
|
|
1410
|
+
for (const key of allowedKeys) {
|
|
1411
|
+
if (secrets[key] !== undefined) {
|
|
1412
|
+
safeSecrets[key] = secrets[key];
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
res.json({ security, budget, brain: safeBrain, secrets: safeSecrets });
|
|
1254
1417
|
} catch (err) { serverError(res, err); }
|
|
1255
1418
|
});
|
|
1256
1419
|
|
|
1257
1420
|
router.post('/api/config-json', requireAuth, requireScope('config:write'), (req, res) => {
|
|
1258
1421
|
try {
|
|
1259
|
-
const { security, budget, brain } = req.body;
|
|
1422
|
+
const { security, budget, brain, secrets } = req.body;
|
|
1260
1423
|
const securityPath = path.join(CONFIG_DIR, 'security.yml');
|
|
1261
1424
|
const budgetPath = path.join(CONFIG_DIR, 'budget.yml');
|
|
1262
1425
|
const brainPath = path.join(CONFIG_DIR, 'brain.json');
|
|
1426
|
+
const secretsPath = path.join(AGENT_DIR, 'secrets.enc');
|
|
1263
1427
|
|
|
1264
1428
|
if (!fs.existsSync(CONFIG_DIR)) {
|
|
1265
1429
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
@@ -1272,7 +1436,37 @@ router.post('/api/config-json', requireAuth, requireScope('config:write'), (req,
|
|
|
1272
1436
|
fs.writeFileSync(budgetPath, yaml.stringify(budget), { encoding: 'utf8', mode: 0o600 });
|
|
1273
1437
|
}
|
|
1274
1438
|
if (brain) {
|
|
1275
|
-
|
|
1439
|
+
let existingBrain = {};
|
|
1440
|
+
if (fs.existsSync(brainPath)) {
|
|
1441
|
+
try {
|
|
1442
|
+
existingBrain = JSON.parse(fs.readFileSync(brainPath, 'utf8')) || {};
|
|
1443
|
+
} catch {}
|
|
1444
|
+
}
|
|
1445
|
+
const nextBrain = { ...existingBrain, ...brain };
|
|
1446
|
+
if ((brain.token === undefined || brain.token === '') && existingBrain.token) {
|
|
1447
|
+
nextBrain.token = existingBrain.token;
|
|
1448
|
+
}
|
|
1449
|
+
delete nextBrain.has_token;
|
|
1450
|
+
fs.writeFileSync(brainPath, JSON.stringify(nextBrain, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
1451
|
+
}
|
|
1452
|
+
if (secrets) {
|
|
1453
|
+
let existingSecrets = {};
|
|
1454
|
+
if (fs.existsSync(secretsPath)) {
|
|
1455
|
+
try {
|
|
1456
|
+
existingSecrets = JSON.parse(fs.readFileSync(secretsPath, 'utf8')) || {};
|
|
1457
|
+
} catch {}
|
|
1458
|
+
}
|
|
1459
|
+
const allowedKeys = ['google_api_key', 'anthropic_api_key', 'openai_api_key', 'tavily_api_key', 'brave_api_key', 'exa_api_key', 'serper_api_key', 'github_token'];
|
|
1460
|
+
for (const key of allowedKeys) {
|
|
1461
|
+
if (secrets[key] !== undefined) {
|
|
1462
|
+
if (secrets[key] === '') {
|
|
1463
|
+
delete existingSecrets[key];
|
|
1464
|
+
} else {
|
|
1465
|
+
existingSecrets[key] = secrets[key];
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
fs.writeFileSync(secretsPath, JSON.stringify(existingSecrets, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
1276
1470
|
}
|
|
1277
1471
|
|
|
1278
1472
|
res.json({ success: true });
|
|
@@ -1548,7 +1742,7 @@ router.get('/api/brains', requireAuth, requireScope('ssss:read'), (req, res) =>
|
|
|
1548
1742
|
let globalNodeCount = 0;
|
|
1549
1743
|
if (fs.existsSync(globalVaultDir)) {
|
|
1550
1744
|
try {
|
|
1551
|
-
globalNodeCount =
|
|
1745
|
+
globalNodeCount = getNodes(globalVaultDir).length;
|
|
1552
1746
|
} catch {
|
|
1553
1747
|
// Count .md files manually
|
|
1554
1748
|
const countMd = (dir) => {
|
|
@@ -1649,7 +1843,19 @@ router.get('/api/brains/:id/nodes', requireAuth, requireScope('ssss:read', 'memo
|
|
|
1649
1843
|
return res.json({ nodes: [], brain_id: brainId });
|
|
1650
1844
|
}
|
|
1651
1845
|
|
|
1652
|
-
|
|
1846
|
+
let nodes = getNodes(vaultDir);
|
|
1847
|
+
const { q, category, status, tag } = req.query;
|
|
1848
|
+
if (q) {
|
|
1849
|
+
const query = String(q).toLowerCase();
|
|
1850
|
+
nodes = nodes.filter(n =>
|
|
1851
|
+
[n.slug, n.title, n.category, (n.tags || []).join(' '), n.body]
|
|
1852
|
+
.join(' ').toLowerCase().includes(query)
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1855
|
+
if (category) nodes = nodes.filter(n => n.category === category);
|
|
1856
|
+
if (status) nodes = nodes.filter(n => n.status === status);
|
|
1857
|
+
if (tag) nodes = nodes.filter(n => (n.tags || []).includes(tag));
|
|
1858
|
+
|
|
1653
1859
|
res.json({ nodes, brain_id: brainId, count: nodes.length });
|
|
1654
1860
|
} catch (err) { serverError(res, err); }
|
|
1655
1861
|
});
|
|
@@ -1789,4 +1995,76 @@ function getLastModified(filePath) {
|
|
|
1789
1995
|
return null;
|
|
1790
1996
|
}
|
|
1791
1997
|
|
|
1998
|
+
/**
|
|
1999
|
+
* GET /api/update/check
|
|
2000
|
+
*/
|
|
2001
|
+
router.get('/api/update/check', requireAuth, async (req, res) => {
|
|
2002
|
+
try {
|
|
2003
|
+
const localPkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
|
|
2004
|
+
const currentVersion = localPkg.version || '0.0.0';
|
|
2005
|
+
|
|
2006
|
+
const registryRes = await fetch('https://registry.npmjs.org/total-recall-brain/latest');
|
|
2007
|
+
if (!registryRes.ok) {
|
|
2008
|
+
throw new Error(`Failed to fetch latest version from npm: ${registryRes.status}`);
|
|
2009
|
+
}
|
|
2010
|
+
const registryData = await registryRes.json();
|
|
2011
|
+
const latestVersion = registryData.version || '0.0.0';
|
|
2012
|
+
|
|
2013
|
+
const updateAvailable = latestVersion !== currentVersion;
|
|
2014
|
+
|
|
2015
|
+
res.json({
|
|
2016
|
+
currentVersion,
|
|
2017
|
+
latestVersion,
|
|
2018
|
+
updateAvailable
|
|
2019
|
+
});
|
|
2020
|
+
} catch (err) {
|
|
2021
|
+
serverError(res, err);
|
|
2022
|
+
}
|
|
2023
|
+
});
|
|
2024
|
+
|
|
2025
|
+
/**
|
|
2026
|
+
* POST /api/update/run
|
|
2027
|
+
*/
|
|
2028
|
+
router.post('/api/update/run', requireAuth, async (req, res) => {
|
|
2029
|
+
try {
|
|
2030
|
+
res.json({ success: true, message: 'Update started. Server is updating and will restart shortly.' });
|
|
2031
|
+
|
|
2032
|
+
setImmediate(async () => {
|
|
2033
|
+
logger.info('update', 'Starting auto-update process...');
|
|
2034
|
+
try {
|
|
2035
|
+
const { exec } = await import('node:child_process');
|
|
2036
|
+
|
|
2037
|
+
const runCmd = (cmd, cwd) => new Promise((resolve, reject) => {
|
|
2038
|
+
logger.info('update', `Running: ${cmd} in ${cwd}`);
|
|
2039
|
+
exec(cmd, { cwd }, (err, stdout, stderr) => {
|
|
2040
|
+
if (err) {
|
|
2041
|
+
logger.error('update', `Command failed: ${cmd}. Error: ${err.message}`);
|
|
2042
|
+
return reject(err);
|
|
2043
|
+
}
|
|
2044
|
+
resolve({ stdout, stderr });
|
|
2045
|
+
});
|
|
2046
|
+
});
|
|
2047
|
+
|
|
2048
|
+
// 1. git pull
|
|
2049
|
+
await runCmd('git pull', ROOT);
|
|
2050
|
+
// 2. npm install
|
|
2051
|
+
await runCmd('npm install', ROOT);
|
|
2052
|
+
// 3. build/install frontend if package.json exists
|
|
2053
|
+
const frontendPath = path.join(ROOT, 'frontend');
|
|
2054
|
+
if (fs.existsSync(path.join(frontendPath, 'package.json'))) {
|
|
2055
|
+
await runCmd('npm install', frontendPath);
|
|
2056
|
+
await runCmd('npm run build', frontendPath);
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
logger.info('update', 'Update successfully applied. Restarting server...');
|
|
2060
|
+
process.exit(0);
|
|
2061
|
+
} catch (err) {
|
|
2062
|
+
logger.error('update', `Auto-update failed: ${err.message}`);
|
|
2063
|
+
}
|
|
2064
|
+
});
|
|
2065
|
+
} catch (err) {
|
|
2066
|
+
serverError(res, err);
|
|
2067
|
+
}
|
|
2068
|
+
});
|
|
2069
|
+
|
|
1792
2070
|
export { router as restRouter };
|