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
@@ -71,10 +71,34 @@
71
71
  "method": "POST",
72
72
  "path": "/api/context/stream"
73
73
  },
74
+ {
75
+ "method": "GET",
76
+ "path": "/api/dashboard/instructions"
77
+ },
74
78
  {
75
79
  "method": "POST",
76
80
  "path": "/api/diagnostics/agents"
77
81
  },
82
+ {
83
+ "method": "DELETE",
84
+ "path": "/api/docs"
85
+ },
86
+ {
87
+ "method": "GET",
88
+ "path": "/api/docs"
89
+ },
90
+ {
91
+ "method": "POST",
92
+ "path": "/api/docs"
93
+ },
94
+ {
95
+ "method": "PUT",
96
+ "path": "/api/docs"
97
+ },
98
+ {
99
+ "method": "GET",
100
+ "path": "/api/docs/read"
101
+ },
78
102
  {
79
103
  "method": "POST",
80
104
  "path": "/api/dream"
@@ -231,6 +255,74 @@
231
255
  "method": "POST",
232
256
  "path": "/api/scripts/:name/run"
233
257
  },
258
+ {
259
+ "method": "GET",
260
+ "path": "/api/secrets"
261
+ },
262
+ {
263
+ "method": "POST",
264
+ "path": "/api/secrets"
265
+ },
266
+ {
267
+ "method": "DELETE",
268
+ "path": "/api/secrets/:key"
269
+ },
270
+ {
271
+ "method": "PATCH",
272
+ "path": "/api/secrets/:key"
273
+ },
274
+ {
275
+ "method": "POST",
276
+ "path": "/api/secrets/:key/reveal"
277
+ },
278
+ {
279
+ "method": "POST",
280
+ "path": "/api/secrets/:key/rotate"
281
+ },
282
+ {
283
+ "method": "POST",
284
+ "path": "/api/secrets/deploy-payload"
285
+ },
286
+ {
287
+ "method": "GET",
288
+ "path": "/api/secrets/export-env"
289
+ },
290
+ {
291
+ "method": "POST",
292
+ "path": "/api/secrets/export-env"
293
+ },
294
+ {
295
+ "method": "POST",
296
+ "path": "/api/secrets/import-env"
297
+ },
298
+ {
299
+ "method": "POST",
300
+ "path": "/api/secrets/parse-env"
301
+ },
302
+ {
303
+ "method": "GET",
304
+ "path": "/api/secrets/providers"
305
+ },
306
+ {
307
+ "method": "GET",
308
+ "path": "/api/secrets/rotation-due"
309
+ },
310
+ {
311
+ "method": "POST",
312
+ "path": "/api/secrets/rotation-due/enqueue"
313
+ },
314
+ {
315
+ "method": "GET",
316
+ "path": "/api/secrets/scan-env"
317
+ },
318
+ {
319
+ "method": "GET",
320
+ "path": "/api/secrets/usage"
321
+ },
322
+ {
323
+ "method": "POST",
324
+ "path": "/api/secrets/usage"
325
+ },
234
326
  {
235
327
  "method": "GET",
236
328
  "path": "/api/sessions"
@@ -267,6 +359,10 @@
267
359
  "method": "PUT",
268
360
  "path": "/api/skills/:name"
269
361
  },
362
+ {
363
+ "method": "GET",
364
+ "path": "/api/skills/:name/files"
365
+ },
270
366
  {
271
367
  "method": "POST",
272
368
  "path": "/api/skills/install"
@@ -299,6 +395,18 @@
299
395
  "method": "GET",
300
396
  "path": "/api/ssss/spec"
301
397
  },
398
+ {
399
+ "method": "POST",
400
+ "path": "/api/sync/remote-vault/proposals/:id/decision"
401
+ },
402
+ {
403
+ "method": "POST",
404
+ "path": "/api/sync/remote-vault/run"
405
+ },
406
+ {
407
+ "method": "GET",
408
+ "path": "/api/sync/remote-vault/status"
409
+ },
302
410
  {
303
411
  "method": "GET",
304
412
  "path": "/api/tasks"
@@ -307,10 +415,18 @@
307
415
  "method": "POST",
308
416
  "path": "/api/tasks"
309
417
  },
418
+ {
419
+ "method": "POST",
420
+ "path": "/api/tasks/:id/retry"
421
+ },
310
422
  {
311
423
  "method": "DELETE",
312
424
  "path": "/api/tasks/cleanup"
313
425
  },
426
+ {
427
+ "method": "GET",
428
+ "path": "/api/tasks/failed"
429
+ },
314
430
  {
315
431
  "method": "POST",
316
432
  "path": "/api/tts"
@@ -347,6 +463,46 @@
347
463
  "method": "GET",
348
464
  "path": "/api/vault/status"
349
465
  },
466
+ {
467
+ "method": "GET",
468
+ "path": "/api/views"
469
+ },
470
+ {
471
+ "method": "POST",
472
+ "path": "/api/views"
473
+ },
474
+ {
475
+ "method": "DELETE",
476
+ "path": "/api/views/:id"
477
+ },
478
+ {
479
+ "method": "POST",
480
+ "path": "/api/webauthn/assert/options"
481
+ },
482
+ {
483
+ "method": "POST",
484
+ "path": "/api/webauthn/assert/verify"
485
+ },
486
+ {
487
+ "method": "DELETE",
488
+ "path": "/api/webauthn/credentials/:id"
489
+ },
490
+ {
491
+ "method": "POST",
492
+ "path": "/api/webauthn/register/options"
493
+ },
494
+ {
495
+ "method": "POST",
496
+ "path": "/api/webauthn/register/verify"
497
+ },
498
+ {
499
+ "method": "GET",
500
+ "path": "/api/webauthn/status"
501
+ },
502
+ {
503
+ "method": "POST",
504
+ "path": "/api/webauthn/step-up/password"
505
+ },
350
506
  {
351
507
  "method": "POST",
352
508
  "path": "/auth/change-password"
@@ -40,7 +40,15 @@ export const MODEL_CATALOG_DIR = path.join(ROOT, 'models', 'catalog', 'total-rec
40
40
  * @returns {string} Absolute path to the memory-vault directory
41
41
  */
42
42
  export function resolveVaultFromQuery(req) {
43
- const brainId = req.query?.brain || req.body?.brainId || req.headers?.['x-total-recall-brain'];
43
+ const rawBrainId = req.query?.brain || req.body?.brainId || req.headers?.['x-total-recall-brain'];
44
+ let brainId = rawBrainId;
45
+
46
+ if (rawBrainId && rawBrainId.includes(',')) {
47
+ const parts = rawBrainId.split(',').map(s => s.trim()).filter(Boolean);
48
+ if (parts.length > 0) {
49
+ brainId = parts[parts.length - 1];
50
+ }
51
+ }
44
52
 
45
53
  // No brain specified or explicitly global → default vault
46
54
  if (!brainId || brainId === 'global') {
@@ -70,10 +78,41 @@ export function resolveVaultFromQuery(req) {
70
78
  }
71
79
  }
72
80
 
81
+ // tenant:<name> → ~/.agent/tenants/<name>/vault (any tenant the user configures)
82
+ if (brainId.startsWith('tenant:')) {
83
+ const tenantName = brainId.slice('tenant:'.length).replace(/[^a-zA-Z0-9._-]/g, '');
84
+ if (tenantName) {
85
+ const tenantVaultDir = path.join(os.homedir(), '.agent', 'tenants', tenantName, 'vault');
86
+ if (fs.existsSync(tenantVaultDir)) {
87
+ return tenantVaultDir;
88
+ }
89
+ }
90
+ }
91
+
73
92
  // Unknown brain ID format or resolution failed → default vault
74
93
  return VAULT_DIR;
75
94
  }
76
95
 
96
+ export function resolveAllVaultsFromQuery(req) {
97
+ const rawBrainId = req.query?.brain || req.body?.brainId || req.headers?.['x-total-recall-brain'];
98
+ if (!rawBrainId) return [VAULT_DIR];
99
+
100
+ const ids = rawBrainId.split(',').map(s => s.trim()).filter(Boolean);
101
+ const vaults = [];
102
+
103
+ for (const id of ids) {
104
+ // We can reuse resolveVaultFromQuery by mocking the req object
105
+ const vault = resolveVaultFromQuery({ query: { brain: id } });
106
+ if (vault) {
107
+ vaults.push(vault);
108
+ }
109
+ }
110
+
111
+ // Deduplicate vaults
112
+ const uniqueVaults = [...new Set(vaults)];
113
+ return uniqueVaults.length > 0 ? uniqueVaults : [VAULT_DIR];
114
+ }
115
+
77
116
  export function notFound(res, msg) {
78
117
  return res.status(404).json({ error: msg || 'Not found' });
79
118
  }
@@ -0,0 +1,260 @@
1
+ import express from 'express';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import crypto from 'node:crypto';
6
+ import matter from 'gray-matter';
7
+
8
+ import { requireAuth, requireScope } from '../auth.mjs';
9
+ import {
10
+ badRequest,
11
+ notFound,
12
+ serverError,
13
+ resolveVaultFromQuery,
14
+ CONFIG_DIR
15
+ } from './_shared.mjs';
16
+ import { getNodes, invalidate } from '../../core/vault-cache.mjs';
17
+ import { processOperationAsync } from '../../core/operation-validator.mjs';
18
+
19
+ const router = express.Router();
20
+
21
+ function getRegistryTypes(brainDir) {
22
+ const globalRegistryPath = path.join(os.homedir(), '.agent', 'skills', 'total-recall', 'config', 'project-registry.json');
23
+ // Portability is resolved per-type from the registry (core + extensions)
24
+ // Since we just need portability mapping for types, let's load it dynamically.
25
+ // Actually, `@ssss/cli` parses things.
26
+ // For now, let's just return a generic helper.
27
+ return {};
28
+ }
29
+
30
+ // Ensure the path is safe
31
+ function isSafePath(p) {
32
+ const normalized = path.normalize(p).replace(/^(\.\.(\/|\\|$))+/, '');
33
+ return normalized === p && !p.startsWith('/') && !p.includes('..');
34
+ }
35
+
36
+ /**
37
+ * GET /api/docs
38
+ * List documents with filtering and pagination.
39
+ */
40
+ router.get('/api/docs', requireAuth, requireScope('ssss:read'), (req, res) => {
41
+ try {
42
+ const vaultDir = resolveVaultFromQuery(req);
43
+ if (!fs.existsSync(vaultDir)) {
44
+ return res.json({ total: 0, docs: [] });
45
+ }
46
+
47
+ let nodes = getNodes(vaultDir);
48
+
49
+ const { type, portability, status, tag, xKind, q, limit, offset } = req.query;
50
+
51
+ if (type) nodes = nodes.filter(n => n.type === type);
52
+ if (status) nodes = nodes.filter(n => n.status === status);
53
+ if (xKind) nodes = nodes.filter(n => n.x_kind === xKind);
54
+ if (tag) nodes = nodes.filter(n => n.tags && n.tags.includes(tag));
55
+
56
+ // Naive resolution for portability if requested
57
+ // (Assuming vault-cache nodes already have a 'portability' property or we extract it)
58
+ if (portability) {
59
+ nodes = nodes.filter(n => {
60
+ // Some frontmatter might define x_portability directly
61
+ if (n.x_portability) return n.x_portability === portability;
62
+ // Hardcoded defaults per SSSS core:
63
+ if (['memory', 'rule', 'workflow', 'assistant', 'page', 'theme'].includes(n.type)) {
64
+ return 'structural' === portability;
65
+ }
66
+ return 'tenant_private' === portability;
67
+ });
68
+ }
69
+
70
+ if (q) {
71
+ const qs = q.toLowerCase();
72
+ nodes = nodes.filter(n =>
73
+ (n.title && n.title.toLowerCase().includes(qs)) ||
74
+ (n.slug && n.slug.toLowerCase().includes(qs))
75
+ );
76
+ }
77
+
78
+ const total = nodes.length;
79
+
80
+ // Sort by updated descending
81
+ nodes.sort((a, b) => new Date(b.updated || 0) - new Date(a.updated || 0));
82
+
83
+ const skip = parseInt(offset, 10) || 0;
84
+ const take = parseInt(limit, 10) || 50;
85
+ const page = nodes.slice(skip, skip + take);
86
+
87
+ res.json({
88
+ total,
89
+ docs: page.map(n => ({
90
+ path: n._filePath ? path.relative(vaultDir, n._filePath) : `${n.category || 'uncategorized'}/${n.slug}.md`,
91
+ type: n.type || 'memory',
92
+ portability: n.x_portability || (['memory', 'rule', 'workflow', 'assistant', 'page', 'theme'].includes(n.type) ? 'structural' : 'tenant_private'),
93
+ name: n.title || n.slug,
94
+ status: n.status,
95
+ tags: n.tags || [],
96
+ updatedAt: n.updated,
97
+ frontmatter: { ...n, body: undefined, _filePath: undefined }
98
+ }))
99
+ });
100
+ } catch (err) { serverError(res, err); }
101
+ });
102
+
103
+ /**
104
+ * GET /api/docs/read
105
+ * Read a single document by path
106
+ */
107
+ router.get('/api/docs/read', requireAuth, requireScope('ssss:read'), (req, res) => {
108
+ try {
109
+ const vaultDir = resolveVaultFromQuery(req);
110
+ const relPath = req.query.path;
111
+ if (!relPath || !isSafePath(relPath)) return badRequest(res, 'Invalid path');
112
+
113
+ const absPath = path.join(vaultDir, relPath);
114
+ if (!fs.existsSync(absPath)) return notFound(res, 'Document not found');
115
+
116
+ const raw = fs.readFileSync(absPath, 'utf8');
117
+ const parsed = matter(raw);
118
+
119
+ res.json({
120
+ path: relPath,
121
+ raw,
122
+ frontmatter: parsed.data,
123
+ body: parsed.content
124
+ });
125
+ } catch (err) { serverError(res, err); }
126
+ });
127
+
128
+ /**
129
+ * POST /api/docs
130
+ * Create a new document
131
+ */
132
+ router.post('/api/docs', requireAuth, requireScope('ssss:write'), async (req, res) => {
133
+ try {
134
+ const vaultDir = resolveVaultFromQuery(req);
135
+ const { path: relPath, content } = req.body;
136
+
137
+ if (!relPath || !isSafePath(relPath)) return badRequest(res, 'Invalid path');
138
+ const absPath = path.join(vaultDir, relPath);
139
+ if (fs.existsSync(absPath)) return res.status(409).json({ error: 'Document already exists' });
140
+
141
+ const envelope = {
142
+ type: 'operation',
143
+ idempotency_key: crypto.randomUUID(),
144
+ path: relPath,
145
+ workspace_id: 'default',
146
+ content
147
+ };
148
+
149
+ const result = await processOperationAsync(envelope, vaultDir, { agentRole: 'admin' });
150
+ if (!result.success) {
151
+ return res.status(400).json({ error: 'Validation failed', details: result });
152
+ }
153
+ invalidate(vaultDir);
154
+ res.json({ success: true, result });
155
+ } catch (err) { serverError(res, err); }
156
+ });
157
+
158
+ /**
159
+ * PUT /api/docs
160
+ * Update an existing document
161
+ */
162
+ router.put('/api/docs', requireAuth, requireScope('ssss:write'), async (req, res) => {
163
+ try {
164
+ const vaultDir = resolveVaultFromQuery(req);
165
+ const { path: relPath, content } = req.body;
166
+
167
+ if (!relPath || !isSafePath(relPath)) return badRequest(res, 'Invalid path');
168
+ const absPath = path.join(vaultDir, relPath);
169
+ if (!fs.existsSync(absPath)) return notFound(res, 'Document not found');
170
+
171
+ const envelope = {
172
+ type: 'operation',
173
+ idempotency_key: crypto.randomUUID(),
174
+ path: relPath,
175
+ workspace_id: 'default',
176
+ content
177
+ };
178
+
179
+ const result = await processOperationAsync(envelope, vaultDir, { agentRole: 'admin' });
180
+ if (!result.success) {
181
+ return res.status(400).json({ error: 'Validation failed', details: result });
182
+ }
183
+ invalidate(vaultDir);
184
+ res.json({ success: true, result });
185
+ } catch (err) { serverError(res, err); }
186
+ });
187
+
188
+ /**
189
+ * DELETE /api/docs
190
+ * Delete a document
191
+ */
192
+ router.delete('/api/docs', requireAuth, requireScope('ssss:write'), async (req, res) => {
193
+ try {
194
+ const vaultDir = resolveVaultFromQuery(req);
195
+ const relPath = req.query.path;
196
+
197
+ if (!relPath || !isSafePath(relPath)) return badRequest(res, 'Invalid path');
198
+ const absPath = path.join(vaultDir, relPath);
199
+ if (!fs.existsSync(absPath)) return notFound(res, 'Document not found');
200
+
201
+ const envelope = {
202
+ type: 'delete',
203
+ idempotency_key: crypto.randomUUID(),
204
+ path: relPath,
205
+ workspace_id: 'default'
206
+ };
207
+
208
+ const result = await processOperationAsync(envelope, vaultDir, { agentRole: 'admin' });
209
+ if (!result.success) {
210
+ return res.status(400).json({ error: 'Validation failed', details: result });
211
+ }
212
+ invalidate(vaultDir);
213
+ res.json({ success: true, result });
214
+ } catch (err) { serverError(res, err); }
215
+ });
216
+
217
+ // Saved Views
218
+ const VIEWS_FILE = path.join(os.homedir(), '.agent', 'config', 'saved-views.json');
219
+
220
+ function getViews() {
221
+ if (!fs.existsSync(VIEWS_FILE)) return [];
222
+ try { return JSON.parse(fs.readFileSync(VIEWS_FILE, 'utf8')); } catch { return []; }
223
+ }
224
+
225
+ function saveViews(views) {
226
+ if (!fs.existsSync(path.dirname(VIEWS_FILE))) fs.mkdirSync(path.dirname(VIEWS_FILE), { recursive: true });
227
+ // Atomic write
228
+ const temp = VIEWS_FILE + '.tmp';
229
+ fs.writeFileSync(temp, JSON.stringify(views, null, 2), 'utf8');
230
+ fs.renameSync(temp, VIEWS_FILE);
231
+ }
232
+
233
+ router.get('/api/views', requireAuth, (req, res) => {
234
+ res.json({ views: getViews() });
235
+ });
236
+
237
+ router.post('/api/views', requireAuth, (req, res) => {
238
+ const views = getViews();
239
+ const newView = {
240
+ id: crypto.randomUUID(),
241
+ name: req.body.name || 'Untitled View',
242
+ brain: req.body.brain || 'global',
243
+ filters: req.body.filters || {},
244
+ sort: req.body.sort || {},
245
+ columns: req.body.columns || [],
246
+ createdAt: new Date().toISOString()
247
+ };
248
+ views.push(newView);
249
+ saveViews(views);
250
+ res.json({ view: newView });
251
+ });
252
+
253
+ router.delete('/api/views/:id', requireAuth, (req, res) => {
254
+ let views = getViews();
255
+ views = views.filter(v => v.id !== req.params.id);
256
+ saveViews(views);
257
+ res.json({ success: true });
258
+ });
259
+
260
+ export { router as docsRouter };