total-recall-brain 3.13.2 → 3.14.4

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 +26 -26
  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
@@ -1060,3 +1060,41 @@ export function pullAllSkills(brainDir, opts = {}) {
1060
1060
  export function defaultBrainDir() {
1061
1061
  return process.env._TR_TEST_BRAIN_DIR || path.join(os.homedir(), '.agent', 'skills', 'total-recall');
1062
1062
  }
1063
+ /**
1064
+ * Pipeline to resolve skill locations across Embedded, Project, and Global scopes.
1065
+ * Implements the SSOT for Embedded (repo) vs System (brain) priority.
1066
+ *
1067
+ * @param {string} skillName
1068
+ * @param {string} [currentRepoPath] - Optional local project path (Embedded layer)
1069
+ * @param {string} [projectBrainDir] - Optional project brain (Project layer)
1070
+ * @param {string} [globalBrainDir] - Optional global brain (Global layer)
1071
+ * @returns {{ type: 'embedded'|'project'|'global', path: string } | null}
1072
+ */
1073
+ export function resolveSkillPipeline(skillName, currentRepoPath, projectBrainDir, globalBrainDir) {
1074
+ // 1. Embedded Skills (Repo-specific memory)
1075
+ if (currentRepoPath) {
1076
+ const embeddedPath = path.join(currentRepoPath, '.agent', 'skills', skillName);
1077
+ if (fs.existsSync(embeddedPath)) {
1078
+ return { type: 'embedded', path: embeddedPath };
1079
+ }
1080
+ }
1081
+
1082
+ // 2. Project Skills (Context Layer)
1083
+ if (projectBrainDir) {
1084
+ const projectPath = path.join(projectBrainDir, '.agent', 'skills', skillName);
1085
+ if (fs.existsSync(projectPath)) {
1086
+ return { type: 'project', path: projectPath };
1087
+ }
1088
+ }
1089
+
1090
+ // 3. Global System Skills (Identity Layer)
1091
+ if (globalBrainDir) {
1092
+ // Standard system skills path inside the global brain registry
1093
+ const globalPath = path.join(resolveRegistryDir(globalBrainDir), 'skills', skillName);
1094
+ if (fs.existsSync(globalPath)) {
1095
+ return { type: 'global', path: globalPath };
1096
+ }
1097
+ }
1098
+
1099
+ return null;
1100
+ }
@@ -312,7 +312,7 @@ export async function buildRulesBlock(skillsDir, nodes = [], { consumer = 'ide',
312
312
  combined = `## Total Recall — Active Memory Context\n\nYour memories and rules are loaded from the active brain vault.\n`;
313
313
  } else {
314
314
  // Compact CLI reference (OKF §6 progressive disclosure — summary, not full manual)
315
- combined = `## Total Recall — Sovereign Memory System (Installed)
315
+ combined = `## Total Recall — Persistent Memory System (Installed)
316
316
 
317
317
  **Quick Reference:**
318
318
  - \`npx total-recall remember <category> "<content>" [options]\` — Save to memory (categories: invariant, preference, correction, fact, concept, pattern, anti-pattern, decision, lore; key flags: --importance, --priority, --modality, --tags, --global, --project)
@@ -103,11 +103,17 @@ async function handleSystem2Deliberation(task, context) {
103
103
  logger.info('task-runner', `Running System 2 deliberation for: ${task.target}`);
104
104
  }
105
105
 
106
+ import { runCrons } from './crons.mjs';
107
+
106
108
  /**
107
109
  * Main loop for the task runner.
108
110
  */
109
111
  export async function runTaskQueue(queueDir, context) {
110
112
  logger.info('task-runner', 'Starting Task Runner...');
113
+
114
+ // Run background daemon crons (examine code, sync, secret management)
115
+ await runCrons(context);
116
+
111
117
  const pending = loadPendingTasks(queueDir);
112
118
 
113
119
  if (pending.length === 0) {
@@ -6,18 +6,22 @@ import { logger } from './logger.mjs';
6
6
  import { brainDir } from './config.mjs';
7
7
 
8
8
  // Standard pricing constants ($ per million tokens)
9
+ // Pricing verified via web search (July 2026)
9
10
  export const PRICING = {
10
11
  claude: {
12
+ // Claude 3.5 Sonnet Standard Pricing
11
13
  input: 3.00 / 1000000,
12
14
  output: 15.00 / 1000000,
13
- cacheRead: 0.30 / 1000000,
14
- cacheCreation: 3.75 / 1000000
15
+ cacheRead: 0.30 / 1000000, // 0.1x base
16
+ cacheCreation: 3.75 / 1000000 // 1.25x base
15
17
  },
16
18
  gemini: {
19
+ // Gemini 1.5 Flash (legacy)
17
20
  input: 0.075 / 1000000,
18
21
  output: 0.30 / 1000000
19
22
  },
20
23
  codex: {
24
+ // GPT-4o Standard (used by Codex CLI)
21
25
  input: 2.50 / 1000000,
22
26
  output: 10.00 / 1000000
23
27
  }
@@ -269,11 +273,20 @@ export function calculateCurrentCost() {
269
273
  weeklyUsd: codexWeeklyCost,
270
274
  dailyTokens: codex.dailyInput + codex.dailyOutput,
271
275
  weeklyTokens: codex.weeklyInput + codex.weeklyOutput
272
- }
276
+ },
277
+ openrouter: { dailyUsd: 0, weeklyUsd: 0, dailyTokens: 0, weeklyTokens: 0 },
278
+ tavily: { dailyUsd: 0, weeklyUsd: 0, dailyTokens: 0, weeklyTokens: 0 },
279
+ brave: { dailyUsd: 0, weeklyUsd: 0, dailyTokens: 0, weeklyTokens: 0 },
280
+ exa: { dailyUsd: 0, weeklyUsd: 0, dailyTokens: 0, weeklyTokens: 0 },
281
+ serper: { dailyUsd: 0, weeklyUsd: 0, dailyTokens: 0, weeklyTokens: 0 }
273
282
  }
274
283
  };
275
284
  }
276
285
 
286
+ export function syncUsageLedger(pricingMap) {
287
+ // Currently a no-op hook for future backend usage ledger syncing.
288
+ }
289
+
277
290
  /**
278
291
  * Safely load and parse budget.yml config.
279
292
  *
@@ -0,0 +1,92 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { invalidate, getNodes } from './vault-cache.mjs';
4
+ import { compileSurface } from './surface.mjs';
5
+ import { buildEmbeddingsIndex, buildSessionEmbeddingsIndex } from './embeddings.mjs';
6
+ import { logger } from './logger.mjs';
7
+
8
+ /**
9
+ * Vault Watcher
10
+ * Watches the memory-vault directory for external edits (e.g., from Obsidian).
11
+ * Debounces changes and triggers cache invalidation, surface compilation, and embeddings rebuild.
12
+ */
13
+
14
+ let recompileTimer = null;
15
+ const RECOMPILE_DEBOUNCE_MS = 2000;
16
+ let watcher = null;
17
+
18
+ export function startVaultWatcher(vaultDir, skillsDir, derivedDir, sessionsDir, instructionsFile) {
19
+ if (watcher) return watcher;
20
+
21
+ if (!fs.existsSync(vaultDir)) {
22
+ fs.mkdirSync(vaultDir, { recursive: true });
23
+ }
24
+
25
+ const callback = (eventType, filename) => {
26
+ // Only react to markdown files
27
+ if (!filename || !filename.endsWith('.md')) return;
28
+
29
+ // Immediately invalidate the cache so subsequent reads see the change
30
+ invalidate(vaultDir);
31
+
32
+ if (recompileTimer) clearTimeout(recompileTimer);
33
+ recompileTimer = setTimeout(async () => {
34
+ recompileTimer = null;
35
+ try {
36
+ logger.info({
37
+ subsystem: 'vault-watcher',
38
+ message: `Detected external edits (${filename} etc). Recompiling surface and embeddings...`,
39
+ });
40
+
41
+ // 1. Recompile instructions surface
42
+ await compileSurface({
43
+ vaultDir,
44
+ skillsDir,
45
+ derivedDir,
46
+ instructionsFile,
47
+ });
48
+
49
+ // 2. Rebuild embeddings
50
+ try {
51
+ const vaultNodes = getNodes(vaultDir);
52
+ await buildEmbeddingsIndex(vaultNodes, derivedDir);
53
+ await buildSessionEmbeddingsIndex(sessionsDir, derivedDir);
54
+ } catch (embedErr) {
55
+ // Ollama/embeddings offline non-fatal
56
+ }
57
+
58
+ logger.info({
59
+ subsystem: 'vault-watcher',
60
+ message: 'Finished rebuilding index for external changes.',
61
+ });
62
+ } catch (err) {
63
+ logger.error({
64
+ subsystem: 'vault-watcher',
65
+ message: `Recompile failed: ${err.message}`,
66
+ });
67
+ }
68
+ }, RECOMPILE_DEBOUNCE_MS);
69
+ };
70
+
71
+ try {
72
+ watcher = fs.watch(vaultDir, { recursive: false }, callback);
73
+ logger.info({
74
+ subsystem: 'vault-watcher',
75
+ message: `Watching ${vaultDir} for external Obsidian edits`,
76
+ });
77
+ } catch (err) {
78
+ logger.error({
79
+ subsystem: 'vault-watcher',
80
+ message: `Failed to start vault watcher: ${err.message}`,
81
+ });
82
+ }
83
+
84
+ return {
85
+ stop() {
86
+ if (watcher) {
87
+ try { watcher.close(); } catch {}
88
+ watcher = null;
89
+ }
90
+ }
91
+ };
92
+ }
@@ -3,8 +3,6 @@ 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
-
8
6
  /**
9
7
  * Atomic file write using write-then-rename to prevent partial corruption.
10
8
  */
@@ -179,7 +179,7 @@ function ssssReferenceDir() {
179
179
  const candidates = [
180
180
  path.join(SKILLS_DIR, 'total-recall', 'references'),
181
181
  path.join(SKILLS_DIR, 'total-recall', 'modules', 'ssss', 'references'),
182
- path.join(SKILLS_DIR, 'total-recall', 'skills', 'tr-ssss', 'references'),
182
+ path.join(SKILLS_DIR, 'okf', 'references'),
183
183
  ];
184
184
  return candidates.find((p) => fs.existsSync(p)) || candidates[0];
185
185
  }
@@ -802,11 +802,11 @@ router.post('/api/integrations/connect', requireAuth, (req, res) => {
802
802
  /**
803
803
  * GET /api/import/rules
804
804
  * Detect existing rule files in given dirs.
805
- * Query: ?dir=/path (repeatable, default: process.cwd())
805
+ * Query: ?dir=/path (repeatable, default: ROOT)
806
806
  */
807
807
  router.get('/api/import/rules', requireAuth, requireScope('memory:read'), (req, res) => {
808
808
  try {
809
- const allowedRoots = [process.cwd(), os.homedir()];
809
+ const allowedRoots = [ROOT, VAULT_DIR];
810
810
  const rawDirs = req.query.dir ? (Array.isArray(req.query.dir) ? req.query.dir : [req.query.dir]) : allowedRoots;
811
811
  const dirs = rawDirs.filter(d => allowedRoots.some(root => path.resolve(d).startsWith(path.resolve(root))));
812
812
  if (dirs.length === 0) return badRequest(res, 'No permitted directories specified');
@@ -824,7 +824,7 @@ router.get('/api/import/rules', requireAuth, requireScope('memory:read'), (req,
824
824
  router.post('/api/import/rules', requireAuth, requireScope('memory:write'), async (req, res) => {
825
825
  try {
826
826
  const { dirs, force = false, dryRun = false } = req.body || {};
827
- const searchDirs = dirs?.length ? dirs : [process.cwd(), os.homedir()];
827
+ const searchDirs = dirs?.length ? dirs : [ROOT, VAULT_DIR];
828
828
  const detected = detectRuleFiles(searchDirs);
829
829
  const toImport = req.body?.files?.length
830
830
  ? detected.filter(f => req.body.files.includes(f.absolutePath))
@@ -1042,14 +1042,18 @@ router.get('/api/files', requireAuth, requireScope('files:read'), (req, res) =>
1042
1042
  fs.mkdirSync(FILES_DIR, { recursive: true });
1043
1043
  }
1044
1044
  const files = fs.readdirSync(FILES_DIR).map(file => {
1045
- const stats = fs.statSync(path.join(FILES_DIR, file));
1046
- return {
1047
- name: file,
1048
- size: stats.size,
1049
- modified: stats.mtime,
1050
- isDirectory: stats.isDirectory()
1051
- };
1052
- });
1045
+ try {
1046
+ const stats = fs.statSync(path.join(FILES_DIR, file));
1047
+ return {
1048
+ name: file,
1049
+ size: stats.size,
1050
+ modified: stats.mtime,
1051
+ isDirectory: stats.isDirectory()
1052
+ };
1053
+ } catch (err) {
1054
+ return null;
1055
+ }
1056
+ }).filter(Boolean);
1053
1057
  res.json(files);
1054
1058
 
1055
1059
  } catch (err) { serverError(res, err); }
@@ -1069,13 +1073,17 @@ router.get("/api/scripts", requireAuth, requireScope("files:read"), (req, res) =
1069
1073
  const files = fs.readdirSync(SCRIPTS_DIR).filter(file =>
1070
1074
  file.endsWith(".mjs") || file.endsWith(".js") || file.endsWith(".py") || file.endsWith(".sh")
1071
1075
  ).map(file => {
1072
- const stats = fs.statSync(path.join(SCRIPTS_DIR, file));
1073
- return {
1074
- name: file,
1075
- size: stats.size,
1076
- modified: stats.mtime
1077
- };
1078
- });
1076
+ try {
1077
+ const stats = fs.statSync(path.join(SCRIPTS_DIR, file));
1078
+ return {
1079
+ name: file,
1080
+ size: stats.size,
1081
+ modified: stats.mtime
1082
+ };
1083
+ } catch (err) {
1084
+ return null;
1085
+ }
1086
+ }).filter(Boolean);
1079
1087
  res.json(files);
1080
1088
  } catch (err) {
1081
1089
  serverError(res, err);
@@ -1320,7 +1328,7 @@ router.get('/api/config-json', requireAuth, requireScope('config:read'), (req, r
1320
1328
  delete safeBrain.token;
1321
1329
  }
1322
1330
 
1323
- const allowedKeys = ['google_api_key', 'anthropic_api_key', 'openai_api_key', 'tavily_api_key', 'brave_api_key', 'exa_api_key', 'serper_api_key', 'github_token'];
1331
+ const allowedKeys = ['google_api_key', 'anthropic_api_key', 'openai_api_key', 'openrouter_api_key', 'tavily_api_key', 'brave_api_key', 'exa_api_key', 'serper_api_key', 'github_token'];
1324
1332
  const safeSecrets = {};
1325
1333
  for (const key of allowedKeys) {
1326
1334
  if (secrets[key] !== undefined) {
@@ -1372,7 +1380,7 @@ router.post('/api/config-json', requireAuth, requireScope('config:write'), (req,
1372
1380
  existingSecrets = JSON.parse(fs.readFileSync(secretsPath, 'utf8')) || {};
1373
1381
  } catch {}
1374
1382
  }
1375
- const allowedKeys = ['google_api_key', 'anthropic_api_key', 'openai_api_key', 'tavily_api_key', 'brave_api_key', 'exa_api_key', 'serper_api_key', 'github_token'];
1383
+ const allowedKeys = ['google_api_key', 'anthropic_api_key', 'openai_api_key', 'openrouter_api_key', 'tavily_api_key', 'brave_api_key', 'exa_api_key', 'serper_api_key', 'github_token'];
1376
1384
  for (const key of allowedKeys) {
1377
1385
  if (secrets[key] !== undefined) {
1378
1386
  if (secrets[key] === '') {
@@ -1626,7 +1634,7 @@ function ssssReferenceDir() {
1626
1634
  const candidates = [
1627
1635
  path.join(SKILLS_DIR, 'total-recall', 'references'),
1628
1636
  path.join(SKILLS_DIR, 'total-recall', 'modules', 'ssss', 'references'),
1629
- path.join(SKILLS_DIR, 'total-recall', 'skills', 'tr-ssss', 'references'),
1637
+ path.join(SKILLS_DIR, 'okf', 'references'),
1630
1638
  ];
1631
1639
  return candidates.find((p) => fs.existsSync(p)) || candidates[0];
1632
1640
  }
@@ -1636,7 +1644,7 @@ function ssssSkillDocPath() {
1636
1644
  path.join(SKILLS_DIR, 'total-recall', 'references', 'ssss-reference.md'),
1637
1645
  path.join(SKILLS_DIR, 'total-recall', 'SKILL.md'),
1638
1646
  path.join(SKILLS_DIR, 'total-recall', 'modules', 'ssss', 'MODULE.md'),
1639
- path.join(SKILLS_DIR, 'total-recall', 'skills', 'tr-ssss', 'SKILL.md'),
1647
+ path.join(SKILLS_DIR, 'okf', 'SKILL.md'),
1640
1648
  ];
1641
1649
  return candidates.find((p) => fs.existsSync(p)) || candidates[0];
1642
1650
  }
@@ -2028,7 +2036,14 @@ router.get('/api/gemini-models', requireAuth, async (req, res) => {
2028
2036
 
2029
2037
 
2030
2038
 
2031
- if (!apiKey) return res.json({ models: [], source: 'missing_key' });
2039
+ const fallbackModels = [
2040
+ { id: 'gemini-3.5-flash', displayName: 'Gemini 3.5 Flash', pricing: null },
2041
+ { id: 'gemini-3.5-pro', displayName: 'Gemini 3.5 Pro', pricing: null },
2042
+ { id: 'gemini-2.5-pro', displayName: 'Gemini 2.5 Pro', pricing: null },
2043
+ { id: 'gemini-2.5-flash', displayName: 'Gemini 2.5 Flash', pricing: null }
2044
+ ];
2045
+
2046
+ if (!apiKey) return res.json({ models: fallbackModels, source: 'missing_key' });
2032
2047
 
2033
2048
  try {
2034
2049
  const controller = new AbortController();
@@ -2058,7 +2073,7 @@ router.get('/api/gemini-models', requireAuth, async (req, res) => {
2058
2073
  }
2059
2074
  } catch {}
2060
2075
 
2061
- res.json({ models: [], source: 'api_error' });
2076
+ res.json({ models: fallbackModels, source: 'api_error' });
2062
2077
  } catch (err) {
2063
2078
  serverError(res, err);
2064
2079
  }
@@ -2215,42 +2230,39 @@ router.get('/api/update/check', requireAuth, async (req, res) => {
2215
2230
  */
2216
2231
  router.post('/api/update/run', requireAuth, async (req, res) => {
2217
2232
  try {
2218
- res.json({ success: true, message: 'Update started. Server is updating and will restart shortly.' });
2219
-
2220
- setImmediate(async () => {
2221
- logger.info('update', 'Starting auto-update process...');
2222
- try {
2223
- const { exec } = await import('node:child_process');
2224
-
2225
- const runCmd = (cmd, cwd) => new Promise((resolve, reject) => {
2226
- logger.info('update', `Running: ${cmd} in ${cwd}`);
2227
- exec(cmd, { cwd }, (err, stdout, stderr) => {
2228
- if (err) {
2229
- logger.error('update', `Command failed: ${cmd}. Error: ${err.message}`);
2230
- return reject(err);
2231
- }
2232
- resolve({ stdout, stderr });
2233
- });
2234
- });
2235
-
2236
- // 1. git pull
2237
- await runCmd('git pull', ROOT);
2238
- // 2. npm install
2239
- await runCmd('npm install', ROOT);
2240
- // 3. build/install frontend if package.json exists
2241
- const frontendPath = path.join(ROOT, 'frontend');
2242
- if (fs.existsSync(path.join(frontendPath, 'package.json'))) {
2243
- await runCmd('npm install', frontendPath);
2244
- await runCmd('npm run build', frontendPath);
2233
+ logger.info('update', 'Starting auto-update process...');
2234
+ const { exec } = await import('node:child_process');
2235
+
2236
+ const runCmd = (cmd, cwd) => new Promise((resolve, reject) => {
2237
+ logger.info('update', `Running: ${cmd} in ${cwd}`);
2238
+ exec(cmd, { cwd }, (err, stdout, stderr) => {
2239
+ if (err) {
2240
+ logger.error('update', `Command failed: ${cmd}. Error: ${err.message}`);
2241
+ return reject(err);
2245
2242
  }
2246
-
2247
- logger.info('update', 'Update successfully applied. Restarting server...');
2248
- process.exit(0);
2249
- } catch (err) {
2250
- logger.error('update', `Auto-update failed: ${err.message}`);
2251
- }
2243
+ resolve({ stdout, stderr });
2244
+ });
2252
2245
  });
2246
+
2247
+ // 1. git pull
2248
+ await runCmd('git pull', ROOT);
2249
+ // 2. npm install
2250
+ await runCmd('npm install', ROOT);
2251
+ // 3. build/install frontend if package.json exists
2252
+ const frontendPath = path.join(ROOT, 'frontend');
2253
+ if (fs.existsSync(path.join(frontendPath, 'package.json'))) {
2254
+ await runCmd('npm install', frontendPath);
2255
+ await runCmd('npm run build', frontendPath);
2256
+ }
2257
+
2258
+ logger.info('update', 'Update successfully applied. Restarting server...');
2259
+ res.json({ success: true, message: 'Update successfully applied. Server is restarting.' });
2260
+
2261
+ setTimeout(() => {
2262
+ process.exit(0);
2263
+ }, 1000);
2253
2264
  } catch (err) {
2265
+ logger.error('update', `Auto-update failed: ${err.message}`);
2254
2266
  serverError(res, err);
2255
2267
  }
2256
2268
  });
@@ -367,6 +367,14 @@
367
367
  "method": "POST",
368
368
  "path": "/api/skills/install"
369
369
  },
370
+ {
371
+ "method": "POST",
372
+ "path": "/api/skills/toggle"
373
+ },
374
+ {
375
+ "method": "POST",
376
+ "path": "/api/skills/audit"
377
+ },
370
378
  {
371
379
  "method": "GET",
372
380
  "path": "/api/skills/search"
@@ -24,7 +24,7 @@ export const FILES_DIR = path.join(BRAIN_DIR, 'files');
24
24
  export const TASKS_DIR = path.join(BRAIN_DIR, 'scheduler', 'queue');
25
25
  export const CONFIG_DIR = path.join(BRAIN_DIR, 'config');
26
26
 
27
- const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
27
+ export const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
28
28
  export const MODEL_CATALOG_DIR = path.join(ROOT, 'models', 'catalog', 'total-recall');
29
29
 
30
30
  /**
@@ -11,7 +11,7 @@
11
11
  import express from 'express';
12
12
  import path from 'node:path';
13
13
  import { requireAuth, requireScope, verifyStepUpToken } from '../auth.mjs';
14
- import { badRequest, serverError, notFound, BRAIN_DIR, resolveVaultFromQuery } from './_shared.mjs';
14
+ import { badRequest, serverError, notFound, BRAIN_DIR, resolveVaultFromQuery, ROOT } from './_shared.mjs';
15
15
  import {
16
16
  listSecretsMeta,
17
17
  setSecret,
@@ -161,7 +161,7 @@ router.post('/api/secrets/export-env', requireAuth, requireScope('keys:write', '
161
161
  return res.json({ results, store: path.join(brainDir, 'config', 'secrets.enc') });
162
162
  }
163
163
 
164
- const targetPath = body.path || body.project_path || process.cwd();
164
+ const targetPath = body.path || body.project_path || ROOT;
165
165
  if (body.preview_only || body.previewOnly) {
166
166
  const projection = await buildEnvProjection(brainDir, {
167
167
  projectPath: targetPath,
@@ -194,7 +194,7 @@ router.post('/api/secrets/export-env', requireAuth, requireScope('keys:write', '
194
194
  router.get('/api/secrets/export-env', requireAuth, requireScope('keys:read', 'config:read'), async (req, res) => {
195
195
  try {
196
196
  const brainDir = brainDirFromReq(req);
197
- const targetPath = req.query.path || process.cwd();
197
+ const targetPath = req.query.path || ROOT;
198
198
  const projection = await buildEnvProjection(brainDir, {
199
199
  projectPath: targetPath,
200
200
  includeGlobal: req.query.no_global !== '1',
@@ -236,7 +236,7 @@ router.get('/api/secrets/scan-env', requireAuth, requireScope('keys:write', 'con
236
236
  const scan = scanEnvSources({
237
237
  brainDir,
238
238
  includeProcessEnv: true,
239
- cwd: process.cwd(),
239
+ cwd: ROOT,
240
240
  });
241
241
  res.json(publicScanResult(scan, existingSet));
242
242
  } catch (err) {
@@ -73,11 +73,9 @@ const SKILL_DIR_CANDIDATES = SKILL_ROOTS.map((r) => r.rel);
73
73
  */
74
74
  const TR_SKILL_EXCLUDE = new Set([
75
75
  'total-recall',
76
- 'tr-skill',
77
- 'tr-ssss',
78
- 'tr-research',
79
- 'tr-cli-agents',
80
- 'tr-cli-agent',
76
+ 'skill',
77
+ 'okf',
78
+ 'research',
81
79
  'cli-agents', // TR CLI agent registry package, not an app skill
82
80
  ]);
83
81
 
@@ -85,7 +83,6 @@ function isTrSkillName(name) {
85
83
  if (!name || typeof name !== 'string') return true;
86
84
  const n = name.toLowerCase();
87
85
  if (TR_SKILL_EXCLUDE.has(n)) return true;
88
- if (n.startsWith('tr-')) return true;
89
86
  if (n.startsWith('total-recall')) return true;
90
87
  return false;
91
88
  }
@@ -187,10 +184,16 @@ function scanSkillRoot(skillsRoot, repo, projectPath, rootInfo) {
187
184
 
188
185
  /**
189
186
  * Collect skills for all registered projects, grouped by repo → IDE.
190
- * Excludes total-recall / tr-* system skills.
187
+ * Excludes total-recall system skills.
191
188
  */
192
189
  export function collectSkillsByRepo() {
193
190
  const registry = loadProjectRegistry();
191
+ // Inject the global homedir so global skills are also scanned and managed
192
+ registry.unshift({
193
+ name: 'Global',
194
+ path: os.homedir()
195
+ });
196
+
194
197
  const byRepo = new Map();
195
198
 
196
199
  for (const entry of registry) {
@@ -260,7 +263,7 @@ export function collectSkillsByRepo() {
260
263
  ide_label,
261
264
  docs,
262
265
  })),
263
- excluded_note: 'total-recall and tr-* skills are hidden from this catalog',
266
+ excluded_note: 'total-recall skills are hidden from this catalog',
264
267
  };
265
268
  }
266
269
 
@@ -334,8 +337,8 @@ function resolveTrSkillScript(scriptName) {
334
337
  const candidates = [
335
338
  path.join(AGENT_DIR, 'skills', 'total-recall', 'modules', 'skill-deploy', 'scripts', scriptName),
336
339
  path.join(ROOT, 'scaffold', '.agent', 'skills', 'total-recall', 'modules', 'skill-deploy', 'scripts', scriptName),
337
- path.join(AGENT_DIR, 'skills', 'total-recall', 'skills', 'tr-skill', 'scripts', scriptName),
338
- path.join(ROOT, 'scaffold', '.agent', 'skills', 'total-recall', 'skills', 'tr-skill', 'scripts', scriptName),
340
+ path.join(AGENT_DIR, 'skills', 'skill', 'scripts', scriptName),
341
+ path.join(ROOT, 'scaffold', '.agent', 'skills', 'skill', 'scripts', scriptName),
339
342
  ];
340
343
  const found = candidates.find((candidate) => fs.existsSync(candidate));
341
344
  if (!found) {
@@ -535,3 +538,82 @@ skillsRouter.delete('/api/skills/:name', requireAuth, requireScope('files:write'
535
538
  serverError(res, err);
536
539
  }
537
540
  });
541
+
542
+ import { callLocalRuntime, loadRuntimeConfig } from '../../core/runtime.mjs';
543
+
544
+ skillsRouter.post('/api/skills/toggle', requireAuth, requireScope('ssss:write'), async (req, res) => {
545
+ try {
546
+ const { skillName, targetRepo, enabled } = req.body;
547
+ if (!skillName || !targetRepo || targetRepo === 'Global') {
548
+ return res.status(400).json({ error: 'Missing parameters or invalid target repo' });
549
+ }
550
+
551
+ const globalSkillPath = path.join(os.homedir(), '.agent', 'skills', skillName);
552
+ const targetSkillPath = path.join(targetRepo, '.agent', 'skills', skillName);
553
+
554
+ if (enabled) {
555
+ if (!fs.existsSync(globalSkillPath)) {
556
+ return res.status(400).json({ error: 'Global skill not found' });
557
+ }
558
+ fs.cpSync(globalSkillPath, targetSkillPath, { recursive: true });
559
+
560
+ const targetSkillMd = path.join(targetSkillPath, 'SKILL.md');
561
+ if (fs.existsSync(targetSkillMd)) {
562
+ let content = fs.readFileSync(targetSkillMd, 'utf8');
563
+ content += `\n\n> **Repo Scope Override:** This skill was copied from the Global registry and is now scoped to \`${targetRepo}\`.\n`;
564
+ fs.writeFileSync(targetSkillMd, content, 'utf8');
565
+ }
566
+ } else {
567
+ if (fs.existsSync(targetSkillPath)) {
568
+ fs.rmSync(targetSkillPath, { recursive: true, force: true });
569
+ }
570
+ }
571
+
572
+ res.json({ success: true, message: `Skill ${skillName} ${enabled ? 'enabled' : 'disabled'} for ${targetRepo}` });
573
+ } catch (err) {
574
+ serverError(res, err);
575
+ }
576
+ });
577
+
578
+ skillsRouter.post('/api/skills/audit', requireAuth, requireScope('ssss:read'), async (req, res) => {
579
+ try {
580
+ const { skillName, repoPath } = req.body;
581
+ if (!skillName) return res.status(400).json({ error: 'skillName required' });
582
+
583
+ let skillDir;
584
+ try {
585
+ ({ skillDir } = resolveSkillDir(repoPath || 'Global', skillName, {}));
586
+ } catch (e) {
587
+ return res.status(404).json({ error: e.message });
588
+ }
589
+
590
+ const skillPath = path.join(skillDir, 'SKILL.md');
591
+ if (!fs.existsSync(skillPath)) return res.status(404).json({ error: 'SKILL.md not found' });
592
+
593
+ const content = fs.readFileSync(skillPath, 'utf8');
594
+ const prompt = `Analyze this AI Agent SKILL.md document.
595
+ Identify if it is outdated, incorrect, or problematic.
596
+ Propose specific updates to improve it.
597
+ If it looks perfect, say so.
598
+ Return a JSON object: { "outdated": boolean, "problematic": boolean, "reason": string, "proposed_changes": string }
599
+
600
+ SKILL DOCUMENT:
601
+ ${content}`;
602
+
603
+ const config = loadRuntimeConfig();
604
+ const result = await callLocalRuntime(prompt, "You are an expert AI agent capabilities auditor. Always return valid JSON matching the requested schema.", config);
605
+
606
+ // Extract JSON block
607
+ const match = result.match(/\{[\s\S]*\}/);
608
+ let auditData;
609
+ try {
610
+ auditData = JSON.parse(match ? match[0] : result);
611
+ } catch (e) {
612
+ auditData = { outdated: false, problematic: true, reason: 'Failed to parse AI response', proposed_changes: 'Check server logs. AI output was not valid JSON.' };
613
+ }
614
+
615
+ res.json({ success: true, audit: auditData });
616
+ } catch (err) {
617
+ serverError(res, err);
618
+ }
619
+ });