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
package/src/cli/config.mjs
CHANGED
|
@@ -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
|
|
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 ⚙️
|
|
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}`);
|
package/src/cli/connect.mjs
CHANGED
|
@@ -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
|
|
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}.`
|
package/src/cli/deploy-ui.mjs
CHANGED
|
@@ -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
|
|
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
|
package/src/cli/deploy.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* total-recall deploy
|
|
3
3
|
*
|
|
4
|
-
* Provision a target machine with the full
|
|
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
|
|
216
|
+
total-recall deploy — Provision a target machine with the Total Recall stack
|
|
217
217
|
|
|
218
218
|
Usage: total-recall deploy [options]
|
|
219
219
|
|
package/src/cli/doctor.mjs
CHANGED
|
@@ -64,7 +64,7 @@ export default async function doctor() {
|
|
|
64
64
|
console.log(`
|
|
65
65
|
┌─────────────────────────────────────────────────────────┐
|
|
66
66
|
│ Total Recall System Doctor │
|
|
67
|
-
│ Pre-flighting
|
|
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
|
|
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
|
-
|
|
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)) {
|
package/src/cli/key.mjs
ADDED
|
@@ -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
|
+
}
|
package/src/cli/secret.mjs
CHANGED
|
@@ -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}`);
|
package/src/cli/upgrade.mjs
CHANGED
|
@@ -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
|
|
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-----`;
|
package/src/cli/wizard.html
CHANGED
|
@@ -655,7 +655,7 @@
|
|
|
655
655
|
<aside class="sidebar">
|
|
656
656
|
<div class="sidebar-brand">
|
|
657
657
|
<h2>⚡ Total Recall</h2>
|
|
658
|
-
<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,
|
|
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
|
|
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
|
|
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">
|
|
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>
|
package/src/core/crons.mjs
CHANGED
|
@@ -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
|
}
|
package/src/core/daemon-loop.mjs
CHANGED
|
@@ -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(
|
|
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) {
|
package/src/core/okf-adapter.mjs
CHANGED
|
@@ -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
|
|
103
|
-
if (okfFrontmatter.
|
|
104
|
-
if (okfFrontmatter.
|
|
105
|
-
|
|
106
|
-
} else if (typeof okfFrontmatter.
|
|
107
|
-
|
|
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:
|
|
128
|
-
updated:
|
|
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
|
-
|
|
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', '
|
|
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) {
|
package/src/core/runtime.mjs
CHANGED
|
@@ -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
|
|