total-recall-brain 3.1.0

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 (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -0
  3. package/bin/antigravity.mjs +212 -0
  4. package/bin/init.mjs +36 -0
  5. package/bin/oci-sniper.sh +69 -0
  6. package/bin/total-recall.mjs +229 -0
  7. package/docs/ARCHITECTURE.md +251 -0
  8. package/docs/IDE_INTEGRATION.md +112 -0
  9. package/package.json +71 -0
  10. package/scaffold/.agent/skills/total-recall/SKILL.md +195 -0
  11. package/scaffold/.agent/skills/total-recall/evals/evals.json +20 -0
  12. package/scaffold/.agent/skills/total-recall/memory-vault/concepts/cli-help-reference.md +145 -0
  13. package/scaffold/.agent/skills/total-recall/memory-vault/invariants/operating-instructions.md +75 -0
  14. package/scaffold/.agent/skills/total-recall/memory-vault/preferences/always-websearch-gap.md +16 -0
  15. package/scaffold/.agent/skills/total-recall/memory-vault/preferences/topic-research-sop.md +17 -0
  16. package/scaffold/.agent/skills/total-recall/references/api-reference.md +187 -0
  17. package/scaffold/.agent/skills/total-recall/references/architecture-reference.md +81 -0
  18. package/scaffold/.agent/skills/total-recall/references/cli-reference.md +103 -0
  19. package/scaffold/.agent/skills/total-recall/references/ssss-reference.md +88 -0
  20. package/scaffold/.agent/skills/total-recall/references/troubleshooting.md +68 -0
  21. package/scaffold/.agent/skills/total-recall/scripts/sync-repo.mjs +83 -0
  22. package/scaffold/.agent/skills/total-recall/skills/cli-agents/SKILL.md +122 -0
  23. package/scaffold/.agent/skills/total-recall/skills/cli-agents/agents.yml +27 -0
  24. package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/antigravity.txt +20 -0
  25. package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/claude.txt +58 -0
  26. package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/codex.txt +81 -0
  27. package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/gemini.txt +44 -0
  28. package/scaffold/.agent/skills/total-recall/skills/research/SKILL.md +337 -0
  29. package/scaffold/.agent/skills/total-recall/skills/skill/SKILL.md +281 -0
  30. package/scaffold/.agent/skills/total-recall/skills/skill/assets/skill-template/SKILL.md +20 -0
  31. package/scaffold/.agent/skills/total-recall/skills/skill/evals/evals.json +20 -0
  32. package/scaffold/.agent/skills/total-recall/skills/skill/hooks/on-skill-add.sh +14 -0
  33. package/scaffold/.agent/skills/total-recall/skills/skill/hooks/on-skill-delete.sh +14 -0
  34. package/scaffold/.agent/skills/total-recall/skills/skill/learnings/.gitkeep +0 -0
  35. package/scaffold/.agent/skills/total-recall/skills/skill/references/.gitkeep +0 -0
  36. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/.gitkeep +0 -0
  37. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/create-skill.sh +65 -0
  38. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/enforce-skill-optimization.mjs +135 -0
  39. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/find-skills.mjs +124 -0
  40. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/scan-skill.mjs +207 -0
  41. package/scaffold/.agent/skills/total-recall/skills/skill/scripts/skill-watcher.mjs +64 -0
  42. package/scaffold/.agent/skills/total-recall/skills/skill/subagents/audit-all-skills.md +62 -0
  43. package/scaffold/.agent/skills/total-recall/skills/ssss/SKILL.md +27 -0
  44. package/scaffold/.agent/skills/total-recall/skills/ssss/evals/evals.json +17 -0
  45. package/scaffold/.agent/skills/total-recall/skills/ssss/references/ssss-spec.md +117 -0
  46. package/scaffold/.agent/skills/total-recall/skills/ssss/scripts/validate-schema.mjs +54 -0
  47. package/scaffold/.agent/skills/total-recall/skills/ssss/subagents/audit-ssss-node.md +9 -0
  48. package/scaffold/.agent/skills/total-recall/subagents/total-recall-diagnostician.md +13 -0
  49. package/src/cli/agent-dir.mjs +105 -0
  50. package/src/cli/backup.mjs +313 -0
  51. package/src/cli/brain.mjs +224 -0
  52. package/src/cli/chat.mjs +74 -0
  53. package/src/cli/config.mjs +247 -0
  54. package/src/cli/connect.mjs +872 -0
  55. package/src/cli/daemon.mjs +228 -0
  56. package/src/cli/deploy-ui.mjs +1793 -0
  57. package/src/cli/deploy.mjs +1726 -0
  58. package/src/cli/doctor.mjs +254 -0
  59. package/src/cli/dream.mjs +80 -0
  60. package/src/cli/forget.mjs +92 -0
  61. package/src/cli/friction.mjs +21 -0
  62. package/src/cli/generate-pat.mjs +65 -0
  63. package/src/cli/hash-password.mjs +22 -0
  64. package/src/cli/help.mjs +153 -0
  65. package/src/cli/import-rules.mjs +102 -0
  66. package/src/cli/ingest/index.mjs +318 -0
  67. package/src/cli/ingest/parsers/chrome-bookmarks.mjs +238 -0
  68. package/src/cli/ingest/parsers/google-keep.mjs +147 -0
  69. package/src/cli/ingest/parsers/search-history.mjs +142 -0
  70. package/src/cli/ingest/parsers/youtube-history.mjs +181 -0
  71. package/src/cli/ingest/utils/dedup.mjs +69 -0
  72. package/src/cli/ingest/utils/takeout-walker.mjs +101 -0
  73. package/src/cli/ingest.mjs +102 -0
  74. package/src/cli/init.mjs +736 -0
  75. package/src/cli/integration-dispatcher.mjs +128 -0
  76. package/src/cli/lint.mjs +166 -0
  77. package/src/cli/map.mjs +194 -0
  78. package/src/cli/migrate.mjs +87 -0
  79. package/src/cli/rebuild.mjs +88 -0
  80. package/src/cli/recall.mjs +219 -0
  81. package/src/cli/relay.mjs +518 -0
  82. package/src/cli/remember.mjs +306 -0
  83. package/src/cli/research.mjs +381 -0
  84. package/src/cli/reset-password.mjs +75 -0
  85. package/src/cli/restore.mjs +135 -0
  86. package/src/cli/setup.mjs +616 -0
  87. package/src/cli/share.mjs +180 -0
  88. package/src/cli/skill.mjs +313 -0
  89. package/src/cli/snapshot.mjs +108 -0
  90. package/src/cli/start.mjs +63 -0
  91. package/src/cli/status.mjs +296 -0
  92. package/src/cli/sync.mjs +383 -0
  93. package/src/cli/uninstall.mjs +613 -0
  94. package/src/cli/upgrade.mjs +141 -0
  95. package/src/cli/wizard.html +3682 -0
  96. package/src/core/blackboard.mjs +54 -0
  97. package/src/core/clarity-rewriter.mjs +770 -0
  98. package/src/core/conclusion-writer.mjs +259 -0
  99. package/src/core/config.mjs +277 -0
  100. package/src/core/conflict-detector.mjs +536 -0
  101. package/src/core/crypto.mjs +107 -0
  102. package/src/core/daemon-control.mjs +158 -0
  103. package/src/core/daemon-loop.mjs +364 -0
  104. package/src/core/dream.mjs +347 -0
  105. package/src/core/drift-detector.mjs +143 -0
  106. package/src/core/embeddings.mjs +632 -0
  107. package/src/core/emergency-alerts.mjs +137 -0
  108. package/src/core/evolution.mjs +107 -0
  109. package/src/core/fact-seeker.mjs +1602 -0
  110. package/src/core/friction.mjs +95 -0
  111. package/src/core/import-rules.mjs +308 -0
  112. package/src/core/inference-engine.mjs +367 -0
  113. package/src/core/logger.mjs +75 -0
  114. package/src/core/memory-layers.mjs +96 -0
  115. package/src/core/migrate.mjs +116 -0
  116. package/src/core/notifications.mjs +77 -0
  117. package/src/core/operation-validator.mjs +272 -0
  118. package/src/core/optimizer.mjs +330 -0
  119. package/src/core/pattern_detector.mjs +113 -0
  120. package/src/core/post-mortem.mjs +413 -0
  121. package/src/core/protect-instructions.mjs +148 -0
  122. package/src/core/quick-capture.mjs +145 -0
  123. package/src/core/research-queue.mjs +431 -0
  124. package/src/core/research.mjs +461 -0
  125. package/src/core/runtime.mjs +515 -0
  126. package/src/core/sandbox.mjs +163 -0
  127. package/src/core/scheduler.mjs +569 -0
  128. package/src/core/schema.mjs +333 -0
  129. package/src/core/search.mjs +219 -0
  130. package/src/core/semantic-index.mjs +276 -0
  131. package/src/core/session-watcher.mjs +850 -0
  132. package/src/core/snapshot.mjs +133 -0
  133. package/src/core/source-adapters.mjs +1038 -0
  134. package/src/core/steering.mjs +137 -0
  135. package/src/core/surface.mjs +444 -0
  136. package/src/core/task_runner.mjs +125 -0
  137. package/src/core/tts.mjs +142 -0
  138. package/src/core/usage-tracker.mjs +349 -0
  139. package/src/core/vault-cache.mjs +76 -0
  140. package/src/core/vault.mjs +273 -0
  141. package/src/core/watchdog.mjs +236 -0
  142. package/src/server/api.mjs +944 -0
  143. package/src/server/auth.mjs +410 -0
  144. package/src/server/index.mjs +868 -0
  145. package/src/server/keys.mjs +181 -0
  146. package/src/server/rest.mjs +1652 -0
  147. package/src/server/route-manifest.json +77 -0
  148. package/src/server/routes/_shared.mjs +93 -0
  149. package/src/server/routes/keys.mjs +55 -0
  150. package/src/server/routes/memory.mjs +249 -0
  151. package/src/server/routes/sessions.mjs +336 -0
  152. package/src/server/routes/share.mjs +138 -0
  153. package/src/server/tools.mjs +618 -0
  154. package/templates/Caddyfile +37 -0
  155. package/templates/com.totalrecall.backup.plist +42 -0
  156. package/templates/com.totalrecall.daemon.plist +40 -0
  157. package/templates/com.totalrecall.duckdns.plist +23 -0
  158. package/templates/com.totalrecall.server.plist +46 -0
  159. package/templates/default-config/budget.yml +6 -0
  160. package/templates/default-config/runtime.yml +11 -0
  161. package/templates/default-config/security.yml +53 -0
  162. package/templates/default-config/voice.yml +34 -0
  163. package/templates/obsidian-queries/conflicts.md +26 -0
  164. package/templates/obsidian-queries/daily.md +17 -0
  165. package/templates/obsidian-queries/index.md +38 -0
  166. package/templates/obsidian-queries/skills.md +14 -0
  167. package/templates/onboarding-interview.md +72 -0
  168. package/templates/skill_engineering.md +40 -0
  169. package/templates/total-recall-daemon.service +21 -0
  170. package/templates/total-recall-server.service +20 -0
@@ -0,0 +1,616 @@
1
+ /**
2
+ * total-recall setup
3
+ *
4
+ * Interactive first-time setup wizard:
5
+ * 1. Ask what provider (Hetzner / DigitalOcean / RunPod / own server / local)
6
+ * 2. Collect API key with masked input
7
+ * 3. Provision the server via provider API
8
+ * 4. SSH in and run deploy
9
+ * 5. Ask what IDEs / chat apps to connect
10
+ * 6. Run connect for each
11
+ *
12
+ * Usage:
13
+ * npx total-recall setup [--yes] [--dry-run]
14
+ */
15
+
16
+ import readline from 'node:readline';
17
+ import { execSync, spawnSync } from 'node:child_process';
18
+ import fs from 'node:fs';
19
+ import os from 'node:os';
20
+ import path from 'node:path';
21
+ import https from 'node:https';
22
+ import { fileURLToPath } from 'node:url';
23
+ import { resolveAgentDir, resolveBrainDir } from './agent-dir.mjs';
24
+
25
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
26
+
27
+ // ─── UI helpers ─────────────────────────────────────────────────────────────
28
+
29
+ function box(title, body = '') {
30
+ const line = '─'.repeat(58);
31
+ console.log(`\n ┌${line}┐`);
32
+ console.log(` │ ${title.padEnd(56)}│`);
33
+ if (body) console.log(` │ ${body.padEnd(56)}│`);
34
+ console.log(` └${line}┘`);
35
+ }
36
+
37
+ function hr() {
38
+ console.log('\n ' + '─'.repeat(58));
39
+ }
40
+
41
+ function ok(msg) { console.log(` ✅ ${msg}`); }
42
+ function info(msg) { console.log(` ℹ ${msg}`); }
43
+ function warn(msg) { console.log(` ⚠️ ${msg}`); }
44
+ function step(n, total, msg) { console.log(`\n [${n}/${total}] ${msg}`); }
45
+
46
+ // ─── Prompt helpers ──────────────────────────────────────────────────────────
47
+
48
+ function prompt(question) {
49
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
50
+ return new Promise(resolve => {
51
+ rl.question(` ${question} `, answer => {
52
+ rl.close();
53
+ resolve(answer.trim());
54
+ });
55
+ });
56
+ }
57
+
58
+ function promptSecret(question) {
59
+ return new Promise(resolve => {
60
+ process.stdout.write(` ${question} `);
61
+ const rl = readline.createInterface({ input: process.stdin, output: null });
62
+ // Disable echo
63
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
64
+ let input = '';
65
+ process.stdin.resume();
66
+ process.stdin.setEncoding('utf8');
67
+
68
+ const handler = (char) => {
69
+ if (char === '\r' || char === '\n') {
70
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
71
+ process.stdin.removeListener('data', handler);
72
+ process.stdin.pause();
73
+ rl.close();
74
+ process.stdout.write('\n');
75
+ resolve(input);
76
+ } else if (char === '') { // Ctrl+C
77
+ process.exit(0);
78
+ } else if (char === '' || char === '\b') { // Backspace
79
+ if (input.length > 0) {
80
+ input = input.slice(0, -1);
81
+ process.stdout.write('\b \b');
82
+ }
83
+ } else {
84
+ input += char;
85
+ process.stdout.write('*');
86
+ }
87
+ };
88
+ process.stdin.on('data', handler);
89
+ });
90
+ }
91
+
92
+ async function choose(question, options) {
93
+ console.log(`\n ${question}`);
94
+ options.forEach((opt, i) => {
95
+ const label = opt.label || opt;
96
+ const desc = opt.desc ? ` — ${opt.desc}` : '';
97
+ console.log(` ${i + 1}) ${label}${desc}`);
98
+ });
99
+ while (true) {
100
+ const answer = await prompt(`Choice [1-${options.length}]:`);
101
+ const n = parseInt(answer, 10);
102
+ if (n >= 1 && n <= options.length) return options[n - 1];
103
+ warn(`Please enter a number between 1 and ${options.length}.`);
104
+ }
105
+ }
106
+
107
+ async function chooseMultiple(question, options) {
108
+ console.log(`\n ${question}`);
109
+ console.log(` (enter comma-separated numbers, or 'none')`);
110
+ options.forEach((opt, i) => {
111
+ console.log(` ${i + 1}) ${opt.label || opt}`);
112
+ });
113
+ const answer = await prompt(`Choices:`);
114
+ if (!answer || answer.toLowerCase() === 'none') return [];
115
+ const nums = answer.split(',').map(s => parseInt(s.trim(), 10)).filter(n => n >= 1 && n <= options.length);
116
+ return [...new Set(nums)].map(n => options[n - 1]);
117
+ }
118
+
119
+ // ─── HTTP helper ─────────────────────────────────────────────────────────────
120
+
121
+ function httpRequest(method, url, { headers = {}, body = null } = {}) {
122
+ return new Promise((resolve, reject) => {
123
+ const u = new URL(url);
124
+ const opts = {
125
+ hostname: u.hostname,
126
+ port: u.port || 443,
127
+ path: u.pathname + u.search,
128
+ method,
129
+ headers: { 'Content-Type': 'application/json', ...headers },
130
+ };
131
+ if (body) {
132
+ const bodyStr = JSON.stringify(body);
133
+ opts.headers['Content-Length'] = Buffer.byteLength(bodyStr);
134
+ }
135
+ const req = https.request(opts, (res) => {
136
+ let data = '';
137
+ res.on('data', chunk => data += chunk);
138
+ res.on('end', () => {
139
+ try { resolve({ status: res.statusCode, body: JSON.parse(data) }); }
140
+ catch { resolve({ status: res.statusCode, body: data }); }
141
+ });
142
+ });
143
+ req.on('error', reject);
144
+ if (body) req.write(JSON.stringify(body));
145
+ req.end();
146
+ });
147
+ }
148
+
149
+ // ─── SSH wait helper ─────────────────────────────────────────────────────────
150
+
151
+ async function waitForSSH(ip, user = 'root', maxWait = 120) {
152
+ info(`Waiting for SSH to come up at ${ip}...`);
153
+ const start = Date.now();
154
+ while ((Date.now() - start) / 1000 < maxWait) {
155
+ const result = spawnSync('ssh', [
156
+ '-o', 'StrictHostKeyChecking=no',
157
+ '-o', 'ConnectTimeout=5',
158
+ '-o', 'BatchMode=yes',
159
+ `${user}@${ip}`, 'echo ok'
160
+ ], { stdio: 'pipe' });
161
+ if (result.status === 0) { ok('SSH ready.'); return true; }
162
+ await new Promise(r => setTimeout(r, 5000));
163
+ process.stdout.write('.');
164
+ }
165
+ throw new Error(`SSH did not come up within ${maxWait}s`);
166
+ }
167
+
168
+ // ─── Provider: Hetzner ───────────────────────────────────────────────────────
169
+
170
+ async function provisionHetzner(apiKey, opts = {}) {
171
+ const { serverType = 'cx42', location = 'nbg1', model = 'gemma4:26b' } = opts;
172
+
173
+ // Get SSH key IDs
174
+ let sshKeys = [];
175
+ const keysRes = await httpRequest('GET', 'https://api.hetzner.cloud/v1/ssh_keys', {
176
+ headers: { Authorization: `Bearer ${apiKey}` }
177
+ });
178
+ if (keysRes.status === 200 && keysRes.body.ssh_keys?.length > 0) {
179
+ sshKeys = keysRes.body.ssh_keys.map(k => k.id);
180
+ info(`Found ${sshKeys.length} SSH key(s) in your Hetzner account.`);
181
+ } else {
182
+ warn('No SSH keys found in your Hetzner account.');
183
+ warn('Upload your public key at: https://console.hetzner.cloud → Security → SSH Keys');
184
+ const cont = await prompt('Press Enter to continue anyway, or Ctrl+C to abort:');
185
+ }
186
+
187
+ info(`Creating Hetzner ${serverType} in ${location}...`);
188
+ const res = await httpRequest('POST', 'https://api.hetzner.cloud/v1/servers', {
189
+ headers: { Authorization: `Bearer ${apiKey}` },
190
+ body: {
191
+ name: 'total-recall-brain',
192
+ server_type: serverType,
193
+ image: 'ubuntu-24.04',
194
+ location,
195
+ ssh_keys: sshKeys,
196
+ labels: { project: 'total-recall' },
197
+ }
198
+ });
199
+
200
+ if (res.status !== 201) {
201
+ throw new Error(`Hetzner API error (${res.status}): ${JSON.stringify(res.body)}`);
202
+ }
203
+
204
+ const server = res.body.server;
205
+ const ip = server.public_net.ipv4.ip;
206
+ ok(`Server created! IP: ${ip} (ID: ${server.id})`);
207
+ info('It takes ~30 seconds for the server to boot.');
208
+
209
+ await waitForSSH(ip);
210
+ return { ip, user: 'root' };
211
+ }
212
+
213
+ // ─── Provider: DigitalOcean ──────────────────────────────────────────────────
214
+
215
+ async function provisionDO(apiKey, opts = {}) {
216
+ const { size = 's-8vcpu-16gb', region = 'nyc3' } = opts;
217
+
218
+ // Get SSH key IDs
219
+ let sshKeyIds = [];
220
+ const keysRes = await httpRequest('GET', 'https://api.digitalocean.com/v2/account/keys', {
221
+ headers: { Authorization: `Bearer ${apiKey}` }
222
+ });
223
+ if (keysRes.status === 200 && keysRes.body.ssh_keys?.length > 0) {
224
+ sshKeyIds = keysRes.body.ssh_keys.map(k => k.id);
225
+ info(`Found ${sshKeyIds.length} SSH key(s) in your DigitalOcean account.`);
226
+ } else {
227
+ warn('No SSH keys found. Add one at: https://cloud.digitalocean.com/account/security');
228
+ }
229
+
230
+ info(`Creating DigitalOcean ${size} droplet in ${region}...`);
231
+ const res = await httpRequest('POST', 'https://api.digitalocean.com/v2/droplets', {
232
+ headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
233
+ body: {
234
+ name: 'total-recall-brain',
235
+ region,
236
+ size,
237
+ image: 'ubuntu-24-04-x64',
238
+ ssh_keys: sshKeyIds,
239
+ monitoring: true,
240
+ tags: ['total-recall'],
241
+ }
242
+ });
243
+
244
+ if (res.status !== 202) {
245
+ throw new Error(`DigitalOcean API error (${res.status}): ${JSON.stringify(res.body)}`);
246
+ }
247
+
248
+ const dropletId = res.body.droplet.id;
249
+ info(`Droplet created (ID: ${dropletId}). Waiting for IP...`);
250
+
251
+ // Poll for IP
252
+ let ip = null;
253
+ for (let i = 0; i < 30; i++) {
254
+ await new Promise(r => setTimeout(r, 5000));
255
+ const poll = await httpRequest('GET', `https://api.digitalocean.com/v2/droplets/${dropletId}`, {
256
+ headers: { Authorization: `Bearer ${apiKey}` }
257
+ });
258
+ const v4 = poll.body.droplet?.networks?.v4 || [];
259
+ const pub = v4.find(n => n.type === 'public');
260
+ if (pub) { ip = pub.ip_address; break; }
261
+ process.stdout.write('.');
262
+ }
263
+
264
+ if (!ip) throw new Error('Could not get droplet IP after 150s');
265
+ ok(`Droplet ready! IP: ${ip}`);
266
+
267
+ await waitForSSH(ip);
268
+ return { ip, user: 'root' };
269
+ }
270
+
271
+ // ─── Remote deploy ────────────────────────────────────────────────────────────
272
+
273
+ function sshRun(ip, user, cmd, { silent = false } = {}) {
274
+ const result = spawnSync('ssh', [
275
+ '-o', 'StrictHostKeyChecking=no',
276
+ `${user}@${ip}`,
277
+ cmd
278
+ ], { stdio: silent ? 'pipe' : 'inherit' });
279
+ if (result.status !== 0 && !silent) {
280
+ throw new Error(`SSH command failed: ${cmd}`);
281
+ }
282
+ return result.stdout?.toString().trim() || '';
283
+ }
284
+
285
+ async function remoteSetup(ip, user, opts = {}) {
286
+ const { domain, model = 'gemma4:26b' } = opts;
287
+
288
+ info(`Setting up Total Recall on ${ip}...`);
289
+
290
+ // Install Node.js 20 via nvm
291
+ step(1, 3, 'Installing Node.js...');
292
+ sshRun(ip, user,
293
+ 'curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh -o /tmp/install_nvm.sh && bash /tmp/install_nvm.sh && rm /tmp/install_nvm.sh && ' +
294
+ 'source ~/.bashrc && nvm install 20 2>/dev/null || ' +
295
+ '(curl -fsSL https://deb.nodesource.com/setup_20.x -o /tmp/setup_node.sh && bash /tmp/setup_node.sh && rm /tmp/setup_node.sh && apt-get install -y nodejs)'
296
+ );
297
+
298
+ // Run total-recall deploy
299
+ step(2, 3, 'Running total-recall deploy...');
300
+ const deployCmd = domain
301
+ ? `npx --yes total-recall deploy --domain ${domain}`
302
+ : 'npx --yes total-recall deploy';
303
+ sshRun(ip, user, `export NVM_DIR="$HOME/.nvm" && source "$NVM_DIR/nvm.sh" && ${deployCmd}`);
304
+
305
+ // Generate PAT
306
+ step(3, 3, 'Generating access token...');
307
+ const pat = sshRun(ip, user,
308
+ 'export NVM_DIR="$HOME/.nvm" && source "$NVM_DIR/nvm.sh" && npx total-recall generate-pat --quiet',
309
+ { silent: true }
310
+ );
311
+
312
+ ok('Remote setup complete!');
313
+ return { pat: pat || null };
314
+ }
315
+
316
+ // ─── GitHub fork ─────────────────────────────────────────────────────────────
317
+
318
+ const UPSTREAM_OWNER = 'gregiteen';
319
+ const UPSTREAM_REPO = 'total-recall';
320
+
321
+ async function forkRepo(githubToken) {
322
+ info('Forking gregiteen/total-recall to your GitHub account...');
323
+ const res = await httpRequest(
324
+ 'POST',
325
+ `https://api.github.com/repos/${UPSTREAM_OWNER}/${UPSTREAM_REPO}/forks`,
326
+ {
327
+ headers: {
328
+ Authorization: `Bearer ${githubToken}`,
329
+ 'User-Agent': 'total-recall-setup',
330
+ Accept: 'application/vnd.github+json',
331
+ },
332
+ }
333
+ );
334
+ if (res.status !== 202 && res.status !== 200) {
335
+ throw new Error(`GitHub fork failed (${res.status}): ${JSON.stringify(res.body)}`);
336
+ }
337
+ const fork = res.body;
338
+ ok(`Forked → ${fork.html_url}`);
339
+ // GitHub forks are async — wait a moment for it to be clonable
340
+ await new Promise(r => setTimeout(r, 4000));
341
+ return { cloneUrl: fork.clone_url, htmlUrl: fork.html_url, fullName: fork.full_name };
342
+ }
343
+
344
+ async function getGitHubUser(githubToken) {
345
+ const res = await httpRequest('GET', 'https://api.github.com/user', {
346
+ headers: {
347
+ Authorization: `Bearer ${githubToken}`,
348
+ 'User-Agent': 'total-recall-setup',
349
+ Accept: 'application/vnd.github+json',
350
+ },
351
+ });
352
+ if (res.status !== 200) return null;
353
+ return res.body;
354
+ }
355
+
356
+ // ─── Store brain config ───────────────────────────────────────────────────────
357
+
358
+ function storeBrainConfig(brDir, brainUrl, token, provider, apiKey, extra = {}) {
359
+ const configDir = path.join(brDir, 'config');
360
+ fs.mkdirSync(configDir, { recursive: true });
361
+
362
+ // brain.json
363
+ fs.writeFileSync(
364
+ path.join(configDir, 'brain.json'),
365
+ JSON.stringify({ url: brainUrl, token, provider, ...extra }, null, 2),
366
+ 'utf8'
367
+ );
368
+
369
+ // secrets.enc (plaintext for now — encrypt in future)
370
+ const secretsPath = path.join(configDir, 'secrets.enc');
371
+ let secrets = {};
372
+ if (fs.existsSync(secretsPath)) {
373
+ try { secrets = JSON.parse(fs.readFileSync(secretsPath, 'utf8')); } catch {}
374
+ }
375
+ if (apiKey) secrets[`${provider}_api_key`] = apiKey;
376
+ fs.writeFileSync(secretsPath, JSON.stringify(secrets, null, 2), { encoding: 'utf8', mode: 0o600 });
377
+ }
378
+
379
+ // ─── Main ─────────────────────────────────────────────────────────────────────
380
+
381
+ export default async function setup(args) {
382
+ const dryRun = args.includes('--dry-run');
383
+ const yes = args.includes('--yes');
384
+
385
+ box('Total Recall Setup', 'Interactive first-time configuration wizard');
386
+
387
+ if (dryRun) {
388
+ warn('Dry-run mode: no changes will be made.');
389
+ }
390
+
391
+ const agentDir = resolveAgentDir();
392
+ const brDir = resolveBrainDir();
393
+
394
+ // ── Step 1: Where are you running? ──────────────────────────────────────────
395
+ console.log('\n Where do you want to run Total Recall?\n');
396
+ console.log(' The full brain (OpenAI-compatible API, dashboard, Dream Cycle) needs a');
397
+ console.log(' server running 24/7 with at least 16 GB RAM for the default model.\n');
398
+
399
+ const deployment = await choose('Choose your deployment:', [
400
+ { label: 'Hetzner Cloud', desc: '⭐ CX42 €18/mo, 16GB RAM — cheapest always-on', value: 'hetzner' },
401
+ { label: 'DigitalOcean', desc: 'Droplet from $96/mo, easiest setup', value: 'do' },
402
+ { label: 'I have a server', desc: 'SSH into your own VM or bare metal', value: 'ssh' },
403
+ { label: 'Local only', desc: 'Run everything on this machine (no cloud)', value: 'local' },
404
+ { label: 'IDE memory only', desc: 'Just memory nodes in your IDE, no server', value: 'ide-only' },
405
+ ]);
406
+
407
+ const deployTarget = deployment.value || deployment;
408
+ let ip = null, user = 'root', apiKey = null, brainUrl = null, pat = null;
409
+ let domain = null;
410
+ let forkUrl = null, forkFullName = null;
411
+
412
+ // ── Step 1.5: GitHub fork (free backup + multi-machine sync) ─────────────────
413
+
414
+ hr();
415
+ console.log('\n 📦 GitHub Backup (recommended)');
416
+ console.log(' Fork total-recall to your GitHub account to get:');
417
+ console.log(' • Free encrypted backup of your brain vault');
418
+ console.log(' • Multi-machine sync via git pull');
419
+ console.log(' • Full version history of every memory change\n');
420
+
421
+ const wantGitHub = yes ? 'y' : (await prompt('Set up GitHub backup? [Y/n]:')).toLowerCase();
422
+ if (wantGitHub !== 'n' && wantGitHub !== 'no') {
423
+ console.log('\n Create a GitHub token at: https://github.com/settings/tokens/new');
424
+ console.log(' Required scopes: repo (for private forks) or public_repo (for public)\n');
425
+ const githubToken = await promptSecret('GitHub Personal Access Token:');
426
+ if (githubToken) {
427
+ const ghUser = await getGitHubUser(githubToken).catch(() => null);
428
+ if (ghUser) {
429
+ info(`Authenticated as @${ghUser.login}`);
430
+ if (!dryRun) {
431
+ try {
432
+ const fork = await forkRepo(githubToken);
433
+ forkUrl = fork.cloneUrl;
434
+ forkFullName = fork.fullName;
435
+ ok(`Your fork: ${fork.htmlUrl}`);
436
+ ok('Run `npx total-recall sync --push` anytime to back up your vault to GitHub.');
437
+ } catch (e) {
438
+ warn(`Could not fork: ${e.message}`);
439
+ warn('You can fork manually at https://github.com/gregiteen/total-recall');
440
+ }
441
+ } else {
442
+ info('[dry-run] would fork gregiteen/total-recall to your account');
443
+ }
444
+ // Store GitHub token for sync --push
445
+ const secretsPath = path.join(resolveBrainDir(), 'config', 'secrets.enc');
446
+ fs.mkdirSync(path.dirname(secretsPath), { recursive: true });
447
+ let secrets = {};
448
+ if (fs.existsSync(secretsPath)) { try { secrets = JSON.parse(fs.readFileSync(secretsPath, 'utf8')); } catch {} }
449
+ secrets.github_token = githubToken;
450
+ if (!dryRun) fs.writeFileSync(secretsPath, JSON.stringify(secrets, null, 2), { encoding: 'utf8', mode: 0o600 });
451
+ } else {
452
+ warn('Could not authenticate with that token — skipping GitHub backup.');
453
+ }
454
+ } else {
455
+ info('Skipping GitHub backup. You can set it up later with: npx total-recall connect github');
456
+ }
457
+ }
458
+
459
+ // ── Step 2: Provider-specific provisioning ──────────────────────────────────
460
+
461
+ if (deployTarget === 'hetzner') {
462
+ hr();
463
+ console.log('\n Hetzner Cloud — Create a project at https://console.hetzner.cloud');
464
+ console.log(' Then: Project → Security → API Tokens → Generate API Token (Read & Write)\n');
465
+ apiKey = await promptSecret('Hetzner API Token:');
466
+ if (!apiKey) { warn('No token provided. Exiting.'); process.exit(1); }
467
+
468
+ const sizeChoice = await choose('Server type:', [
469
+ { label: 'CX42 (16 GB, 8 vCPU, ~€18/mo) ⭐', value: 'cx42', desc: 'runs gemma4:26b' },
470
+ { label: 'CX52 (32 GB, 16 vCPU, ~€32/mo)', value: 'cx52', desc: 'runs gemma4:31b' },
471
+ { label: 'AX42 (64 GB, 12 ded. CPU, ~€46/mo)', value: 'ax42', desc: 'best value overall' },
472
+ ]);
473
+
474
+ const regionChoice = await choose('Region:', [
475
+ { label: 'Nuremberg, Germany (nbg1)', value: 'nbg1' },
476
+ { label: 'Helsinki, Finland (hel1)', value: 'hel1' },
477
+ { label: 'Ashburn, US (ash)', value: 'ash' },
478
+ ]);
479
+
480
+ if (!dryRun) {
481
+ const result = await provisionHetzner(apiKey, {
482
+ serverType: sizeChoice.value,
483
+ location: regionChoice.value,
484
+ });
485
+ ip = result.ip; user = result.user;
486
+ }
487
+
488
+ } else if (deployTarget === 'do') {
489
+ hr();
490
+ console.log('\n DigitalOcean — Create a token at https://cloud.digitalocean.com/account/api/tokens\n');
491
+ apiKey = await promptSecret('DigitalOcean API Token:');
492
+ if (!apiKey) { warn('No token provided. Exiting.'); process.exit(1); }
493
+
494
+ const sizeChoice = await choose('Droplet size:', [
495
+ { label: 's-8vcpu-16gb ($96/mo, 16 GB)', value: 's-8vcpu-16gb', desc: 'runs gemma4:26b' },
496
+ { label: 's-8vcpu-32gb-amd ($160/mo, 32 GB)', value: 's-8vcpu-32gb-amd', desc: 'runs gemma4:31b' },
497
+ ]);
498
+
499
+ if (!dryRun) {
500
+ const result = await provisionDO(apiKey, { size: sizeChoice.value });
501
+ ip = result.ip; user = result.user;
502
+ }
503
+
504
+ } else if (deployTarget === 'ssh') {
505
+ hr();
506
+ ip = await prompt('Server IP or hostname:');
507
+ user = await prompt('SSH user [root]:') || 'root';
508
+ if (!ip) { warn('No IP provided. Exiting.'); process.exit(1); }
509
+ if (!dryRun) await waitForSSH(ip, user, 30);
510
+
511
+ } else if (deployTarget === 'local') {
512
+ info('Running deploy locally on this machine...');
513
+ if (!dryRun) {
514
+ const result = spawnSync('node', [
515
+ path.join(__dirname, '..', '..', 'bin', 'total-recall.mjs'),
516
+ 'deploy'
517
+ ], { stdio: 'inherit' });
518
+ if (result.status !== 0) throw new Error('Local deploy failed');
519
+ ok('Local deploy complete!');
520
+ }
521
+ brainUrl = 'http://127.0.0.1:3000';
522
+ }
523
+
524
+ // ── Step 3: Domain (optional) ────────────────────────────────────────────────
525
+
526
+ if (ip && deployTarget !== 'local' && deployTarget !== 'ide-only') {
527
+ hr();
528
+ console.log('\n Optional: a domain name enables HTTPS via Caddy auto-TLS.');
529
+ console.log(` Without a domain, your brain will be at http://${ip}:3000\n`);
530
+ domain = await prompt('Domain (leave blank for none):');
531
+ brainUrl = domain ? `https://${domain}` : `http://${ip}:3000`;
532
+ }
533
+
534
+ // ── Step 4: Remote deploy ────────────────────────────────────────────────────
535
+
536
+ if (ip && !dryRun) {
537
+ hr();
538
+ const result = await remoteSetup(ip, user, { domain });
539
+ pat = result.pat;
540
+ ok(`Brain deployed at: ${brainUrl}`);
541
+ if (pat) ok(`Access token: ${pat}`);
542
+ }
543
+
544
+ // ── Step 5: Store config locally ─────────────────────────────────────────────
545
+
546
+ if (brainUrl && !dryRun) {
547
+ storeBrainConfig(brDir, brainUrl, pat, deployTarget, apiKey);
548
+ ok(`Brain config saved to ${path.join(brDir, 'config', 'brain.json')}`);
549
+ }
550
+
551
+ // ── Step 6: Connect IDEs ─────────────────────────────────────────────────────
552
+
553
+ if (deployTarget !== 'ide-only') {
554
+ hr();
555
+ }
556
+
557
+ const ideOptions = [
558
+ { label: 'Claude Code', value: 'claude-code' },
559
+ { label: 'Codex', value: 'codex' },
560
+ { label: 'Antigravity', value: 'antigravity' },
561
+ { label: 'Gemini CLI', value: 'gemini' },
562
+ { label: 'Cursor', value: 'cursor' },
563
+ { label: 'Aider', value: 'aider' },
564
+ { label: 'Obsidian', value: 'obsidian' },
565
+ { label: 'UltraChat', value: 'ultrachat' },
566
+ ];
567
+
568
+ const selectedIDEs = await chooseMultiple(
569
+ 'Which IDEs / apps do you use? (connect them to your brain)',
570
+ ideOptions
571
+ );
572
+
573
+ if (selectedIDEs.length > 0) {
574
+ hr();
575
+ info('Connecting IDEs...');
576
+ const connectArgs = brainUrl ? ['--brain', brainUrl] : [];
577
+ if (pat) connectArgs.push('--token', pat);
578
+
579
+ for (const ide of selectedIDEs) {
580
+ const client = ide.value || ide;
581
+ const extraArgs = [...connectArgs];
582
+ if (dryRun) {
583
+ info(`[dry-run] would run: total-recall connect ${client} ${extraArgs.join(' ')}`);
584
+ } else {
585
+ const connectHandler = await import(path.join(__dirname, 'connect.mjs'));
586
+ try {
587
+ await connectHandler.default([client, ...extraArgs]);
588
+ } catch (e) {
589
+ warn(`Could not connect ${client}: ${e.message}`);
590
+ }
591
+ }
592
+ }
593
+ }
594
+
595
+ // ── Done ─────────────────────────────────────────────────────────────────────
596
+
597
+ hr();
598
+ console.log('\n ✅ Total Recall setup complete!\n');
599
+
600
+ if (brainUrl) {
601
+ console.log(` Brain URL: ${brainUrl}`);
602
+ if (pat) console.log(` Access token: ${pat}`);
603
+ console.log('');
604
+ console.log(' OpenAI-compatible config for any app:');
605
+ console.log(` baseURL: ${brainUrl}/v1`);
606
+ console.log(` model: total-recall/gemma4`);
607
+ if (pat) console.log(` apiKey: ${pat}`);
608
+ }
609
+
610
+ console.log('\n Next steps:');
611
+ console.log(' • Add memory: edit files in ~/.agent/skills/total-recall/memory-vault/ then run: npx total-recall compile');
612
+ console.log(' • Check status: npx total-recall status');
613
+ console.log(' • Start Dream Cycle: npx total-recall daemon start');
614
+ console.log(' • Full CLI: npx total-recall --help');
615
+ console.log('');
616
+ }