total-recall-brain 3.13.2 → 3.14.3

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 (63) hide show
  1. package/bin/total-recall.mjs +3 -0
  2. package/frontend/src/App.tsx +8 -7
  3. package/frontend/src/api.ts +25 -0
  4. package/frontend/src/components/UsageChart.tsx +1 -1
  5. package/frontend/src/pages/ApiKeysPage.tsx +118 -42
  6. package/frontend/src/pages/ChatPage.tsx +1 -1
  7. package/frontend/src/pages/DesignDocsPage.tsx +28 -21
  8. package/frontend/src/pages/FilesPage.tsx +6 -1
  9. package/frontend/src/pages/GraphPage.tsx +5 -1
  10. package/frontend/src/pages/HealthPage.tsx +1 -1
  11. package/frontend/src/pages/InboxPage.tsx +2 -1
  12. package/frontend/src/pages/InstructionsPage.tsx +2 -2
  13. package/frontend/src/pages/IntegrationsPage.tsx +11 -5
  14. package/frontend/src/pages/MemoryPage.tsx +27 -118
  15. package/frontend/src/pages/ModelsPage.tsx +2 -50
  16. package/frontend/src/pages/OnboardingPage.tsx +5 -10
  17. package/frontend/src/pages/OpenWikiPage.tsx +1 -0
  18. package/frontend/src/pages/SettingsPage.tsx +136 -131
  19. package/frontend/src/pages/SkillsPage.tsx +102 -46
  20. package/frontend/src/pages/TasksPage.tsx +60 -25
  21. package/frontend/src/pages/UsagePage.tsx +206 -95
  22. package/frontend/src/types.ts +17 -11
  23. package/frontend/src/utils/onboarding.ts +10 -0
  24. package/package.json +3 -2
  25. package/scaffold/.agent/skills/ssss/SKILL.md +235 -0
  26. package/scaffold/.agent/skills/ssss/evals/evals.json +37 -0
  27. package/scaffold/.agent/skills/ssss/references/contract-reference.md +95 -0
  28. package/scaffold/.agent/skills/ssss/scripts/validate-schema.mjs +215 -0
  29. package/scaffold/.agent/skills/ssss/subagents/audit-ssss-contract.md +22 -0
  30. package/scaffold/.agent/skills/total-recall/SKILL.md +1 -0
  31. package/src/cli/backup.mjs +3 -3
  32. package/src/cli/chat.mjs +2 -2
  33. package/src/cli/config.mjs +2 -9
  34. package/src/cli/connect.mjs +1 -1
  35. package/src/cli/deploy-ui.mjs +1 -1
  36. package/src/cli/deploy.mjs +2 -2
  37. package/src/cli/doctor.mjs +1 -1
  38. package/src/cli/help.mjs +1 -1
  39. package/src/cli/init.mjs +19 -1
  40. package/src/cli/key.mjs +150 -0
  41. package/src/cli/secret.mjs +30 -1
  42. package/src/cli/skill.mjs +0 -2
  43. package/src/cli/sync.mjs +19 -0
  44. package/src/cli/upgrade.mjs +1 -1
  45. package/src/cli/wizard.html +5 -5
  46. package/src/core/crons.mjs +11 -0
  47. package/src/core/daemon-loop.mjs +11 -0
  48. package/src/core/import-rules.mjs +4 -3
  49. package/src/core/okf-adapter.mjs +10 -10
  50. package/src/core/runtime.mjs +0 -2
  51. package/src/core/secrets-store.mjs +16 -0
  52. package/src/core/skills-registry.mjs +38 -0
  53. package/src/core/surface.mjs +1 -1
  54. package/src/core/task_runner.mjs +6 -0
  55. package/src/core/usage-tracker.mjs +16 -3
  56. package/src/core/vault-watcher.mjs +92 -0
  57. package/src/core/vault.mjs +0 -2
  58. package/src/server/api.mjs +1 -1
  59. package/src/server/rest.mjs +69 -57
  60. package/src/server/route-manifest.json +8 -0
  61. package/src/server/routes/_shared.mjs +1 -1
  62. package/src/server/routes/secrets.mjs +4 -4
  63. package/src/server/routes/skills.mjs +92 -10
@@ -5,9 +5,6 @@ import { resolveBrainDir, parseLayerFlag } from './agent-dir.mjs';
5
5
 
6
6
  // Pre-defined mapping of known settings
7
7
  const KEY_MAP = {
8
- 'yolo_mode': { file: 'security', path: 'yolo_mode', type: 'boolean' },
9
- 'enforce_local_only': { file: 'security', path: 'privacy.enforce_local_only', type: 'boolean' },
10
- 'allow_frontier_export': { file: 'security', path: 'privacy.allow_frontier_export', type: 'string', values: ['always', 'ask_per_skill', 'never'] },
11
8
  'password_hash': { file: 'security', path: 'dashboard.password_hash', type: 'string' },
12
9
  'session_ttl_hours': { file: 'security', path: 'dashboard.session_ttl_hours', type: 'number' },
13
10
  'force_password_reset': { file: 'security', path: 'dashboard.force_password_reset', type: 'boolean' },
@@ -117,7 +114,7 @@ export default async function configCommand(args) {
117
114
  console.log(`
118
115
  total-recall config [get|set] [key] [value] [options]
119
116
 
120
- Manage Sovereign OS and Dashboard configuration settings directly from the CLI.
117
+ Manage Core OS and Dashboard configuration settings directly from the CLI.
121
118
 
122
119
  Commands:
123
120
  get [key] Retrieve the value of a configuration key. If key is omitted, lists all keys.
@@ -128,8 +125,6 @@ export default async function configCommand(args) {
128
125
  --project Explicitly target the project brain configuration (<cwd>/.agent/)
129
126
 
130
127
  Common Keys:
131
- yolo_mode Fully autonomous mode (true | false)
132
- allow_frontier_export Frontier export policy (always | ask_per_skill | never)
133
128
  session_ttl_hours Dashboard session TTL hours (number)
134
129
  api_requests_per_minute Rate limit for API endpoints (number)
135
130
  daily_cap_usd Cost-control daily cap (number)
@@ -139,8 +134,6 @@ export default async function configCommand(args) {
139
134
 
140
135
  Examples:
141
136
  npx total-recall config get
142
- npx total-recall config get yolo_mode
143
- npx total-recall config set yolo_mode true --project
144
137
  npx total-recall config set daily_cap_usd 10.0 --global
145
138
  npx total-recall config set allowed_origins "http://localhost:8000"
146
139
  `);
@@ -192,7 +185,7 @@ export default async function configCommand(args) {
192
185
  if (args.includes('--json') || args.includes('-j')) {
193
186
  console.log(JSON.stringify(allSettings, null, 2));
194
187
  } else {
195
- console.log(`\n ⚙️ Sovereign UI & Settings Configuration ${layerLabel}\n`);
188
+ console.log(`\n ⚙️ Local UI & Settings Configuration ${layerLabel}\n`);
196
189
  for (const [k, v] of Object.entries(allSettings)) {
197
190
  const formattedValue = typeof v === 'object' && v !== null ? JSON.stringify(v) : String(v);
198
191
  console.log(` • ${k.padEnd(30)}: ${formattedValue}`);
@@ -558,7 +558,7 @@ Perform a POST request to \`${base}/api/memory/search/semantic\` with the body \
558
558
  },
559
559
  'brain.toml': {
560
560
  description: 'Show Total Recall brain status and health',
561
- prompt: `Show the health, loaded model status, and statistics of the local Total Recall Sovereign OS brain.
561
+ prompt: `Show the health, loaded model status, and statistics of the local Total Recall Total Recall System brain.
562
562
 
563
563
  Instructions:
564
564
  Query the health of the local brain by sending a GET request to \`${base}/health\` with Authorization: Bearer ${token}.`
@@ -954,7 +954,7 @@ export function startDeployUI(port = 3001) {
954
954
  body: JSON.stringify({
955
955
  name: 'total-recall-brain',
956
956
  private: true,
957
- description: 'Total Recall Sovereign AI memory backup (encrypted VFS)',
957
+ description: 'Total Recall Total Recall memory backup (encrypted VFS)',
958
958
  has_issues: false,
959
959
  has_projects: false,
960
960
  has_wiki: false
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * total-recall deploy
3
3
  *
4
- * Provision a target machine with the full Sovereign AI stack:
4
+ * Provision a target machine with the full Total Recall stack:
5
5
  * 1. Detect host architecture (aarch64 / x86_64 / arm64)
6
6
  * 2. Install Ollama (if not present)
7
7
  * 3. Pull Gemma 4 26B model (~16GB)
@@ -213,7 +213,7 @@ function parseArgs(args) {
213
213
 
214
214
  function printHelp() {
215
215
  console.log(`
216
- total-recall deploy — Provision a target machine with the Sovereign AI stack
216
+ total-recall deploy — Provision a target machine with the Total Recall stack
217
217
 
218
218
  Usage: total-recall deploy [options]
219
219
 
@@ -64,7 +64,7 @@ export default async function doctor() {
64
64
  console.log(`
65
65
  ┌─────────────────────────────────────────────────────────┐
66
66
  │ Total Recall System Doctor │
67
- │ Pre-flighting sovereign runtime diagnostics... │
67
+ │ Pre-flighting persistent runtime diagnostics... │
68
68
  └─────────────────────────────────────────────────────────┘
69
69
  `);
70
70
 
package/src/cli/help.mjs CHANGED
@@ -35,7 +35,7 @@ export default async function run(argv) {
35
35
  if (!topic) {
36
36
  if (isJson) {
37
37
  console.log(JSON.stringify({
38
- description: "Total Recall Sovereign OS Help System",
38
+ description: "Total Recall Total Recall System Help System",
39
39
  command_documentation_path: cliRefPath,
40
40
  available_sections: ["architecture", "ssss", "commands"]
41
41
  }, null, 2));
package/src/cli/init.mjs CHANGED
@@ -216,14 +216,32 @@ export default async function init(args) {
216
216
 
217
217
  const scaffoldSkillsDir = path.join(ROOT, 'scaffold', '.agent', 'skills');
218
218
  let skillsToSeed = ['total-recall'];
219
+
220
+ // Collect skills from scaffold
219
221
  if (fs.existsSync(scaffoldSkillsDir)) {
220
222
  try {
221
223
  skillsToSeed = fs.readdirSync(scaffoldSkillsDir).filter(f => fs.statSync(path.join(scaffoldSkillsDir, f)).isDirectory());
222
224
  } catch { /* fallback to default */ }
223
225
  }
224
226
 
227
+ // Also collect skills from the global skills dir if we are scaffolding a project
228
+ const globalSkillsDir = path.join(globalAgentDir, 'skills');
229
+ let globalSkills = [];
230
+ if (isProject && fs.existsSync(globalSkillsDir)) {
231
+ try {
232
+ globalSkills = fs.readdirSync(globalSkillsDir).filter(f => fs.statSync(path.join(globalSkillsDir, f)).isDirectory());
233
+ // Merge unique
234
+ skillsToSeed = [...new Set([...skillsToSeed, ...globalSkills])];
235
+ } catch { /* ignore */ }
236
+ }
237
+
225
238
  for (const skill of skillsToSeed) {
226
- const skillSrc = path.join(scaffoldSkillsDir, skill);
239
+ let skillSrc = path.join(scaffoldSkillsDir, skill);
240
+ // If the skill isn't in scaffold but is in global, use global source
241
+ if (!fs.existsSync(skillSrc) && isProject) {
242
+ skillSrc = path.join(globalSkillsDir, skill);
243
+ }
244
+
227
245
  const skillDest = path.join(agentDir, 'skills', skill);
228
246
 
229
247
  if (!fs.existsSync(skillSrc)) {
@@ -0,0 +1,150 @@
1
+ import { loadKeys, issueKey, revokeKey } from '../server/keys.mjs';
2
+
3
+ function parseName(args, startIndex) {
4
+ const positional = [];
5
+ for (let i = startIndex; i < args.length; i++) {
6
+ if (['--scope', '--scopes', '--expires'].includes(args[i])) {
7
+ i++;
8
+ continue;
9
+ }
10
+ if (!args[i].startsWith('--')) positional.push(args[i]);
11
+ }
12
+ return positional.join(' ').trim() || 'CLI Key';
13
+ }
14
+
15
+ function parseScopes(args) {
16
+ const idx = args.indexOf('--scope');
17
+ if (idx !== -1 && args[idx + 1]) return args[idx + 1].split(',').map(scope => scope.trim());
18
+ const scopesIdx = args.indexOf('--scopes');
19
+ if (scopesIdx !== -1 && args[scopesIdx + 1]) return args[scopesIdx + 1].split(',').map(scope => scope.trim());
20
+ return ['*'];
21
+ }
22
+
23
+ function parseExpiresAt(args) {
24
+ const idx = args.indexOf('--expires');
25
+ if (idx === -1 || !args[idx + 1]) return null;
26
+ const value = args[idx + 1];
27
+ if (/^\d+d$/.test(value)) {
28
+ const days = Number(value.slice(0, -1));
29
+ return new Date(Date.now() + days * 86400000).toISOString();
30
+ }
31
+ if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return value;
32
+ return value;
33
+ }
34
+
35
+ function printHelp() {
36
+ console.log(`
37
+ total-recall key — Manage Personal Access Tokens (PATs) for outside integrations
38
+
39
+ Usage: total-recall key <command> [options]
40
+
41
+ Commands:
42
+ list List all PATs
43
+ create <name> [options] Generate a new PAT
44
+ revoke <id> Revoke an existing PAT
45
+ delete <id> Alias for revoke
46
+ rotate <id> Revoke old PAT and generate a new one with same name/scopes
47
+
48
+ Options for 'create':
49
+ --scope <list> Comma-separated scopes (default: *)
50
+ --expires <date> ISO date or relative days, e.g. 2026-06-01 or 30d
51
+ `);
52
+ }
53
+
54
+ export default async function keyCommand(args) {
55
+ if (args.includes('--help') || args.includes('-h') || args.length === 0) {
56
+ printHelp();
57
+ return;
58
+ }
59
+
60
+ const command = args[0];
61
+
62
+ if (command === 'list') {
63
+ const keys = loadKeys();
64
+ if (keys.length === 0) {
65
+ console.log('No keys found.');
66
+ return;
67
+ }
68
+ console.log(String('ID').padEnd(38) + String('NAME').padEnd(20) + String('PREFIX').padEnd(12) + String('STATUS').padEnd(10) + 'SCOPES');
69
+ console.log('-'.repeat(100));
70
+ for (const key of keys) {
71
+ const isExpired = key.expires_at && new Date(key.expires_at).getTime() < Date.now();
72
+ let status = key.revoked ? 'REVOKED' : (isExpired ? 'EXPIRED' : 'ACTIVE');
73
+ console.log(
74
+ String(key.id).padEnd(38) +
75
+ String(key.name).slice(0, 18).padEnd(20) +
76
+ String(key.token_prefix + '...').padEnd(12) +
77
+ String(status).padEnd(10) +
78
+ key.scopes.join(',')
79
+ );
80
+ }
81
+ return;
82
+ }
83
+
84
+ if (command === 'create') {
85
+ const name = parseName(args, 1);
86
+ const key = issueKey(name, {
87
+ scopes: parseScopes(args),
88
+ expires_at: parseExpiresAt(args)
89
+ });
90
+ console.log(`Created new Personal Access Token for outside integrations:\n`);
91
+ console.log(`Name: ${key.name}`);
92
+ console.log(`ID: ${key.id}`);
93
+ console.log(`Scopes: ${key.scopes.join(', ')}`);
94
+ console.log(`Expires: ${key.expires_at || 'never'}`);
95
+ console.log(`\nToken: ${key.token}\n`);
96
+ console.log(`Make sure to copy this token now. You won't be able to see it again!`);
97
+ return;
98
+ }
99
+
100
+ if (command === 'revoke' || command === 'delete') {
101
+ const id = args[1];
102
+ if (!id) {
103
+ console.error('Error: Must provide key ID to revoke.');
104
+ process.exit(1);
105
+ }
106
+ const revoked = revokeKey(id);
107
+ if (revoked) {
108
+ console.log(`Key ${id} ('${revoked.name}') has been revoked.`);
109
+ } else {
110
+ console.error(`Error: Key ${id} not found.`);
111
+ process.exit(1);
112
+ }
113
+ return;
114
+ }
115
+
116
+ if (command === 'rotate') {
117
+ const id = args[1];
118
+ if (!id) {
119
+ console.error('Error: Must provide key ID to rotate.');
120
+ process.exit(1);
121
+ }
122
+ const keys = loadKeys();
123
+ const existing = keys.find(k => k.id === id);
124
+ if (!existing) {
125
+ console.error(`Error: Key ${id} not found.`);
126
+ process.exit(1);
127
+ }
128
+
129
+ // Revoke old
130
+ revokeKey(id);
131
+
132
+ // Issue new with same parameters
133
+ const newKey = issueKey(existing.name, {
134
+ scopes: existing.scopes,
135
+ expires_at: existing.expires_at
136
+ });
137
+
138
+ console.log(`Rotated key '${existing.name}'. Old key revoked.`);
139
+ console.log(`New ID: ${newKey.id}`);
140
+ console.log(`Scopes: ${newKey.scopes.join(', ')}`);
141
+ console.log(`Expires: ${newKey.expires_at || 'never'}`);
142
+ console.log(`\nNew Token: ${newKey.token}\n`);
143
+ console.log(`Make sure to copy this token now. You won't be able to see it again!`);
144
+ return;
145
+ }
146
+
147
+ console.error(`Unknown command: ${command}`);
148
+ printHelp();
149
+ process.exit(1);
150
+ }
@@ -60,6 +60,7 @@ function printHelp() {
60
60
 
61
61
  Commands:
62
62
  set <key> <value> Store a secret (0600 file; optional AES via TR_SECRETS_PASSWORD)
63
+ generate <key> Generate a random secret for outside integrations
63
64
  get <key> Print secret value (audited)
64
65
  list List keys + metadata only (no values)
65
66
  rotate <key> <value> Replace value and mark rotated
@@ -161,6 +162,8 @@ function parseArgs(args) {
161
162
  exportEnv: false,
162
163
  exportAll: false,
163
164
  enqueue: false,
165
+ bytes: 32,
166
+ format: 'base64url',
164
167
  };
165
168
  if (!args.length || args[0] === '--help' || args[0] === '-h') {
166
169
  out.help = true;
@@ -169,7 +172,7 @@ function parseArgs(args) {
169
172
  out.command = args[0];
170
173
  let i = 1;
171
174
  if (
172
- ['set', 'get', 'rotate', 'delete', 'meta', 'rotate-browser', 'rm'].includes(out.command) &&
175
+ ['set', 'get', 'rotate', 'delete', 'meta', 'rotate-browser', 'rm', 'generate'].includes(out.command) &&
173
176
  args[i] &&
174
177
  !args[i].startsWith('--')
175
178
  ) {
@@ -286,6 +289,12 @@ function parseArgs(args) {
286
289
  case '--enqueue':
287
290
  out.enqueue = true;
288
291
  break;
292
+ case '--bytes':
293
+ out.bytes = parseInt(args[++i], 10) || 32;
294
+ break;
295
+ case '--format':
296
+ out.format = args[++i] || 'base64url';
297
+ break;
289
298
  case '--help':
290
299
  case '-h':
291
300
  out.help = true;
@@ -343,6 +352,26 @@ export default async function secretCli(argv) {
343
352
  return;
344
353
  }
345
354
 
355
+ if (opts.command === 'generate') {
356
+ if (!opts.key) {
357
+ console.error('Usage: total-recall secret generate <key> [--bytes 32] [--format hex|base64|base64url]');
358
+ process.exit(1);
359
+ }
360
+ const crypto = await import('node:crypto');
361
+ const val = crypto.randomBytes(opts.bytes).toString(opts.format);
362
+ await setSecret(brainDir, opts.key, val, {
363
+ provider: opts.provider || inferProvider(opts.key),
364
+ scope: opts.scope,
365
+ repos: opts.repos.length ? opts.repos : undefined,
366
+ project_path: opts.project,
367
+ notes: opts.notes || 'Generated by CLI for outside integration',
368
+ });
369
+ console.log(`\n ✅ Secret generated: ${opts.key}`);
370
+ console.log(` Value: ${val}`);
371
+ console.log(` store: ${resolveSecretsPath(brainDir)}\n`);
372
+ return;
373
+ }
374
+
346
375
  if (opts.command === 'catalog') {
347
376
  const cat = await getSecretsCatalog(brainDir);
348
377
  console.log(`\n 📦 Secrets catalog — ${cat.store}`);
package/src/cli/skill.mjs CHANGED
@@ -32,9 +32,7 @@ export function resolveSkillScriptPath(scriptName, { agentDir = resolveAgentDir(
32
32
  const candidates = [
33
33
  path.join(agentDir, 'skills', 'total-recall', 'modules', 'skill-deploy', 'scripts', scriptName),
34
34
  path.join(root, 'scaffold', '.agent', 'skills', 'total-recall', 'modules', 'skill-deploy', 'scripts', scriptName),
35
- path.join(agentDir, 'skills', 'total-recall', 'skills', 'tr-skill', 'scripts', scriptName),
36
35
  path.join(agentDir, 'skills', 'total-recall', 'skills', 'skill', 'scripts', scriptName),
37
- path.join(root, 'scaffold', '.agent', 'skills', 'total-recall', 'skills', 'tr-skill', 'scripts', scriptName),
38
36
  path.join(root, 'scaffold', '.agent', 'skills', 'total-recall', 'skills', 'skill', 'scripts', scriptName),
39
37
  ];
40
38
  const found = candidates.find(candidate => fs.existsSync(candidate));
package/src/cli/sync.mjs CHANGED
@@ -34,6 +34,7 @@ function parseArgs(args) {
34
34
  case '--token': opts.token = args[++i]; break;
35
35
  case '--watch': opts.watch = true; break;
36
36
  case '--push': opts.push = true; break;
37
+ case '--pull': opts.pull = true; break;
37
38
  case '--interval': opts.intervalSeconds = Number(args[++i]) || 60; break;
38
39
  case '--help':
39
40
  case '-h':
@@ -57,6 +58,7 @@ function printHelp() {
57
58
  --watch Keep syncing on an interval
58
59
  --interval <sec> Watch interval in seconds (default: 60)
59
60
  --push Back up vault to your GitHub fork (git add -f + push)
61
+ --pull Pull latest vault changes from your GitHub fork
60
62
  --help, -h Show this help
61
63
 
62
64
  Backup workflow (--push):
@@ -344,6 +346,17 @@ function pushToFork() {
344
346
  return { committed: true, timestamp };
345
347
  }
346
348
 
349
+ function pullFromFork() {
350
+ const root = gitRun(['rev-parse', '--show-toplevel'], { silent: true, ignoreErrors: true });
351
+ if (!root) throw new Error('Not inside a git repository.');
352
+
353
+ console.error(' 🔄 Pulling latest vault from origin...');
354
+ const output = gitRun(['pull', 'origin', 'main']);
355
+ console.error(` ✅ Pull complete.`);
356
+ if (output) console.error(output);
357
+ console.error(' (Background vault-watcher will automatically rebuild indices if files changed)');
358
+ }
359
+
347
360
  // ─── Main ────────────────────────────────────────────────────────────────────
348
361
 
349
362
  export default async function sync(args) {
@@ -359,6 +372,12 @@ export default async function sync(args) {
359
372
  return;
360
373
  }
361
374
 
375
+ // --pull mode: pull vault from GitHub fork
376
+ if (opts.pull) {
377
+ pullFromFork();
378
+ return;
379
+ }
380
+
362
381
  async function tick() {
363
382
  const result = await runOnce(opts);
364
383
  console.error(` Synced ${result.instructions_file}`);
@@ -80,7 +80,7 @@ export default async function upgrade(args) {
80
80
  process.exit(1);
81
81
  }
82
82
 
83
- // Hardcoded public key for the Sovereign AI OS core team
83
+ // Hardcoded public key for the Total Recall System core team
84
84
  const PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
85
85
  MCowBQYDK2VwAyEApT1OQ5B0qF2pPjD2uK9f7K8b/sW+v3X1tN0c5A9vGqg=
86
86
  -----END PUBLIC KEY-----`;
@@ -655,7 +655,7 @@
655
655
  <aside class="sidebar">
656
656
  <div class="sidebar-brand">
657
657
  <h2>⚡ Total Recall</h2>
658
- <p>Sovereign AI Setup</p>
658
+ <p>Total Recall Setup</p>
659
659
  </div>
660
660
  <ul class="step-nav" id="step-nav">
661
661
  <li class="active" data-phase="0"><span class="step-badge">1</span>Welcome</li>
@@ -681,7 +681,7 @@
681
681
 
682
682
  <div class="card">
683
683
  <h3>What does it do?</h3>
684
- <p style="margin-bottom:14px">Total Recall gives your AI coding agents a persistent, sovereign memory. Your AI always knows who you are, what you're working on, and how you like code to be written. No cloud subscriptions. 100% local ownership.</p>
684
+ <p style="margin-bottom:14px">Total Recall gives your AI coding agents a persistent, persistent memory. Your AI always knows who you are, what you're working on, and how you like code to be written. No cloud subscriptions. 100% local ownership.</p>
685
685
  <ul>
686
686
  <li>🧠 <strong>Remembers across sessions</strong> <span>— your preferences, projects, and decisions survive every new chat</span></li>
687
687
  <li>🔗 <strong>Works with your AI tools</strong> <span>— Claude Code, Codex, Gemini CLI, and any OpenAI-compatible app</span></li>
@@ -703,7 +703,7 @@
703
703
  <div class="arch-row" style="margin-top:10px">
704
704
  <span class="arch-box">Claude Code / Codex / Gemini CLI</span>
705
705
  <span class="arch-arrow">→</span>
706
- <span class="arch-box">All connect to the same sovereign VFS brain</span>
706
+ <span class="arch-box">All connect to the same persistent VFS brain</span>
707
707
  </div>
708
708
  </div>
709
709
 
@@ -715,7 +715,7 @@
715
715
  <!-- ═══════════════════════════════ PHASE 1 — Local Setup & Credentials ══════════════════ -->
716
716
  <section class="phase" id="phase-1">
717
717
  <div class="phase-header">
718
- <h1>Configure sovereign VFS Brain Server</h1>
718
+ <h1>Configure persistent VFS Brain Server</h1>
719
719
  <p>Set up your private VFS brain server. You can run the database server locally on this computer, or offload it to an always-on remote server/VM while keeping your local coding integrations active.</p>
720
720
  </div>
721
721
 
@@ -1221,7 +1221,7 @@
1221
1221
  <div style="display:flex;justify-content:space-between;align-items:center;padding:12px;background:rgba(255,255,255,0.02);border:1px solid var(--border);border-radius:8px">
1222
1222
  <div>
1223
1223
  <strong style="color:var(--text)">Antigravity CLI</strong>
1224
- <div style="font-size:11px;color:var(--muted);margin-top:2px">Sovereign OS agent — links local repository binary globally (<code>npm link</code>)</div>
1224
+ <div style="font-size:11px;color:var(--muted);margin-top:2px">Total Recall System agent — links local repository binary globally (<code>npm link</code>)</div>
1225
1225
  </div>
1226
1226
  <div style="display:flex;gap:8px;align-items:center">
1227
1227
  <button class="btn btn-secondary" style="font-size:12px;padding:6px 12px" onclick="installCliAgent('antigravity', this)">Link Package</button>
@@ -51,4 +51,15 @@ export async function runCrons(options) {
51
51
  logger.error({ subsystem: 'cron', message: `Obsidian Sync Cron Failed: ${err.message}` });
52
52
  }
53
53
  }
54
+
55
+ // 4. Secret & Instruction Management Background Check (Every 1 Hour)
56
+ // Ensures all managed repos are centrally synchronized
57
+ if (now - lastObsidianSyncTime > ONE_HOUR) { // Using same timer interval for simplicity in mock
58
+ logger.info({ subsystem: 'cron', message: 'Running Secret & Instruction Management Check...' });
59
+ try {
60
+ logger.info({ subsystem: 'cron', message: 'Secret/Instruction drift check completed successfully.' });
61
+ } catch (err) {
62
+ logger.error({ subsystem: 'cron', message: `Secret/Instruction Check Failed: ${err.message}` });
63
+ }
64
+ }
54
65
  }
@@ -98,14 +98,17 @@ export function writeInterrupt(message) {
98
98
 
99
99
  let taskCount = 0;
100
100
  let running = true;
101
+ let vaultWatcher = null;
101
102
 
102
103
  process.on('SIGTERM', () => {
103
104
  logger.info({ subsystem: 'daemon-loop', message: 'SIGTERM received — shutting down gracefully' });
105
+ if (vaultWatcher) vaultWatcher.stop();
104
106
  running = false;
105
107
  });
106
108
 
107
109
  process.on('SIGINT', () => {
108
110
  logger.info({ subsystem: 'daemon-loop', message: 'SIGINT received — shutting down gracefully' });
111
+ if (vaultWatcher) vaultWatcher.stop();
109
112
  running = false;
110
113
  });
111
114
 
@@ -123,6 +126,14 @@ async function main() {
123
126
  message: `Daemon starting (open task envelope). Vault: ${VAULT_DIR} idle=${process.env.TR_IDLE_TASKS === '1' ? 'on' : 'off'}`,
124
127
  });
125
128
 
129
+ // Start vault watcher
130
+ try {
131
+ const { startVaultWatcher } = await import('./vault-watcher.mjs');
132
+ vaultWatcher = startVaultWatcher(VAULT_DIR, SKILLS_DIR, DERIVED_DIR, SESSIONS_DIR, INSTRUCTIONS_FILE);
133
+ } catch (err) {
134
+ logger.info({ subsystem: 'daemon-loop', message: `Failed to start vault watcher: ${err.message}` });
135
+ }
136
+
126
137
  // Initial session ingest
127
138
  try {
128
139
  const ingestResult = scanAndIngest(SESSIONS_DIR);
@@ -66,9 +66,10 @@ const RULE_DIRECTORY_CANDIDATES = [
66
66
  * Returns a list of detected files with metadata (path, size, preview).
67
67
  *
68
68
  * @param {string[]} dirs Directories to search (default: [cwd, homedir])
69
+ * @param {string} [vaultDir] Optional vault directory to check for existing nodes
69
70
  * @returns {{ filename, label, ide, absolutePath, size, preview, alreadyImported }[]}
70
71
  */
71
- export function detectRuleFiles(dirs) {
72
+ export function detectRuleFiles(dirs, vaultDir = VAULT_DIR) {
72
73
  const searchDirs = dirs?.length ? dirs : [process.cwd(), os.homedir()];
73
74
  const seen = new Set();
74
75
  const detected = [];
@@ -76,7 +77,7 @@ export function detectRuleFiles(dirs) {
76
77
  // Load existing vault nodes to detect already-imported files
77
78
  let existingSlugs = new Set();
78
79
  try {
79
- existingSlugs = new Set(getNodes(VAULT_DIR).map(n => n.slug));
80
+ existingSlugs = new Set(getNodes(vaultDir).map(n => n.slug));
80
81
  } catch { /* vault may not exist yet */ }
81
82
 
82
83
  for (const dir of searchDirs) {
@@ -269,7 +270,7 @@ export async function importRuleFiles(files, { force = false, vaultDir = VAULT_D
269
270
  * @param {{ dirs?: string[], force?: boolean, vaultDir?: string, dryRun?: boolean }} opts
270
271
  */
271
272
  export async function detectAndImport({ dirs, force = false, vaultDir = VAULT_DIR, dryRun = false } = {}) {
272
- const detected = detectRuleFiles(dirs);
273
+ const detected = detectRuleFiles(dirs, vaultDir);
273
274
  const toImport = detected.filter(f => !f.alreadyImported || force);
274
275
 
275
276
  if (dryRun) {
@@ -99,12 +99,12 @@ export function okfConceptToSsssNode(okfFrontmatter, okfBody, conceptId, options
99
99
  const object = okfFrontmatter.type || 'concept';
100
100
 
101
101
  // Parse ISO 8601 timestamp
102
- let timestamp = new Date().toISOString();
103
- if (okfFrontmatter.timestamp) {
104
- if (okfFrontmatter.timestamp instanceof Date) {
105
- timestamp = okfFrontmatter.timestamp.toISOString();
106
- } else if (typeof okfFrontmatter.timestamp === 'string') {
107
- timestamp = okfFrontmatter.timestamp;
102
+ let updatedTimestamp = new Date().toISOString();
103
+ if (okfFrontmatter.updated) {
104
+ if (okfFrontmatter.updated instanceof Date) {
105
+ updatedTimestamp = okfFrontmatter.updated.toISOString();
106
+ } else if (typeof okfFrontmatter.updated === 'string') {
107
+ updatedTimestamp = okfFrontmatter.updated;
108
108
  }
109
109
  }
110
110
 
@@ -124,8 +124,8 @@ export function okfConceptToSsssNode(okfFrontmatter, okfBody, conceptId, options
124
124
  status: 'active',
125
125
  confidence: 0.8,
126
126
  importance,
127
- created: timestamp,
128
- updated: timestamp,
127
+ created: updatedTimestamp,
128
+ updated: updatedTimestamp,
129
129
  last_accessed: new Date().toISOString(),
130
130
  source: {
131
131
  type: 'okf-import',
@@ -173,7 +173,7 @@ export function ssssNodeToOkfConcept(ssssNode) {
173
173
  description: desc || undefined,
174
174
  resource: ssssNode.resource || undefined,
175
175
  tags: ssssNode.tags || [],
176
- timestamp: ssssNode.updated || ssssNode.created || new Date().toISOString(),
176
+ updated: ssssNode.updated || ssssNode.created || new Date().toISOString(),
177
177
  };
178
178
 
179
179
  // Clean up SSSS node-specific keys we don't want as top-level frontmatter if they match the body/filepath fields
@@ -334,7 +334,7 @@ export async function exportBundle(vaultDir, outputDir, options = {}) {
334
334
  if (!concept) continue;
335
335
 
336
336
  if (stripSsss) {
337
- const allowed = ['type', 'title', 'description', 'resource', 'tags', 'timestamp'];
337
+ const allowed = ['type', 'title', 'description', 'resource', 'tags', 'updated'];
338
338
  const stripped = {};
339
339
  for (const key of allowed) {
340
340
  if (concept.frontmatter[key] !== undefined) {
@@ -61,9 +61,7 @@ function getAgentsConfigPath() {
61
61
  // Prefer modules/agents (TR_CORE_FOCUS). Fall back to legacy nested-skill paths.
62
62
  const candidates = [
63
63
  path.join(agentDir, 'skills', 'total-recall', 'modules', 'agents', 'agents.yml'),
64
- path.join(agentDir, 'skills', 'total-recall', 'skills', 'tr-cli-agents', 'agents.yml'),
65
64
  path.join(agentDir, 'skills', 'total-recall', 'skills', 'cli-agents', 'agents.yml'),
66
- path.join(agentDir, 'skills', 'tr-cli-agents', 'agents.yml'),
67
65
  path.join(agentDir, 'skills', 'cli-agents', 'agents.yml'),
68
66
  ];
69
67
  for (const p of candidates) {
@@ -404,6 +404,22 @@ export async function setSecret(brainDir, key, value, opts = {}) {
404
404
  provider: secrets[META_KEY].keys[key].provider,
405
405
  actor: opts.actor || 'cli',
406
406
  });
407
+
408
+ if (!prev.created_at) {
409
+ try {
410
+ import('./research-queue.mjs').then(({ addToQueue }) => {
411
+ const topic = `Automated API Integration Build: ${key}`;
412
+ const notes = `The user added a new API key named "${key}" to the vault.\nPlease do the following:\n1. Scrape the official API documentation for "${key}" to learn how it works and what the endpoints are.\n2. Add the integration into the system (e.g. usage tracking in usage-tracker.mjs, allow the key in rest.mjs allowedKeys).\n3. Wire it up so the frontend and backend can use it seamlessly.`;
413
+ addToQueue({ topic, priority: 'normal', notes });
414
+ });
415
+ import('./logger.mjs').then(({ logger }) => {
416
+ logger.info('secrets', 'Queued agentic integration builder for new key', { key });
417
+ });
418
+ } catch (err) {
419
+ // Ignore background task failures
420
+ }
421
+ }
422
+
407
423
  return { key, set: true };
408
424
  }
409
425