total-recall-brain 3.13.0 → 3.13.2

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 (152) hide show
  1. package/README.md +91 -108
  2. package/bin/antigravity.mjs +81 -52
  3. package/bin/total-recall.mjs +32 -57
  4. package/extension/background.js +1 -1
  5. package/extension/options/options.js +9 -3
  6. package/extension/sidepanel/sidepanel.js +0 -2
  7. package/frontend/index.html +4 -3
  8. package/frontend/package.json +1 -0
  9. package/frontend/src/App.tsx +109 -152
  10. package/frontend/src/api.ts +605 -23
  11. package/frontend/src/components/BrainSelector.tsx +211 -199
  12. package/frontend/src/components/DocumentEditorModal.tsx +57 -0
  13. package/frontend/src/components/DocumentTable.tsx +47 -0
  14. package/frontend/src/components/Graph3D.tsx +120 -71
  15. package/frontend/src/components/UsageChart.tsx +3 -3
  16. package/frontend/src/components/brand/BrandMark.tsx +95 -0
  17. package/frontend/src/index.css +286 -86
  18. package/frontend/src/pages/ApiKeysPage.tsx +1878 -215
  19. package/frontend/src/pages/ChatPage.tsx +103 -45
  20. package/frontend/src/pages/DesignDocsPage.tsx +5 -5
  21. package/frontend/src/pages/FilesPage.tsx +10 -6
  22. package/frontend/src/pages/GraphPage.tsx +5 -3
  23. package/frontend/src/pages/HelpPage.tsx +2 -2
  24. package/frontend/src/pages/InboxPage.tsx +82 -0
  25. package/frontend/src/pages/InstructionsPage.tsx +1 -1
  26. package/frontend/src/pages/IntegrationsPage.tsx +21 -10
  27. package/frontend/src/pages/LoginPage.tsx +95 -77
  28. package/frontend/src/pages/MemoryPage.tsx +31 -8
  29. package/frontend/src/pages/OnboardingPage.tsx +690 -0
  30. package/frontend/src/pages/OpenWikiPage.tsx +412 -323
  31. package/frontend/src/pages/SettingsPage.tsx +825 -922
  32. package/frontend/src/pages/SkillsPage.tsx +272 -147
  33. package/frontend/src/pages/UsagePage.tsx +23 -31
  34. package/frontend/src/pages/VaultPage.tsx +223 -0
  35. package/frontend/src/types.ts +13 -0
  36. package/package.json +4 -4
  37. package/scaffold/.agent/skills/total-recall/SKILL.md +43 -4
  38. package/scaffold/.agent/skills/total-recall/modules/README.md +10 -0
  39. package/scaffold/.agent/skills/total-recall/modules/agents/README.md +5 -0
  40. package/scaffold/.agent/skills/total-recall/{skills/tr-cli-agents → modules/agents}/agents.yml +8 -0
  41. package/scaffold/.agent/skills/total-recall/modules/skill-deploy/README.md +9 -0
  42. package/scaffold/.agent/skills/total-recall/openwiki/README.md +10 -0
  43. package/scaffold/.agent/skills/total-recall/openwiki/architecture.md +5 -0
  44. package/scaffold/.agent/skills/total-recall/openwiki/ide.md +10 -0
  45. package/scaffold/.agent/skills/total-recall/openwiki/memory.md +11 -0
  46. package/scaffold/.agent/skills/total-recall/openwiki/secrets.md +7 -0
  47. package/scaffold/.agent/skills/total-recall/openwiki/skills.md +7 -0
  48. package/src/cli/brain.mjs +63 -5
  49. package/src/cli/connect.mjs +11 -19
  50. package/src/cli/daemon.mjs +5 -4
  51. package/src/cli/deploy-ui.mjs +8 -3
  52. package/src/cli/deploy.mjs +84 -27
  53. package/src/cli/ingest/index.mjs +1 -1
  54. package/src/cli/init.mjs +63 -34
  55. package/src/cli/remember.mjs +3 -3
  56. package/src/cli/secret.mjs +812 -0
  57. package/src/cli/setup.mjs +34 -7
  58. package/src/cli/share.mjs +2 -2
  59. package/src/cli/skill.mjs +396 -35
  60. package/src/cli/sync.mjs +4 -4
  61. package/src/cli/task.mjs +287 -0
  62. package/src/core/clarity-rewriter.mjs +5 -5
  63. package/src/core/config.mjs +44 -2
  64. package/src/core/conflict-detector.mjs +5 -5
  65. package/src/core/crons.mjs +54 -0
  66. package/src/core/daemon-loop.mjs +87 -235
  67. package/src/core/dream.mjs +170 -10
  68. package/src/core/env-import.mjs +456 -0
  69. package/src/core/fact-seeker.mjs +14 -14
  70. package/src/core/import-rules.mjs +20 -7
  71. package/src/core/inference-engine.mjs +3 -3
  72. package/src/core/migrate.mjs +14 -8
  73. package/src/core/okf-adapter.mjs +2 -2
  74. package/src/core/operation-validator.mjs +39 -331
  75. package/src/core/optimizer.mjs +7 -0
  76. package/src/core/post-mortem.mjs +2 -2
  77. package/src/core/project-brain.mjs +349 -0
  78. package/src/core/provider-catalog.mjs +296 -0
  79. package/src/core/quick-capture.mjs +1 -1
  80. package/src/core/remote-vault-sync.mjs +194 -0
  81. package/src/core/research-queue.mjs +7 -11
  82. package/src/core/runtime.mjs +33 -9
  83. package/src/core/scheduler.mjs +59 -5
  84. package/src/core/schema.mjs +14 -0
  85. package/src/core/search.mjs +1 -1
  86. package/src/core/secrets-env-export.mjs +323 -0
  87. package/src/core/secrets-rotate.mjs +245 -0
  88. package/src/core/secrets-store.mjs +645 -0
  89. package/src/core/semantic-index.mjs +3 -3
  90. package/src/core/skills-registry.mjs +1062 -0
  91. package/src/core/source-adapters.mjs +4 -1
  92. package/src/core/ssss-host-extension.mjs +94 -0
  93. package/src/core/ssss-kernel-bridge.mjs +530 -0
  94. package/src/core/surface.mjs +14 -0
  95. package/src/core/task-envelope.mjs +298 -0
  96. package/src/core/task-executors.mjs +600 -0
  97. package/src/core/validated-write.mjs +124 -56
  98. package/src/core/vault.mjs +21 -34
  99. package/src/core/webauthn-store.mjs +272 -0
  100. package/src/server/api.mjs +26 -16
  101. package/src/server/auth.mjs +51 -0
  102. package/src/server/index.mjs +3 -3
  103. package/src/server/keys.mjs +30 -4
  104. package/src/server/rest.mjs +198 -25
  105. package/src/server/route-manifest.json +156 -0
  106. package/src/server/routes/_shared.mjs +40 -1
  107. package/src/server/routes/docs.mjs +260 -0
  108. package/src/server/routes/memory.mjs +145 -32
  109. package/src/server/routes/research.mjs +30 -0
  110. package/src/server/routes/secrets.mjs +470 -0
  111. package/src/server/routes/share.mjs +2 -2
  112. package/src/server/routes/skills.mjs +452 -54
  113. package/src/server/routes/sync.mjs +48 -0
  114. package/src/server/routes/webauthn.mjs +184 -0
  115. package/src/server/update-route-manifest.mjs +39 -0
  116. package/templates/onboarding-interview.md +32 -22
  117. package/templates/openwiki/README.md +10 -0
  118. package/templates/openwiki/architecture.md +5 -0
  119. package/templates/openwiki/ide.md +10 -0
  120. package/templates/openwiki/memory.md +11 -0
  121. package/templates/openwiki/secrets.md +7 -0
  122. package/templates/openwiki/skills.md +7 -0
  123. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/SKILL.md +0 -121
  124. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/antigravity.txt +0 -20
  125. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/claude.txt +0 -58
  126. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/codex.txt +0 -81
  127. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/gemini.txt +0 -44
  128. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/scripts/.gitkeep +0 -0
  129. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/subagents/.gitkeep +0 -0
  130. package/scaffold/.agent/skills/total-recall/skills/tr-research/SKILL.md +0 -338
  131. package/scaffold/.agent/skills/total-recall/skills/tr-skill/SKILL.md +0 -282
  132. package/scaffold/.agent/skills/total-recall/skills/tr-skill/assets/skill-template/SKILL.md +0 -20
  133. package/scaffold/.agent/skills/total-recall/skills/tr-skill/evals/evals.json +0 -20
  134. package/scaffold/.agent/skills/total-recall/skills/tr-skill/hooks/on-skill-add.sh +0 -14
  135. package/scaffold/.agent/skills/total-recall/skills/tr-skill/hooks/on-skill-delete.sh +0 -14
  136. package/scaffold/.agent/skills/total-recall/skills/tr-skill/learnings/.gitkeep +0 -0
  137. package/scaffold/.agent/skills/total-recall/skills/tr-skill/references/.gitkeep +0 -0
  138. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/.gitkeep +0 -0
  139. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/create-skill.sh +0 -65
  140. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/enforce-skill-optimization.mjs +0 -135
  141. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/skill-watcher.mjs +0 -64
  142. package/scaffold/.agent/skills/total-recall/skills/tr-skill/subagents/audit-all-skills.md +0 -62
  143. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/SKILL.md +0 -298
  144. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/evals/evals.json +0 -17
  145. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/CHANGELOG.md +0 -124
  146. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/ssss-spec.md +0 -1325
  147. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/scripts/validate-schema.mjs +0 -232
  148. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/subagents/audit-ssss-node.md +0 -9
  149. /package/scaffold/.agent/skills/total-recall/{skills/tr-cli-agents/evals → modules/skill-deploy/scripts}/.gitkeep +0 -0
  150. /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/find-skills.mjs +0 -0
  151. /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/install-skill.mjs +0 -0
  152. /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/scan-skill.mjs +0 -0
@@ -404,6 +404,57 @@ export async function changePasswordHandler(req, res) {
404
404
  res.json({ success: true });
405
405
  }
406
406
 
407
+ /**
408
+ * Short-lived step-up token for high-risk actions (secret reveal / copy).
409
+ * Requires a fresh passkey assertion or password re-entry to mint.
410
+ *
411
+ * @param {{ purpose?: string, ttlSeconds?: number, actor?: string }} opts
412
+ */
413
+ export function mintStepUpToken(opts = {}) {
414
+ const purpose = opts.purpose || 'secrets:reveal';
415
+ const ttl = Math.min(300, Math.max(15, opts.ttlSeconds || 60));
416
+ return jwt.sign(
417
+ {
418
+ purpose,
419
+ step_up: true,
420
+ actor: opts.actor || 'dashboard',
421
+ },
422
+ JWT_SECRET,
423
+ { expiresIn: ttl },
424
+ );
425
+ }
426
+
427
+ /**
428
+ * @param {string} token
429
+ * @param {string} expectedPurpose
430
+ * @returns {{ ok: true, payload: object } | { ok: false, error: string }}
431
+ */
432
+ export function verifyStepUpToken(token, expectedPurpose = 'secrets:reveal') {
433
+ if (!token || typeof token !== 'string') {
434
+ return { ok: false, error: 'Step-up token required' };
435
+ }
436
+ try {
437
+ const payload = jwt.verify(token, JWT_SECRET);
438
+ if (!payload?.step_up || payload.purpose !== expectedPurpose) {
439
+ return { ok: false, error: 'Invalid step-up purpose' };
440
+ }
441
+ return { ok: true, payload };
442
+ } catch {
443
+ return { ok: false, error: 'Step-up token invalid or expired — re-authenticate with passkey' };
444
+ }
445
+ }
446
+
447
+ /**
448
+ * Re-check dashboard password (step-up when no passkey registered).
449
+ */
450
+ export async function verifyDashboardPassword(password) {
451
+ if (!password) return false;
452
+ const config = loadSecurityConfig();
453
+ const passwordHash = config.dashboard?.password_hash;
454
+ if (!passwordHash) return false;
455
+ return bcrypt.compare(String(password), passwordHash);
456
+ }
457
+
407
458
  export function logoutHandler(req, res) {
408
459
  res.clearCookie('session');
409
460
  res.json({ success: true });
@@ -114,7 +114,7 @@ app.get('/health', requireAuthOrLocal, async (req, res) => {
114
114
  // CLI agent availability
115
115
  const { findBinaryInPath } = await import('../core/runtime.mjs');
116
116
  const cliAgents = [];
117
- for (const bin of ['antigravity', 'gemini', 'claude', 'codex']) {
117
+ for (const bin of ['antigravity', 'grok', 'gemini', 'claude', 'codex']) {
118
118
  if (findBinaryInPath(bin)) cliAgents.push(bin);
119
119
  }
120
120
 
@@ -179,7 +179,7 @@ app.get('/health', requireAuthOrLocal, async (req, res) => {
179
179
  app.get('/api/health', requireAuth, requireScope('health:read'), async (req, res) => {
180
180
  const { findBinaryInPath } = await import('../core/runtime.mjs');
181
181
  const agents = [];
182
- for (const bin of ['antigravity', 'gemini', 'claude', 'codex']) {
182
+ for (const bin of ['antigravity', 'grok', 'gemini', 'claude', 'codex']) {
183
183
  if (findBinaryInPath(bin)) agents.push(bin);
184
184
  }
185
185
 
@@ -189,7 +189,7 @@ app.get('/api/health', requireAuth, requireScope('health:read'), async (req, res
189
189
  runtime: 'cli-agents',
190
190
  uptime_seconds: Math.floor(process.uptime()),
191
191
  capabilities: [],
192
- reason: 'No CLI agents found. Install antigravity, claude, or codex.',
192
+ reason: 'No CLI agents found. Install antigravity, grok, claude, or codex.',
193
193
  });
194
194
  }
195
195
 
@@ -25,6 +25,7 @@ export const KNOWN_SCOPES = [
25
25
  'models:read',
26
26
  'instructions:read',
27
27
  'ssss:read',
28
+ 'ssss:write',
28
29
  'memory:read',
29
30
  'memory:write',
30
31
  'memory:recompile',
@@ -52,6 +53,12 @@ function safeEqualHex(a, b) {
52
53
 
53
54
  function normalizeKey(raw) {
54
55
  const key = { ...raw };
56
+ // name must always be a string — guard against bad issueKey({ name }) calls
57
+ if (key.name && typeof key.name === 'object') {
58
+ key.name = String(key.name.name || key.name.label || 'Unnamed Key');
59
+ } else if (typeof key.name !== 'string' || !key.name.trim()) {
60
+ key.name = key.name == null ? 'Unnamed Key' : String(key.name);
61
+ }
55
62
  if (key.token && typeof key.token === 'string') {
56
63
  if (key.token === 'local') {
57
64
  key.revoked = true;
@@ -129,16 +136,35 @@ function saveKeys(keys) {
129
136
  fs.writeFileSync(KEYS_FILE, sanitized.map(k => JSON.stringify(k)).join('\n') + (sanitized.length ? '\n' : ''), { encoding: 'utf8', mode: 0o600 });
130
137
  }
131
138
 
132
- export function issueKey(name, options = {}) {
139
+ /**
140
+ * Issue a PAT.
141
+ * @param {string|object} nameOrOpts - display name, or { name, scopes, expires_at }
142
+ * @param {object} [options]
143
+ */
144
+ export function issueKey(nameOrOpts, options = {}) {
145
+ let name = nameOrOpts;
146
+ let opts = options;
147
+ // Accept issueKey({ name, scopes }) form safely
148
+ if (nameOrOpts && typeof nameOrOpts === 'object' && !Array.isArray(nameOrOpts)) {
149
+ name = nameOrOpts.name || nameOrOpts.label || 'Unnamed Key';
150
+ opts = {
151
+ scopes: nameOrOpts.scopes ?? options.scopes,
152
+ expires_at: nameOrOpts.expires_at ?? nameOrOpts.expiresAt ?? options.expires_at ?? options.expiresAt,
153
+ };
154
+ }
155
+ if (typeof name !== 'string' || !name.trim()) {
156
+ name = 'Unnamed Key';
157
+ }
158
+
133
159
  const keys = loadKeys();
134
160
  const token = `tr_${crypto.randomBytes(32).toString('base64url')}`;
135
161
  const key = {
136
162
  id: crypto.randomUUID(),
137
- name: name || 'Unnamed Key',
163
+ name: name.trim(),
138
164
  token_hash: hashToken(token),
139
165
  token_prefix: token.slice(0, 8),
140
- scopes: normalizeScopes(options.scopes),
141
- expires_at: options.expires_at || options.expiresAt || null,
166
+ scopes: normalizeScopes(opts.scopes),
167
+ expires_at: opts.expires_at || opts.expiresAt || null,
142
168
  created_at: new Date().toISOString(),
143
169
  last_used_at: null,
144
170
  hit_count: 0,
@@ -33,7 +33,7 @@
33
33
  * GET /api/config get sanitized runtime + security config
34
34
  *
35
35
  * Models (OpenAI-compatible extension)
36
- * GET /v1/models list available Ollama models
36
+ * GET /v1/models list available models (OpenAI-compatible)
37
37
  *
38
38
  * Vault (admin operations)
39
39
  * POST /api/vault/compile trigger full surface compile (re-generates compile.json)
@@ -81,12 +81,16 @@ import { synthesize as synthesizeTts, isTtsEnabled, TtsNotConfiguredError } from
81
81
  import { logger } from '../core/logger.mjs';
82
82
  import { memoryRouter } from './routes/memory.mjs';
83
83
  import { keysRouter } from './routes/keys.mjs';
84
+ import { secretsRouter } from './routes/secrets.mjs';
84
85
  import { sessionsRouter } from './routes/sessions.mjs';
85
86
  import { shareRouter } from './routes/share.mjs';
86
87
  import { authRouter } from './routes/auth.mjs';
88
+ import { webauthnRouter } from './routes/webauthn.mjs';
87
89
  import { sandboxRouter } from './routes/sandbox.mjs';
88
90
  import { researchRouter } from './routes/research.mjs';
89
91
  import { skillsRouter } from './routes/skills.mjs';
92
+ import { docsRouter } from './routes/docs.mjs';
93
+ import syncRouter from './routes/sync.mjs';
90
94
  // ollamaUrl removed — CLI agents replace Ollama
91
95
  import {
92
96
  AGENT_DIR,
@@ -101,7 +105,7 @@ import {
101
105
  CONFIG_DIR
102
106
  } from './routes/_shared.mjs';
103
107
 
104
- const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
108
+ const ROOT = process.env.TR_ROOT || path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
105
109
  const MODEL_CATALOG_DIR = path.join(ROOT, 'models', 'catalog', 'total-recall');
106
110
 
107
111
  function listFilesRecursive(root, predicate) {
@@ -161,14 +165,18 @@ const router = express.Router();
161
165
  // handlers below so URL precedence stays identical to the pre-refactor file.
162
166
  router.use(memoryRouter);
163
167
  router.use(keysRouter);
168
+ router.use(secretsRouter);
164
169
  router.use(sessionsRouter);
165
170
  router.use(shareRouter);
166
171
  router.use(authRouter);
172
+ router.use(webauthnRouter);
167
173
  router.use(sandboxRouter);
168
174
  router.use(researchRouter);
169
175
  router.use(skillsRouter);
176
+ router.use(docsRouter);
177
+ router.use(syncRouter);
170
178
 
171
- // ─── Helpers ──────────────────────────────────────────────────────────────────
179
+ // ─── Brain Routing Middleware ──────────────────────────────────────────────────────────────────
172
180
 
173
181
  function notFound(res, msg) {
174
182
  return res.status(404).json({ error: msg || 'Not found' });
@@ -198,7 +206,6 @@ function sanitizeNode({ body, ...rest }) {
198
206
  * POST /api/memory/search/semantic
199
207
  * Body: { query: string, top_k?: number }
200
208
  * Returns top-k vault nodes ranked by vector similarity to the query.
201
- * Requires Ollama with nomic-embed-text running.
202
209
  */
203
210
  router.post('/api/memory/search/semantic', requireAuth, requireScope('memory:read'), async (req, res) => {
204
211
  try {
@@ -208,7 +215,6 @@ router.post('/api/memory/search/semantic', requireAuth, requireScope('memory:rea
208
215
  if (results.length === 0) return res.status(503).json({ error: 'Embeddings index is empty. Run POST /api/vault/compile to build it.' });
209
216
  res.json({ query, top_k: Math.min(Number(top_k) || 5, 20), results });
210
217
  } catch (err) {
211
- if (err.message?.includes('Ollama')) return res.status(503).json({ error: err.message });
212
218
  serverError(res, err);
213
219
  }
214
220
  });
@@ -233,7 +239,7 @@ router.post('/api/vault/compile', requireAuth, requireScope('memory:recompile'),
233
239
  const vaultNodes = getNodes(VAULT_DIR);
234
240
  vaultEmbed = await buildEmbeddingsIndex(vaultNodes, DERIVED_DIR);
235
241
  sessionEmbed = await buildSessionEmbeddingsIndex(SESSIONS_DIR, DERIVED_DIR);
236
- } catch { /* Ollama may not be running — non-fatal */ }
242
+ } catch { /* embedding service may be unavailable — non-fatal */ }
237
243
  res.json({ compiled: true, elapsed_ms: Date.now() - start, vault_embeddings: vaultEmbed, session_embeddings: sessionEmbed });
238
244
  } catch (err) {
239
245
  serverError(res, err);
@@ -633,7 +639,7 @@ router.get('/api', (req, res) => {
633
639
  'GET /api/memory/stats': 'Node counts by category',
634
640
  'GET /api/memory/:slug': 'Get node by slug',
635
641
  'POST /api/memory': 'Create node (slug, title, category, content)',
636
- 'POST /api/memory/search/semantic': 'Semantic search by meaning (query, top_k) — requires Ollama',
642
+ 'POST /api/memory/search/semantic': 'Semantic search by meaning (query, top_k)',
637
643
  'PUT /api/memory/:slug': 'Replace node',
638
644
  'PATCH /api/memory/:slug': 'Partial update',
639
645
  'DELETE /api/memory/:slug': 'Delete node',
@@ -666,7 +672,7 @@ router.get('/api', (req, res) => {
666
672
  },
667
673
  discovery: {
668
674
  'GET /.well-known/total-recall.json': 'Client auto-config manifest',
669
- 'GET /health': 'System health (disk, ollama, vault)',
675
+ 'GET /health': 'System health (disk, embedding service, vault)',
670
676
  },
671
677
  },
672
678
  scopes: {
@@ -751,7 +757,7 @@ router.post('/api/integrations/connect', requireAuth, (req, res) => {
751
757
 
752
758
  const validClients = [
753
759
  'vscode', 'pi', 'hermes', 'openclaw', 'cursor', 'claude-code',
754
- 'codex', 'gemini', 'aider', 'ultrachat', 'obsidian',
760
+ 'codex', 'gemini', 'aider', 'http-api', 'obsidian',
755
761
  'generic', 'antigravity'
756
762
  ];
757
763
 
@@ -815,7 +821,7 @@ router.get('/api/import/rules', requireAuth, requireScope('memory:read'), (req,
815
821
  * Import rule files into the vault.
816
822
  * Body: { dirs?: string[], files?: string[], force?: boolean, dryRun?: boolean }
817
823
  */
818
- router.post('/api/import/rules', requireAuth, requireScope('memory:write'), (req, res) => {
824
+ router.post('/api/import/rules', requireAuth, requireScope('memory:write'), async (req, res) => {
819
825
  try {
820
826
  const { dirs, force = false, dryRun = false } = req.body || {};
821
827
  const searchDirs = dirs?.length ? dirs : [process.cwd(), os.homedir()];
@@ -824,7 +830,7 @@ router.post('/api/import/rules', requireAuth, requireScope('memory:write'), (req
824
830
  ? detected.filter(f => req.body.files.includes(f.absolutePath))
825
831
  : detected.filter(f => !f.alreadyImported || force);
826
832
  if (dryRun) return res.json({ dryRun: true, detected, toImport, imported: [], skipped: [], failed: [] });
827
- const result = importRuleFiles(toImport, { force, vaultDir: VAULT_DIR });
833
+ const result = await importRuleFiles(toImport, { force, vaultDir: VAULT_DIR });
828
834
  res.json({ detected, ...result });
829
835
 
830
836
  } catch (err) { serverError(res, err); }
@@ -1519,8 +1525,50 @@ router.post('/api/tts', requireAuth, requireScope('tts:use'), async (req, res) =
1519
1525
 
1520
1526
  // ─── Instructions (sync consumers) ─────────────────────────────────────────────
1521
1527
 
1528
+ router.get('/api/dashboard/instructions', requireAuth, requireScope('instructions:read'), (req, res) => {
1529
+ const surfaces = [];
1530
+ const surfaceFiles = ['AGENTS.md', 'GEMINI.md', 'CLAUDE.md', 'INSTRUCTIONS.md'];
1531
+
1532
+ for (const name of surfaceFiles) {
1533
+ let filePath;
1534
+ if (name === 'INSTRUCTIONS.md') {
1535
+ filePath = path.join(ROOT, name);
1536
+ } else {
1537
+ filePath = path.join(ROOT, name);
1538
+ }
1539
+
1540
+ let size = 0;
1541
+ let lastCompiled = '';
1542
+ let active = false;
1543
+
1544
+ if (fs.existsSync(filePath)) {
1545
+ const stat = fs.statSync(filePath);
1546
+ size = stat.size;
1547
+ lastCompiled = stat.mtime.toISOString();
1548
+ active = true;
1549
+ }
1550
+
1551
+ surfaces.push({
1552
+ name,
1553
+ filename: name,
1554
+ size,
1555
+ lastCompiled,
1556
+ active
1557
+ });
1558
+ }
1559
+
1560
+ const lastCompileTimestamp = fs.existsSync(INSTRUCTIONS) ? fs.statSync(INSTRUCTIONS).mtime.toISOString() : '';
1561
+ const totalNodes = fs.existsSync(VAULT_DIR) ? getNodes(VAULT_DIR).length : 0;
1562
+
1563
+ res.json({
1564
+ surfaces,
1565
+ lastCompileTimestamp,
1566
+ totalNodes
1567
+ });
1568
+ });
1569
+
1522
1570
  router.get('/api/instructions', requireAuth, requireScope('instructions:read'), (req, res) => {
1523
- return sendTextResource(res, INSTRUCTIONS, 'instructions');
1571
+ return sendTextResource(res, path.join(ROOT, 'INSTRUCTIONS.md'), 'instructions');
1524
1572
  });
1525
1573
 
1526
1574
  router.put('/api/instructions', requireAuth, requireScope('instructions:write'), (req, res) => {
@@ -1574,7 +1622,23 @@ function absoluteUrl(req, routePath) {
1574
1622
  }
1575
1623
 
1576
1624
  function ssssReferenceDir() {
1577
- return path.join(SKILLS_DIR, 'total-recall', 'skills', 'tr-ssss', 'references');
1625
+ // Prefer compact TR references; fall back to legacy nested module paths.
1626
+ const candidates = [
1627
+ path.join(SKILLS_DIR, 'total-recall', 'references'),
1628
+ path.join(SKILLS_DIR, 'total-recall', 'modules', 'ssss', 'references'),
1629
+ path.join(SKILLS_DIR, 'total-recall', 'skills', 'tr-ssss', 'references'),
1630
+ ];
1631
+ return candidates.find((p) => fs.existsSync(p)) || candidates[0];
1632
+ }
1633
+
1634
+ function ssssSkillDocPath() {
1635
+ const candidates = [
1636
+ path.join(SKILLS_DIR, 'total-recall', 'references', 'ssss-reference.md'),
1637
+ path.join(SKILLS_DIR, 'total-recall', 'SKILL.md'),
1638
+ path.join(SKILLS_DIR, 'total-recall', 'modules', 'ssss', 'MODULE.md'),
1639
+ path.join(SKILLS_DIR, 'total-recall', 'skills', 'tr-ssss', 'SKILL.md'),
1640
+ ];
1641
+ return candidates.find((p) => fs.existsSync(p)) || candidates[0];
1578
1642
  }
1579
1643
 
1580
1644
  function listSsssReferences(req) {
@@ -1615,7 +1679,7 @@ router.get('/api/ssss', requireAuth, requireScope('ssss:read'), (req, res) => {
1615
1679
  name: 'ssss-skill',
1616
1680
  url: absoluteUrl(req, '/api/ssss/skill/ssss'),
1617
1681
  ...(() => {
1618
- const r = readTextResource(path.join(SKILLS_DIR, 'total-recall', 'skills', 'tr-ssss', 'SKILL.md'), 'ssss-skill');
1682
+ const r = readTextResource(ssssSkillDocPath(), 'ssss-skill');
1619
1683
  return r ? { sha256: r.sha256, bytes: r.bytes, modified: r.modified } : { sha256: null, bytes: 0, modified: null };
1620
1684
  })()
1621
1685
  },
@@ -1623,7 +1687,11 @@ router.get('/api/ssss', requireAuth, requireScope('ssss:read'), (req, res) => {
1623
1687
  name: 'ssss-spec',
1624
1688
  url: absoluteUrl(req, '/api/ssss/spec'),
1625
1689
  ...(() => {
1626
- const r = readTextResource(path.join(ssssReferenceDir(), 'ssss-spec.md'), 'ssss-spec');
1690
+ const refs = ssssReferenceDir();
1691
+ const specPath = ['ssss-reference.md', 'ssss-spec.md']
1692
+ .map((n) => path.join(refs, n))
1693
+ .find((p) => fs.existsSync(p));
1694
+ const r = specPath ? readTextResource(specPath, 'ssss-spec') : null;
1627
1695
  return r ? { sha256: r.sha256, bytes: r.bytes, modified: r.modified } : { sha256: null, bytes: 0, modified: null };
1628
1696
  })()
1629
1697
  },
@@ -1637,16 +1705,29 @@ router.get('/api/ssss', requireAuth, requireScope('ssss:read'), (req, res) => {
1637
1705
  });
1638
1706
  });
1639
1707
 
1640
- router.get('/api/ssss/instructions', requireAuth, requireScope('ssss:read', 'instructions:read'), (_req, res) => {
1641
- return sendTextResource(res, INSTRUCTIONS, 'instructions');
1708
+ router.get('/api/ssss/instructions', requireAuth, requireScope('ssss:read', 'instructions:read'), (req, res) => {
1709
+ const surface = req.query.surface;
1710
+ if (surface) {
1711
+ if (surface === 'INSTRUCTIONS.md') {
1712
+ return sendTextResource(res, path.join(ROOT, 'INSTRUCTIONS.md'), 'instructions');
1713
+ }
1714
+ const safeSurface = path.basename(surface);
1715
+ const surfacePath = path.join(ROOT, safeSurface);
1716
+ return sendTextResource(res, surfacePath, safeSurface);
1717
+ }
1718
+ return sendTextResource(res, path.join(ROOT, 'INSTRUCTIONS.md'), 'instructions');
1642
1719
  });
1643
1720
 
1644
1721
  router.get('/api/ssss/skill/ssss', requireAuth, requireScope('ssss:read'), (_req, res) => {
1645
- return sendTextResource(res, path.join(SKILLS_DIR, 'total-recall', 'skills', 'tr-ssss', 'SKILL.md'), 'ssss-skill');
1722
+ return sendTextResource(res, ssssSkillDocPath(), 'ssss-skill');
1646
1723
  });
1647
1724
 
1648
1725
  router.get('/api/ssss/spec', requireAuth, requireScope('ssss:read'), (_req, res) => {
1649
- return sendTextResource(res, path.join(ssssReferenceDir(), 'ssss-spec.md'), 'ssss-spec');
1726
+ const refs = ssssReferenceDir();
1727
+ const specPath = ['ssss-reference.md', 'ssss-spec.md']
1728
+ .map((n) => path.join(refs, n))
1729
+ .find((p) => fs.existsSync(p));
1730
+ return sendTextResource(res, specPath || path.join(refs, 'ssss-reference.md'), 'ssss-spec');
1650
1731
  });
1651
1732
 
1652
1733
  router.get('/api/ssss/references', requireAuth, requireScope('ssss:read'), (req, res) => {
@@ -1666,7 +1747,7 @@ router.get('/api/ssss/references/:name', requireAuth, requireScope('ssss:read'),
1666
1747
  * List all known brains (global + registered projects) with state metadata.
1667
1748
  * The global brain reads project brain frontmatter for state display.
1668
1749
  */
1669
- router.get('/api/brains', requireAuth, requireScope('ssss:read'), (req, res) => {
1750
+ router.get('/api/brains', requireAuth, requireScope('ssss:read'), async (req, res) => {
1670
1751
  try {
1671
1752
  const globalBrainDir = path.join(os.homedir(), '.agent', 'skills', 'total-recall');
1672
1753
  const brains = [];
@@ -1737,7 +1818,47 @@ router.get('/api/brains', requireAuth, requireScope('ssss:read'), (req, res) =>
1737
1818
  last_compiled: project.last_compiled || getLastModified(path.join(project.brainDir, 'memory-derived', 'graph-index.jsonl')),
1738
1819
  });
1739
1820
  }
1740
- } catch { /* ignore registry parse errors */ }
1821
+ } catch (err) {
1822
+ logger.error('server', 'Failed to read project registry', { error: err.message });
1823
+ }
1824
+ }
1825
+
1826
+ // Optional remote/tenant vault from config (path-agnostic — user sets vaultDir)
1827
+ try {
1828
+ const config = await import('../core/config.mjs');
1829
+ if (config.remoteVaultSync?.enabled && config.remoteVaultSync.vaultDir) {
1830
+ const tenantVaultDir = config.remoteVaultSync.vaultDir;
1831
+ const tenantId =
1832
+ process.env.TR_TENANT_ID ||
1833
+ path.basename(path.dirname(tenantVaultDir)) ||
1834
+ 'remote';
1835
+ const exists = fs.existsSync(tenantVaultDir);
1836
+ let nodeCount = 0;
1837
+ if (exists) {
1838
+ const countMd = (dir) => {
1839
+ if (!fs.existsSync(dir)) return 0;
1840
+ let count = 0;
1841
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1842
+ if (entry.isDirectory()) count += countMd(path.join(dir, entry.name));
1843
+ else if (entry.name.endsWith('.md')) count++;
1844
+ }
1845
+ return count;
1846
+ };
1847
+ nodeCount = countMd(tenantVaultDir);
1848
+ }
1849
+ brains.push({
1850
+ id: `tenant:${tenantId}`,
1851
+ name: process.env.TR_TENANT_NAME || `Tenant (${tenantId})`,
1852
+ layer: 'tenant',
1853
+ path: tenantVaultDir,
1854
+ project_root: path.dirname(tenantVaultDir),
1855
+ exists,
1856
+ node_count: nodeCount,
1857
+ last_compiled: null,
1858
+ });
1859
+ }
1860
+ } catch {
1861
+ // ignore
1741
1862
  }
1742
1863
 
1743
1864
  res.json({ brains });
@@ -1749,13 +1870,15 @@ router.get('/api/brains', requireAuth, requireScope('ssss:read'), (req, res) =>
1749
1870
  * GET /api/brains/:id/nodes
1750
1871
  * List all memory nodes for a specific brain.
1751
1872
  */
1752
- router.get('/api/brains/:id/nodes', requireAuth, requireScope('ssss:read', 'memory:read'), (req, res) => {
1873
+ router.get('/api/brains/:id/nodes', requireAuth, requireScope('ssss:read', 'memory:read'), async (req, res) => {
1753
1874
  try {
1754
1875
  const brainId = req.params.id;
1755
1876
  let brainDir;
1877
+ let vaultDir;
1756
1878
 
1757
1879
  if (brainId === 'global') {
1758
1880
  brainDir = path.join(os.homedir(), '.agent', 'skills', 'total-recall');
1881
+ vaultDir = path.join(brainDir, 'memory-vault');
1759
1882
  } else if (brainId.startsWith('project:')) {
1760
1883
  const projectName = brainId.slice('project:'.length);
1761
1884
  const globalBrainDir = path.join(os.homedir(), '.agent', 'skills', 'total-recall');
@@ -1766,14 +1889,35 @@ router.get('/api/brains/:id/nodes', requireAuth, requireScope('ssss:read', 'memo
1766
1889
  const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
1767
1890
  const project = registry.find(p => p.name === projectName);
1768
1891
  if (!project) {
1769
- return res.status(404).json({ error: `Project \"${projectName}\" not found in registry` });
1892
+ return res.status(404).json({ error: `Project "${projectName}" not found in registry` });
1770
1893
  }
1771
1894
  brainDir = project.brainDir;
1895
+ vaultDir = path.join(brainDir, 'memory-vault');
1896
+ } else if (brainId.startsWith('tenant:')) {
1897
+ const tenantName = brainId.slice('tenant:'.length).replace(/[^a-zA-Z0-9._-]/g, '');
1898
+ const config = await import('../core/config.mjs');
1899
+ // Prefer configured remote vault when id matches configured tenant; else ~/.agent/tenants/<name>/vault
1900
+ const configuredId =
1901
+ process.env.TR_TENANT_ID ||
1902
+ (config.remoteVaultSync?.vaultDir
1903
+ ? path.basename(path.dirname(config.remoteVaultSync.vaultDir))
1904
+ : null);
1905
+ if (
1906
+ config.remoteVaultSync?.enabled &&
1907
+ config.remoteVaultSync.vaultDir &&
1908
+ (tenantName === configuredId || tenantName === 'remote' || tenantName === 'default')
1909
+ ) {
1910
+ vaultDir = config.remoteVaultSync.vaultDir;
1911
+ } else {
1912
+ vaultDir = path.join(os.homedir(), '.agent', 'tenants', tenantName, 'vault');
1913
+ if (!fs.existsSync(vaultDir)) {
1914
+ return res.status(404).json({ error: `Tenant "${tenantName}" not found` });
1915
+ }
1916
+ }
1772
1917
  } else {
1773
- return res.status(400).json({ error: 'Invalid brain ID. Use \"global\" or \"project:<name>\"' });
1918
+ return res.status(400).json({ error: 'Invalid brain ID. Use "global", "project:<name>", or "tenant:<name>"' });
1774
1919
  }
1775
1920
 
1776
- const vaultDir = path.join(brainDir, 'memory-vault');
1777
1921
  if (!fs.existsSync(vaultDir)) {
1778
1922
  return res.json({ nodes: [], brain_id: brainId });
1779
1923
  }
@@ -2111,6 +2255,35 @@ router.post('/api/update/run', requireAuth, async (req, res) => {
2111
2255
  }
2112
2256
  });
2113
2257
 
2258
+ // ─── DLQ Admin Routes ────────────────────────────────────────────────────────────
2259
+ const QUEUE_DIR = path.join(BRAIN_DIR, 'scheduler', 'queue');
2260
+
2261
+ router.get('/api/tasks/failed', requireAuth, async (req, res) => {
2262
+ try {
2263
+ const { loadPendingTasks } = await import('../core/scheduler.mjs');
2264
+ const allTasks = loadPendingTasks(QUEUE_DIR);
2265
+ const failed = allTasks.filter(t => t.status === 'failed');
2266
+ res.json({ total: failed.length, tasks: failed });
2267
+ } catch (err) {
2268
+ serverError(res, err);
2269
+ }
2270
+ });
2271
+
2272
+ router.post('/api/tasks/:id/retry', requireAuth, async (req, res) => {
2273
+ try {
2274
+ const taskId = req.params.id;
2275
+ const { loadPendingTasks, updateTaskStatus } = await import('../core/scheduler.mjs');
2276
+ const allTasks = loadPendingTasks(QUEUE_DIR);
2277
+ const task = allTasks.find(t => t.slug === taskId);
2278
+ if (!task) return res.status(404).json({ error: 'Task not found in queue' });
2279
+ if (task.status !== 'failed') return res.status(400).json({ error: 'Task is not in failed state' });
2280
+ updateTaskStatus(task, 'pending', QUEUE_DIR);
2281
+ res.json({ success: true, message: `Task ${taskId} re-queued` });
2282
+ } catch (err) {
2283
+ serverError(res, err);
2284
+ }
2285
+ });
2286
+
2114
2287
  router.get('/api/help', requireAuth, (req, res) => {
2115
2288
  const { topic } = req.query;
2116
2289
  const docsDir = path.join(ROOT, 'docs');