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
@@ -1,85 +1,158 @@
1
1
  /**
2
- * src/core/validated-write.mjs
3
- *
4
- * Validated write adapter — routes memory mutations through the SSSS §6
5
- * Operation Contract (processOperation) instead of bypassing it via raw
6
- * writeNode(). This ensures every write gets:
7
- * - Envelope validation (Stage 1)
8
- * - Idempotency deduplication (Stage 2)
9
- * - Authorization checks (Stage 3)
10
- * - Lease protection (Stage 4)
11
- * - Schema validation (Stage 5)
12
- * - Atomic commit (Stage 6)
13
- * - Audit trail (Stage 7)
2
+ * Validated write adapter — all vault node mutations go through the SSSS 0.9
3
+ * package kernel (processOperationAsync).
14
4
  *
15
5
  * Usage:
16
- * import { writeNodeValidated } from './validated-write.mjs';
17
- * const result = writeNodeValidated(node, vaultDir);
6
+ * const result = await writeNodeValidatedAsync(node, vaultDir);
18
7
  * if (!result.success) throw new Error(result.validation.errors.join('; '));
19
8
  */
20
9
 
21
10
  import crypto from 'node:crypto';
22
- import path from 'node:path';
23
- import matter from 'gray-matter';
24
- import { processOperation } from './operation-validator.mjs';
11
+ import { processOperationAsync } from './operation-validator.mjs';
25
12
  import { safeStringify } from './vault.mjs';
26
13
  import { invalidate } from './vault-cache.mjs';
27
14
 
15
+ const SAFE_NAME = /^[a-zA-Z0-9_-]+$/;
16
+
28
17
  /**
29
- * Write a memory node through the full §6 Operation Contract pipeline.
30
- *
31
- * @param {object} node - The memory node object (slug, category, title, body, etc.)
32
- * @param {string} vaultDir - Absolute path to the vault directory
33
- * @param {object} [options]
34
- * @param {string} [options.agentRole='admin'] - Agent authorization role
35
- * @param {string} [options.workspaceId='default'] - Workspace scope
36
- * @param {string} [options.idempotencyKey] - Idempotency key (auto-generated if omitted)
37
- * @param {string} [options.leaseStore] - Path to lease store directory
38
- * @param {string} [options.eventLogDir] - Path to event log directory
39
- * @param {boolean} [options.dryRun=false] - Validate without committing
40
- * @returns {object} Operation response (§6.4)
18
+ * Normalize a vault node so package universal frontmatter + TR memory v2 pass.
41
19
  */
42
- export function writeNodeValidated(node, vaultDir, options = {}) {
20
+ export function prepareNodeForContract(node = {}) {
21
+ const prepared = { ...node };
22
+ if (!prepared.type) prepared.type = 'memory';
23
+
24
+ // Identity fallbacks
25
+ if (!prepared.slug) {
26
+ prepared.slug = prepared.proposal_id || prepared.migration_id || prepared.release_id || 'unnamed';
27
+ }
28
+ // Proposal "category" is a topic label in TR; vault folder is always proposals/.
29
+ if (prepared.type === 'proposal') {
30
+ if (prepared.category && prepared.category !== 'proposals') {
31
+ prepared.proposal_topic = prepared.proposal_topic || prepared.category;
32
+ }
33
+ prepared.category = 'proposals';
34
+ } else if (!prepared.category) {
35
+ if (prepared.type === 'migration') prepared.category = 'migrations';
36
+ else if (prepared.type === 'schema-proposal') prepared.category = 'schema-proposals';
37
+ else prepared.category = 'uncategorized';
38
+ }
39
+
40
+ // Package 0.9 universal fields
41
+ if (!prepared.title) {
42
+ prepared.title = prepared.name || prepared.summary || prepared.slug || 'Untitled';
43
+ }
44
+ if (!prepared.description) {
45
+ prepared.description = prepared.rationale || prepared.summary || prepared.title;
46
+ }
47
+ if (!prepared.timestamp) {
48
+ prepared.timestamp = prepared.updated || prepared.created || prepared.proposed_at
49
+ || prepared.applied_at || new Date().toISOString();
50
+ }
51
+
52
+ if (prepared.type === 'memory') {
53
+ if (!prepared.schema_version) prepared.schema_version = 2;
54
+ if (!prepared.status) prepared.status = 'active';
55
+ if (!prepared.updated) prepared.updated = new Date().toISOString();
56
+ if (!prepared.last_accessed) prepared.last_accessed = new Date().toISOString();
57
+ if (!prepared.created) prepared.created = prepared.updated;
58
+ // Package core enum for memory.category is closed; remap host-only folders.
59
+ const MEMORY_CATEGORIES = new Set([
60
+ 'invariants', 'patterns', 'anti-patterns', 'preferences',
61
+ 'decisions', 'concepts', 'facts', 'lore',
62
+ ]);
63
+ if (prepared.category && !MEMORY_CATEGORIES.has(prepared.category)) {
64
+ const original = prepared.category;
65
+ prepared.tags = Array.isArray(prepared.tags) ? [...prepared.tags] : [];
66
+ if (!prepared.tags.includes(`folder:${original}`)) prepared.tags.push(`folder:${original}`);
67
+ prepared.category = original === 'instructions' ? 'preferences' : 'facts';
68
+ }
69
+ // Fill schema v2 required fields so partial updates (graph linking) still validate.
70
+ if (prepared.schema_version === 2) {
71
+ if (prepared.confidence === undefined || prepared.confidence === null) prepared.confidence = 0.5;
72
+ if (prepared.importance === undefined || prepared.importance === null) prepared.importance = 3;
73
+ if (!prepared.modality) prepared.modality = 'descriptive';
74
+ if (!prepared.subject) prepared.subject = 'agent';
75
+ if (!prepared.predicate) prepared.predicate = 'know';
76
+ if (!prepared.object) prepared.object = prepared.slug || 'fact';
77
+ if (!prepared.sentiment_polarity) prepared.sentiment_polarity = 'descriptive';
78
+ if (!prepared.sentiment_target) prepared.sentiment_target = prepared.slug || 'memory';
79
+ }
80
+ const now = new Date().toISOString();
81
+ if (!prepared.x_temporal_context) {
82
+ prepared.x_temporal_context = prepared.updated || prepared.created || now;
83
+ }
84
+ if (!prepared.x_citations) {
85
+ prepared.x_citations = [{
86
+ source: prepared.source?.type || 'unknown',
87
+ title: prepared.title || 'Untitled Memory',
88
+ url: prepared.source?.session_id ? `session://${prepared.source.session_id}` : 'unknown',
89
+ published: prepared.x_temporal_context,
90
+ relevance: 1.0,
91
+ accessed: now,
92
+ }];
93
+ }
94
+ }
95
+
96
+ return prepared;
97
+ }
98
+
99
+ function buildEnvelope(node, options = {}) {
43
100
  const {
44
- agentRole = 'admin',
45
101
  workspaceId = 'default',
46
102
  idempotencyKey,
47
- leaseStore,
48
- eventLogDir,
49
103
  dryRun = false,
104
+ path: pathOverride,
50
105
  } = options;
51
106
 
52
- // Build the Markdown content from the node object
53
- const { body, _filePath, ...frontmatter } = node;
54
- if (!frontmatter.type) frontmatter.type = 'memory';
55
- if (!frontmatter.schema_version) frontmatter.schema_version = 2;
56
- if (!frontmatter.updated) frontmatter.updated = new Date().toISOString();
57
- if (!frontmatter.last_accessed) frontmatter.last_accessed = new Date().toISOString();
107
+ const prepared = prepareNodeForContract(node);
108
+ const { body, _filePath, _layer, _filepath, ...frontmatter } = prepared;
58
109
 
59
- const content = safeStringify(body || '', frontmatter);
110
+ if (!SAFE_NAME.test(String(frontmatter.slug))) {
111
+ throw new Error(`Invalid slug: ${frontmatter.slug}`);
112
+ }
113
+ if (!SAFE_NAME.test(String(frontmatter.category))) {
114
+ throw new Error(`Invalid category: ${frontmatter.category}`);
115
+ }
60
116
 
61
- // Build the §6.1 envelope
62
- const category = frontmatter.category || 'uncategorized';
63
- const slug = frontmatter.slug || 'unnamed';
64
- const vfsPath = `${category}/${slug}.md`;
117
+ const content = safeStringify(body || '', frontmatter);
118
+ const vfsPath = pathOverride || `${frontmatter.category}/${frontmatter.slug}.md`;
65
119
 
66
- const envelope = {
120
+ return {
67
121
  type: 'operation',
68
122
  idempotency_key: idempotencyKey || crypto.randomUUID(),
69
123
  path: vfsPath,
70
124
  workspace_id: workspaceId,
71
125
  content,
72
- dry_run: dryRun,
126
+ dry_run: !!dryRun,
127
+ actor: { role: options.agentRole || 'system' },
73
128
  };
129
+ }
130
+
131
+ /**
132
+ * @deprecated Use {@link writeNodeValidatedAsync}.
133
+ */
134
+ export function writeNodeValidated(node, vaultDir, options = {}) {
135
+ return writeNodeValidatedAsync(node, vaultDir, options);
136
+ }
74
137
 
75
- // Run the full §6.3 pipeline
76
- const result = processOperation(envelope, vaultDir, {
138
+ /**
139
+ * Write a node through the SSSS 0.9 package kernel.
140
+ */
141
+ export async function writeNodeValidatedAsync(node, vaultDir, options = {}) {
142
+ const {
143
+ agentRole = 'system',
144
+ leaseStore,
145
+ eventLogDir,
146
+ dryRun = false,
147
+ } = options;
148
+
149
+ const envelope = buildEnvelope(node, { ...options, agentRole });
150
+ const result = await processOperationAsync(envelope, vaultDir, {
77
151
  agentRole,
78
152
  leaseStore,
79
153
  eventLogDir,
80
154
  });
81
155
 
82
- // Invalidate vault cache on successful commit
83
156
  if (result.success && !dryRun) {
84
157
  invalidate(vaultDir);
85
158
  }
@@ -88,13 +161,8 @@ export function writeNodeValidated(node, vaultDir, options = {}) {
88
161
  }
89
162
 
90
163
  /**
91
- * Validate a node against the §6 pipeline without committing.
92
- * Useful for pre-flight checks on user input.
93
- *
94
- * @param {object} node - The memory node object
95
- * @param {string} vaultDir - Absolute path to the vault directory
96
- * @returns {object} Validation result with errors/warnings
164
+ * Validate a node without committing.
97
165
  */
98
166
  export function validateNode(node, vaultDir) {
99
- return writeNodeValidated(node, vaultDir, { dryRun: true });
167
+ return writeNodeValidatedAsync(node, vaultDir, { dryRun: true });
100
168
  }
@@ -3,6 +3,8 @@ import path from 'path';
3
3
  import matter from 'gray-matter';
4
4
  import { logger } from './logger.mjs';
5
5
 
6
+ const SAFE_NAME = /^[a-zA-Z0-9_-]+$/;
7
+
6
8
  /**
7
9
  * Atomic file write using write-then-rename to prevent partial corruption.
8
10
  */
@@ -113,42 +115,27 @@ export function loadMergedNodes(globalVaultDir, projectVaultDir) {
113
115
 
114
116
 
115
117
  /**
116
- * Write a memory node to the vault.
118
+ * Write a vault node through the SSSS 0.9 Operation Contract (package kernel).
119
+ * Always async — callers must await.
120
+ *
121
+ * @param {object} node
122
+ * @param {string} vaultDir - Vault root (or scoped root such as proposals/)
123
+ * @param {object} [options]
124
+ * @returns {Promise<object>} Operation response
117
125
  */
118
- const SAFE_NAME = /^[a-zA-Z0-9_-]+$/;
119
-
120
- export function writeNode(node, vaultDir) {
121
- if (!SAFE_NAME.test(node.slug)) throw new Error(`Invalid slug: ${node.slug}`);
122
- if (!SAFE_NAME.test(node.category)) throw new Error(`Invalid category: ${node.category}`);
123
- const targetDir = path.join(vaultDir, node.category);
124
- const filePath = path.join(targetDir, `${node.slug}.md`);
125
-
126
- const dirPath = path.dirname(filePath);
127
- if (!fs.existsSync(dirPath)) {
128
- fs.mkdirSync(dirPath, { recursive: true });
126
+ export async function writeNode(node, vaultDir, options = {}) {
127
+ // Dynamic import avoids circular dependency with validated-write → vault.safeStringify
128
+ const { writeNodeValidatedAsync } = await import('./validated-write.mjs');
129
+ const result = await writeNodeValidatedAsync(node, vaultDir, {
130
+ agentRole: options.agentRole || 'system',
131
+ workspaceId: options.workspaceId || 'default',
132
+ ...options,
133
+ });
134
+ if (!result.success) {
135
+ const errors = result.validation?.errors || [result.error || 'unknown validation failure'];
136
+ throw new Error(`writeNode contract failure: ${errors.join('; ')}`);
129
137
  }
130
-
131
- const { body, ...frontmatter } = node;
132
-
133
- if (frontmatter.type === 'memory') {
134
- const now = new Date().toISOString();
135
- if (!frontmatter.x_temporal_context) {
136
- frontmatter.x_temporal_context = frontmatter.updated || frontmatter.created || now;
137
- }
138
- if (!frontmatter.x_citations) {
139
- frontmatter.x_citations = [{
140
- source: frontmatter.source?.type || 'unknown',
141
- title: frontmatter.title || 'Untitled Memory',
142
- url: frontmatter.source?.session_id ? `session://${frontmatter.source.session_id}` : 'unknown',
143
- published: frontmatter.x_temporal_context,
144
- relevance: 1.0,
145
- accessed: now
146
- }];
147
- }
148
- }
149
-
150
- const raw = safeStringify(body || '', frontmatter);
151
- atomicWrite(filePath, raw);
138
+ return result;
152
139
  }
153
140
 
154
141
  /**
@@ -0,0 +1,272 @@
1
+ /**
2
+ * WebAuthn / passkey credential store for Total Recall dashboard.
3
+ *
4
+ * Credentials live under <brain>/config/webauthn.json (mode 0600).
5
+ * Challenges are in-memory only (short TTL).
6
+ * Never store secret values here — only authenticator public keys.
7
+ */
8
+
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import crypto from 'node:crypto';
12
+ import {
13
+ generateRegistrationOptions,
14
+ verifyRegistrationResponse,
15
+ generateAuthenticationOptions,
16
+ verifyAuthenticationResponse,
17
+ } from '@simplewebauthn/server';
18
+
19
+ const CHALLENGE_TTL_MS = 5 * 60 * 1000;
20
+ /** @type {Map<string, { challenge: string, type: string, exp: number }>} */
21
+ const challenges = new Map();
22
+
23
+ const DASHBOARD_USER = {
24
+ id: Buffer.from('total-recall-dashboard-owner'),
25
+ name: 'dashboard',
26
+ displayName: 'Total Recall Owner',
27
+ };
28
+
29
+ export function resolveWebAuthnPath(brainDir) {
30
+ return path.join(brainDir, 'config', 'webauthn.json');
31
+ }
32
+
33
+ function loadStore(brainDir) {
34
+ const filePath = resolveWebAuthnPath(brainDir);
35
+ if (!fs.existsSync(filePath)) {
36
+ return { version: 1, credentials: [] };
37
+ }
38
+ try {
39
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
40
+ if (!data || !Array.isArray(data.credentials)) {
41
+ return { version: 1, credentials: [] };
42
+ }
43
+ return data;
44
+ } catch {
45
+ return { version: 1, credentials: [] };
46
+ }
47
+ }
48
+
49
+ function saveStore(brainDir, store) {
50
+ const filePath = resolveWebAuthnPath(brainDir);
51
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
52
+ fs.writeFileSync(filePath, JSON.stringify(store, null, 2) + '\n', {
53
+ encoding: 'utf8',
54
+ mode: 0o600,
55
+ });
56
+ }
57
+
58
+ export function listPasskeys(brainDir) {
59
+ const store = loadStore(brainDir);
60
+ return store.credentials.map((c) => ({
61
+ id: c.id,
62
+ created_at: c.created_at || null,
63
+ transports: c.transports || [],
64
+ deviceType: c.deviceType || null,
65
+ backedUp: !!c.backedUp,
66
+ label: c.label || 'Passkey',
67
+ }));
68
+ }
69
+
70
+ export function hasPasskeys(brainDir) {
71
+ return loadStore(brainDir).credentials.length > 0;
72
+ }
73
+
74
+ function putChallenge(key, challenge, type) {
75
+ challenges.set(key, { challenge, type, exp: Date.now() + CHALLENGE_TTL_MS });
76
+ }
77
+
78
+ function takeChallenge(key, type) {
79
+ const row = challenges.get(key);
80
+ challenges.delete(key);
81
+ if (!row) return null;
82
+ if (row.exp < Date.now()) return null;
83
+ if (type && row.type !== type) return null;
84
+ return row.challenge;
85
+ }
86
+
87
+ /**
88
+ * Derive rpID + origin from the request (localhost-safe).
89
+ */
90
+ export function resolveRpFromRequest(req) {
91
+ const rawHost = String(req.get('x-forwarded-host') || req.get('host') || 'localhost:3000');
92
+ const host = rawHost.split(',')[0].trim();
93
+ let hostname = host.split(':')[0] || 'localhost';
94
+ // WebAuthn: 127.0.0.1 is awkward; prefer localhost
95
+ if (hostname === '127.0.0.1') hostname = 'localhost';
96
+
97
+ const protoHeader = req.get('x-forwarded-proto');
98
+ const proto =
99
+ protoHeader?.split(',')[0]?.trim() ||
100
+ (req.secure || process.env.NODE_ENV === 'production' ? 'https' : 'http');
101
+
102
+ // Rebuild origin; map 127.0.0.1 → localhost for WebAuthn origin match
103
+ let originHost = host.replace(/^127\.0\.0\.1/, 'localhost');
104
+ const origin = `${proto}://${originHost}`;
105
+
106
+ return {
107
+ rpID: hostname,
108
+ rpName: 'Total Recall',
109
+ origin,
110
+ };
111
+ }
112
+
113
+ function challengeKey(req, kind) {
114
+ const sid = req.auth?.type || 'anon';
115
+ return `${kind}:${sid}:${req.ip || 'local'}`;
116
+ }
117
+
118
+ /**
119
+ * @param {string} brainDir
120
+ * @param {import('express').Request} req
121
+ */
122
+ export async function beginRegistration(brainDir, req) {
123
+ const { rpID, rpName } = resolveRpFromRequest(req);
124
+ const store = loadStore(brainDir);
125
+ const options = await generateRegistrationOptions({
126
+ rpName,
127
+ rpID,
128
+ userName: DASHBOARD_USER.name,
129
+ userDisplayName: DASHBOARD_USER.displayName,
130
+ userID: DASHBOARD_USER.id,
131
+ attestationType: 'none',
132
+ excludeCredentials: store.credentials.map((c) => ({
133
+ id: c.id,
134
+ transports: c.transports,
135
+ })),
136
+ authenticatorSelection: {
137
+ residentKey: 'preferred',
138
+ userVerification: 'preferred',
139
+ authenticatorAttachment: 'platform',
140
+ },
141
+ });
142
+ putChallenge(challengeKey(req, 'reg'), options.challenge, 'reg');
143
+ return options;
144
+ }
145
+
146
+ /**
147
+ * @param {string} brainDir
148
+ * @param {import('express').Request} req
149
+ * @param {object} response - browser registration response JSON
150
+ * @param {{ label?: string }} opts
151
+ */
152
+ export async function finishRegistration(brainDir, req, response, opts = {}) {
153
+ const expectedChallenge = takeChallenge(challengeKey(req, 'reg'), 'reg');
154
+ if (!expectedChallenge) {
155
+ throw new Error('Registration challenge expired or missing. Try again.');
156
+ }
157
+ const { rpID, origin } = resolveRpFromRequest(req);
158
+ const verification = await verifyRegistrationResponse({
159
+ response,
160
+ expectedChallenge,
161
+ expectedOrigin: origin,
162
+ expectedRPID: rpID,
163
+ requireUserVerification: false,
164
+ });
165
+ if (!verification.verified || !verification.registrationInfo) {
166
+ throw new Error('Passkey registration failed verification');
167
+ }
168
+
169
+ const { credential, credentialDeviceType, credentialBackedUp } = verification.registrationInfo;
170
+ const store = loadStore(brainDir);
171
+ const id = credential.id;
172
+ // Replace if same id re-registered
173
+ store.credentials = store.credentials.filter((c) => c.id !== id);
174
+ store.credentials.push({
175
+ id,
176
+ publicKey: Buffer.from(credential.publicKey).toString('base64url'),
177
+ counter: credential.counter ?? 0,
178
+ transports: response.response?.transports || credential.transports || [],
179
+ deviceType: credentialDeviceType,
180
+ backedUp: !!credentialBackedUp,
181
+ label: opts.label || 'Passkey',
182
+ created_at: new Date().toISOString(),
183
+ });
184
+ saveStore(brainDir, store);
185
+ return { id, verified: true };
186
+ }
187
+
188
+ /**
189
+ * @param {string} brainDir
190
+ * @param {import('express').Request} req
191
+ */
192
+ export async function beginAuthentication(brainDir, req) {
193
+ const store = loadStore(brainDir);
194
+ if (!store.credentials.length) {
195
+ throw new Error('No passkeys registered. Register a passkey first.');
196
+ }
197
+ const { rpID } = resolveRpFromRequest(req);
198
+ const options = await generateAuthenticationOptions({
199
+ rpID,
200
+ userVerification: 'preferred',
201
+ allowCredentials: store.credentials.map((c) => ({
202
+ id: c.id,
203
+ transports: c.transports,
204
+ })),
205
+ });
206
+ putChallenge(challengeKey(req, 'auth'), options.challenge, 'auth');
207
+ return options;
208
+ }
209
+
210
+ /**
211
+ * @param {string} brainDir
212
+ * @param {import('express').Request} req
213
+ * @param {object} response
214
+ */
215
+ export async function finishAuthentication(brainDir, req, response) {
216
+ const expectedChallenge = takeChallenge(challengeKey(req, 'auth'), 'auth');
217
+ if (!expectedChallenge) {
218
+ throw new Error('Authentication challenge expired or missing. Try again.');
219
+ }
220
+ const store = loadStore(brainDir);
221
+ const credId = response?.id;
222
+ const stored = store.credentials.find((c) => c.id === credId);
223
+ if (!stored) {
224
+ throw new Error('Unknown passkey credential');
225
+ }
226
+ const { rpID, origin } = resolveRpFromRequest(req);
227
+
228
+ const verification = await verifyAuthenticationResponse({
229
+ response,
230
+ expectedChallenge,
231
+ expectedOrigin: origin,
232
+ expectedRPID: rpID,
233
+ credential: {
234
+ id: stored.id,
235
+ publicKey: Buffer.from(stored.publicKey, 'base64url'),
236
+ counter: stored.counter || 0,
237
+ transports: stored.transports,
238
+ },
239
+ requireUserVerification: false,
240
+ });
241
+
242
+ if (!verification.verified) {
243
+ throw new Error('Passkey authentication failed');
244
+ }
245
+
246
+ const newCounter = verification.authenticationInfo?.newCounter;
247
+ if (typeof newCounter === 'number') {
248
+ stored.counter = newCounter;
249
+ saveStore(brainDir, store);
250
+ }
251
+
252
+ return {
253
+ verified: true,
254
+ credentialId: stored.id,
255
+ };
256
+ }
257
+
258
+ export function deletePasskey(brainDir, credentialId) {
259
+ const store = loadStore(brainDir);
260
+ const before = store.credentials.length;
261
+ store.credentials = store.credentials.filter((c) => c.id !== credentialId);
262
+ if (store.credentials.length === before) {
263
+ return { found: false };
264
+ }
265
+ saveStore(brainDir, store);
266
+ return { found: true, deleted: true };
267
+ }
268
+
269
+ /** Test helper */
270
+ export function _clearChallengesForTests() {
271
+ challenges.clear();
272
+ }
@@ -176,7 +176,12 @@ function resolveRequestedModel(requestedModel, runtimeConfig) {
176
176
  }
177
177
 
178
178
  function ssssReferenceDir() {
179
- return path.join(SKILLS_DIR, 'total-recall', 'skills', 'tr-ssss', 'references');
179
+ const candidates = [
180
+ path.join(SKILLS_DIR, 'total-recall', 'references'),
181
+ path.join(SKILLS_DIR, 'total-recall', 'modules', 'ssss', 'references'),
182
+ path.join(SKILLS_DIR, 'total-recall', 'skills', 'tr-ssss', 'references'),
183
+ ];
184
+ return candidates.find((p) => fs.existsSync(p)) || candidates[0];
180
185
  }
181
186
 
182
187
  function listSsssReferences(req) {
@@ -401,7 +406,7 @@ apiRouter.post('/v1/chat/completions', requireScope('chat:write'), async (req, r
401
406
 
402
407
 
403
408
  const dateStr = new Date().toLocaleString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', timeZoneName: 'short' });
404
- let baseSystemPrompt = `You are Total Recall, a Sovereign AI OS running a database-free, Markdown-first Structured Semantic Syntax System (SSSS) architecture. The current date and time is ${dateStr}.
409
+ let baseSystemPrompt = `You are Total Recall — portable personal memory for any IDE. Your brain is a database-free, Markdown-first Structured Semantic Syntax System (SSSS) vault on disk. Core loop: write (remember/session) → dream (consolidate) → read (recall + compiled surfaces) → optional daemon tasks. The current date and time is ${dateStr}.
405
410
  CRITICAL RULES — you have these tools and MUST use them:
406
411
 
407
412
  BROWSER TOOLS (headless Chromium — use for web tasks):
@@ -510,10 +515,14 @@ You have a REAL browser AND full desktop control. Use them. Navigate, click, typ
510
515
  baseSystemPrompt += `\n\n=== TIER 1 HOT MEMORY INSTRUCTIONS ===\n${compressInstructions(instructions)}`;
511
516
  }
512
517
 
513
- const ssssPath = path.join(AGENT_DIR, 'skills', 'total-recall', 'skills', 'tr-ssss', 'SKILL.md');
514
- if (fs.existsSync(ssssPath)) {
518
+ const ssssCandidates = [
519
+ path.join(AGENT_DIR, 'skills', 'total-recall', 'references', 'ssss-reference.md'),
520
+ path.join(AGENT_DIR, 'skills', 'total-recall', 'SKILL.md'),
521
+ ];
522
+ const ssssPath = ssssCandidates.find((p) => fs.existsSync(p));
523
+ if (ssssPath) {
515
524
  const ssssContent = fs.readFileSync(ssssPath, 'utf8');
516
- baseSystemPrompt += `\n\n=== STRUCTURED SEMANTIC SYNTAX SYSTEM (SSSS) DOCUMENTATION ===\nYou are the orchestrator of this system. Here is the architectural specification:\n${compressSsssSkill(ssssContent)}`;
525
+ baseSystemPrompt += `\n\n=== STRUCTURED SEMANTIC SYNTAX SYSTEM (SSSS) DOCUMENTATION ===\nPrefer @ssss/cli for mutations. Compact reference:\n${compressSsssSkill(ssssContent)}`;
517
526
  }
518
527
 
519
528
  // Load user profile — drives all personalisation and idle work
@@ -580,13 +589,14 @@ Use execute_code to call these endpoints with node's built-in fetch() — you ar
580
589
  // No profile yet — enter interview mode
581
590
  const interviewTask = fs.readFileSync(interviewTaskPath, 'utf8');
582
591
  baseSystemPrompt += `\n\n=== ONBOARDING INTERVIEW MODE ===
583
- This user has not been onboarded yet. You MUST conduct the onboarding interview before doing anything else.
584
- DO NOT answer any other questions until the interview is complete.
585
- Ask questions one at a time, warmly and conversationally. Listen carefully. Reflect back what you hear.
586
- After the interview, write the user's answers to:
592
+ This user has no user-profile yet. Conduct the portable-memory onboarding interview before unrelated work.
593
+ Total Recall is personal memory for any IDE (write → dream → read → optional tasks) — not a vendor OS takeover.
594
+ Ask questions one at a time, warmly and conversationally. Reflect each answer before the next.
595
+ After the interview, write answers to:
587
596
  - ${BRAIN_DIR}/memory-vault/preferences/user-profile.md
588
597
  - ${BRAIN_DIR}/memory-vault/preferences/user-priorities.md
589
598
  - ${BRAIN_DIR}/memory-vault/preferences/user-interests.md
599
+ Prefer CLI when available: npx total-recall remember preference "..." --tags user,profile,onboarding
590
600
  Then mark ${interviewTaskPath} as status: done.
591
601
 
592
602
  INTERVIEW TASK:
@@ -643,7 +653,7 @@ ${interviewTask}`;
643
653
  // Overwrite system prompt to protect the CPU/memory footprint of the fallback model
644
654
  const systemMsgIdx = currentMessages.findIndex(m => m.role === 'system');
645
655
  if (systemMsgIdx !== -1) {
646
- currentMessages[systemMsgIdx].content = `You are Total Recall, a Sovereign AI OS running a database-free, Markdown-first Structured Semantic Syntax System (SSSS) architecture. The current date and time is ${dateStr}.
656
+ currentMessages[systemMsgIdx].content = `You are Total Recall — portable personal memory (Markdown vault on disk). The current date and time is ${dateStr}.
647
657
  You are operating in a lightweight text-only mode. Answer the user's questions directly, concisely, and helpfully without using tools. Keep your responses short and complete.`;
648
658
  }
649
659
 
@@ -913,20 +923,20 @@ apiRouter.get('/v1/chat/suggestions', requireAuth, requireScope('chat:read'), (r
913
923
  const standardSuggestions = [
914
924
  {
915
925
  type: 'fact',
916
- title: '🧠 Sovereign OS Operations',
917
- text: 'Let\'s review the core architecture of your Sovereign OS and see how we can make memory vaults even more powerful.',
926
+ title: '🧠 Memory loop',
927
+ text: 'Walk the portable memory loop: remember → dream → recall, and connect an IDE so surfaces stay in sync.',
918
928
  nodes: []
919
929
  },
920
930
  {
921
931
  type: 'concept',
922
- title: '💡 Custom Agent Skills',
923
- text: 'What kind of specialized automation skills should we design next for developer productivity?',
932
+ title: '💡 Skills across any repo',
933
+ text: 'What skills should we track or deploy next? Hosts are equal — skill track any path via the registry.',
924
934
  nodes: []
925
935
  },
926
936
  {
927
937
  type: 'question',
928
- title: '❓ Brain Health & Scaling',
929
- text: 'Would you like to analyze system health indexes and run a memory compilation to ensure all search indexes are fully synchronized?',
938
+ title: '❓ Vault health',
939
+ text: 'Want a vault compile / dream pass so search indexes and instruction surfaces stay synchronized?',
930
940
  nodes: []
931
941
  }
932
942
  ];