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.
- package/bin/total-recall.mjs +3 -0
- package/frontend/src/App.tsx +8 -7
- package/frontend/src/api.ts +25 -0
- package/frontend/src/components/UsageChart.tsx +1 -1
- package/frontend/src/pages/ApiKeysPage.tsx +118 -42
- package/frontend/src/pages/ChatPage.tsx +1 -1
- package/frontend/src/pages/DesignDocsPage.tsx +28 -21
- package/frontend/src/pages/FilesPage.tsx +6 -1
- package/frontend/src/pages/GraphPage.tsx +5 -1
- package/frontend/src/pages/HealthPage.tsx +1 -1
- package/frontend/src/pages/InboxPage.tsx +2 -1
- package/frontend/src/pages/InstructionsPage.tsx +2 -2
- package/frontend/src/pages/IntegrationsPage.tsx +11 -5
- package/frontend/src/pages/MemoryPage.tsx +27 -118
- package/frontend/src/pages/ModelsPage.tsx +2 -50
- package/frontend/src/pages/OnboardingPage.tsx +5 -10
- package/frontend/src/pages/OpenWikiPage.tsx +1 -0
- package/frontend/src/pages/SettingsPage.tsx +136 -131
- package/frontend/src/pages/SkillsPage.tsx +102 -46
- package/frontend/src/pages/TasksPage.tsx +60 -25
- package/frontend/src/pages/UsagePage.tsx +206 -95
- package/frontend/src/types.ts +17 -11
- package/frontend/src/utils/onboarding.ts +10 -0
- package/package.json +3 -2
- package/scaffold/.agent/skills/ssss/SKILL.md +235 -0
- package/scaffold/.agent/skills/ssss/evals/evals.json +37 -0
- package/scaffold/.agent/skills/ssss/references/contract-reference.md +95 -0
- package/scaffold/.agent/skills/ssss/scripts/validate-schema.mjs +215 -0
- package/scaffold/.agent/skills/ssss/subagents/audit-ssss-contract.md +22 -0
- package/scaffold/.agent/skills/total-recall/SKILL.md +1 -0
- package/src/cli/backup.mjs +3 -3
- package/src/cli/chat.mjs +2 -2
- package/src/cli/config.mjs +2 -9
- package/src/cli/connect.mjs +1 -1
- package/src/cli/deploy-ui.mjs +1 -1
- package/src/cli/deploy.mjs +2 -2
- package/src/cli/doctor.mjs +1 -1
- package/src/cli/help.mjs +1 -1
- package/src/cli/init.mjs +19 -1
- package/src/cli/key.mjs +150 -0
- package/src/cli/secret.mjs +30 -1
- package/src/cli/skill.mjs +0 -2
- package/src/cli/sync.mjs +19 -0
- package/src/cli/upgrade.mjs +1 -1
- package/src/cli/wizard.html +5 -5
- package/src/core/crons.mjs +11 -0
- package/src/core/daemon-loop.mjs +11 -0
- package/src/core/import-rules.mjs +4 -3
- package/src/core/okf-adapter.mjs +10 -10
- package/src/core/runtime.mjs +0 -2
- package/src/core/secrets-store.mjs +16 -0
- package/src/core/skills-registry.mjs +38 -0
- package/src/core/surface.mjs +1 -1
- package/src/core/task_runner.mjs +6 -0
- package/src/core/usage-tracker.mjs +16 -3
- package/src/core/vault-watcher.mjs +92 -0
- package/src/core/vault.mjs +0 -2
- package/src/server/api.mjs +1 -1
- package/src/server/rest.mjs +69 -57
- package/src/server/route-manifest.json +8 -0
- package/src/server/routes/_shared.mjs +1 -1
- package/src/server/routes/secrets.mjs +4 -4
- 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
|
+
}
|
package/src/core/surface.mjs
CHANGED
|
@@ -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 —
|
|
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)
|
package/src/core/task_runner.mjs
CHANGED
|
@@ -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
|
+
}
|
package/src/core/vault.mjs
CHANGED
package/src/server/api.mjs
CHANGED
|
@@ -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, '
|
|
182
|
+
path.join(SKILLS_DIR, 'okf', 'references'),
|
|
183
183
|
];
|
|
184
184
|
return candidates.find((p) => fs.existsSync(p)) || candidates[0];
|
|
185
185
|
}
|
package/src/server/rest.mjs
CHANGED
|
@@ -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:
|
|
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 = [
|
|
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 : [
|
|
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
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
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
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
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, '
|
|
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, '
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
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
|
-
|
|
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 ||
|
|
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 ||
|
|
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:
|
|
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
|
-
'
|
|
77
|
-
'
|
|
78
|
-
'
|
|
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
|
|
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
|
|
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', '
|
|
338
|
-
path.join(ROOT, 'scaffold', '.agent', 'skills', '
|
|
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
|
+
});
|