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,736 @@
1
+ /**
2
+ * total-recall init
3
+ *
4
+ * Bootstrap Total Recall with a layered brain architecture.
5
+ *
6
+ * Two modes:
7
+ * 1. `npx total-recall init` (default) — creates the GLOBAL brain at
8
+ * ~/.agent/skills/total-recall/. This holds identity: universal
9
+ * preferences, invariants, corrections, coding principles.
10
+ *
11
+ * 2. `npx total-recall init --project` — creates a PROJECT brain at
12
+ * <cwd>/.agent/skills/total-recall/. This holds project-specific
13
+ * knowledge: facts, decisions, architecture patterns.
14
+ * Also registers the project in the global brain's project registry.
15
+ *
16
+ * Both modes seed core skills, default memory nodes, and compile
17
+ * IDE instruction files (GEMINI.md, .cursorrules, CLAUDE.md, etc.)
18
+ *
19
+ * Usage:
20
+ * npx total-recall init [options]
21
+ *
22
+ * Options:
23
+ * --project Create a project-level brain in the current directory
24
+ * --dry-run Print what would be done without making changes
25
+ * --help Show this help
26
+ */
27
+
28
+ import fs from 'node:fs';
29
+ import path from 'node:path';
30
+ import os from 'node:os';
31
+ import { fileURLToPath } from 'node:url';
32
+ import { exec, spawn, spawnSync } from 'node:child_process';
33
+
34
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
35
+ const ROOT = path.join(__dirname, '..', '..');
36
+
37
+ // ─── Helpers ────────────────────────────────────────────────────────────────
38
+
39
+ function log(msg) { console.error(` ${msg}`); }
40
+ function logStep(n, msg) { console.error(`\n [${n}] ${msg}`); }
41
+ function logOk(msg) { console.error(` ✅ ${msg}`); }
42
+ function logSkip(msg) { console.error(` ⏭ ${msg} (already exists)`); }
43
+ function logWarn(msg) { console.error(` ⚠️ ${msg}`); }
44
+
45
+ function ensureDir(dirPath, dryRun) {
46
+ if (fs.existsSync(dirPath)) return false;
47
+ if (!dryRun) fs.mkdirSync(dirPath, { recursive: true });
48
+ return true;
49
+ }
50
+
51
+ function copyFile(src, dest, dryRun) {
52
+ if (fs.existsSync(dest)) {
53
+ logSkip(path.basename(dest));
54
+ return false;
55
+ }
56
+ if (!dryRun) fs.copyFileSync(src, dest);
57
+ logOk(`Installed ${path.relative(process.cwd(), dest)}`);
58
+ return true;
59
+ }
60
+
61
+ function copyDirMerge(src, dest, dryRun) {
62
+ if (!fs.existsSync(src)) return;
63
+ ensureDir(dest, dryRun);
64
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
65
+ const srcPath = path.join(src, entry.name);
66
+ const destPath = path.join(dest, entry.name);
67
+ if (entry.isDirectory()) {
68
+ copyDirMerge(srcPath, destPath, dryRun);
69
+ } else {
70
+ copyFile(srcPath, destPath, dryRun);
71
+ }
72
+ }
73
+ }
74
+
75
+ function parseArgs(args) {
76
+ const opts = {
77
+ dryRun: false,
78
+ help: false,
79
+ brain: null,
80
+ token: null,
81
+ project: false,
82
+ deployMode: null,
83
+ domain: null,
84
+ tunnelName: null,
85
+ tunnelCredentials: null,
86
+ yes: false
87
+ };
88
+ for (let i = 0; i < args.length; i++) {
89
+ const arg = args[i];
90
+ if (arg === '--dry-run') opts.dryRun = true;
91
+ else if (arg === '--help' || arg === '-h') opts.help = true;
92
+ else if (arg === '--project') opts.project = true;
93
+ else if (arg === '--brain') opts.brain = args[++i];
94
+ else if (arg === '--token') opts.token = args[++i];
95
+ else if (arg === '--deploy-mode') opts.deployMode = args[++i];
96
+ else if (arg === '--domain') opts.domain = args[++i];
97
+ else if (arg === '--tunnel-name') opts.tunnelName = args[++i];
98
+ else if (arg === '--tunnel-credentials') opts.tunnelCredentials = args[++i];
99
+ else if (arg === '--yes' || arg === '-y') opts.yes = true;
100
+ }
101
+ return opts;
102
+ }
103
+
104
+ function printHelp() {
105
+ console.log(`
106
+ total-recall init — Bootstrap Total Recall with layered brain architecture
107
+
108
+ Usage: total-recall init [options]
109
+
110
+ Modes:
111
+ (default) Create the GLOBAL brain at ~/.agent/skills/total-recall/
112
+ Holds: universal preferences, invariants, corrections, coding principles
113
+ --project Create a PROJECT brain at <cwd>/.agent/skills/total-recall/
114
+ Holds: project-specific facts, decisions, architecture patterns
115
+ Also registers the project in the global brain's project registry.
116
+
117
+ Options:
118
+ --deploy-mode <mode> Set the UI deploy mode (local | quick-tunnel | named-tunnel | custom-domain)
119
+ --domain <domain> Domain for custom-domain/named-tunnel mode
120
+ --tunnel-name <name> Name of the Cloudflare tunnel for named-tunnel mode
121
+ --tunnel-credentials <path> Path to credentials JSON for named-tunnel mode
122
+ --yes, -y Skip interactive prompts and use defaults
123
+ --dry-run Print what would be done without making changes
124
+ --help, -h Show this help
125
+
126
+ Examples:
127
+ npx total-recall init # Global brain (run once)
128
+ npx total-recall init --project # Project brain (run per project)
129
+ `);
130
+ }
131
+
132
+ // ─── Main ───────────────────────────────────────────────────────────────────
133
+
134
+ export default async function init(args) {
135
+ const opts = parseArgs(args);
136
+ if (opts.help) { printHelp(); return; }
137
+
138
+ const cwd = process.cwd();
139
+ const globalAgentDir = path.join(os.homedir(), '.agent');
140
+ const globalBrainDir = path.join(globalAgentDir, 'skills', 'total-recall');
141
+
142
+ // Determine target based on --project flag
143
+ const isProject = opts.project;
144
+ const agentDir = isProject ? path.join(cwd, '.agent') : globalAgentDir;
145
+ const brainDir = isProject ? path.join(agentDir, 'skills', 'total-recall') : globalBrainDir;
146
+ const layerLabel = isProject ? 'PROJECT' : 'GLOBAL';
147
+ const targetPath = isProject ? cwd : os.homedir();
148
+ let passwordMessage = "";
149
+
150
+ console.error(`
151
+ ┌─────────────────────────────────────────────────────────┐
152
+ │ Total Recall Init (${layerLabel.padEnd(7)}) │
153
+ │ Bootstrapping into: ${targetPath.slice(0, 36).padEnd(36)} │
154
+ └─────────────────────────────────────────────────────────┘
155
+ `);
156
+
157
+ if (opts.dryRun) logWarn('DRY RUN — no changes will be made\n');
158
+
159
+ // ── Step 1: Create .agent/ directory layout ──
160
+ logStep('1/4', 'Creating .agent/ directory structure');
161
+
162
+ const dirs = [
163
+ path.join(agentDir, 'skills'),
164
+ ];
165
+
166
+ let created = 0;
167
+ for (const dir of dirs) {
168
+ const wasCreated = ensureDir(dir, opts.dryRun);
169
+ if (wasCreated) {
170
+ if (opts.dryRun) log(` mkdir ${path.relative(cwd, dir)}`);
171
+ created++;
172
+ }
173
+ }
174
+ logOk(`Base directory structure ready (${created} created, ${dirs.length - created} already existed)`);
175
+
176
+ // ── Step 2: Seed core skills ──
177
+ logStep('2/4', 'Installing core skills into .agent/skills/');
178
+
179
+ const scaffoldSkillsDir = path.join(ROOT, 'scaffold', '.agent', 'skills');
180
+ let skillsToSeed = ['total-recall'];
181
+ if (fs.existsSync(scaffoldSkillsDir)) {
182
+ try {
183
+ skillsToSeed = fs.readdirSync(scaffoldSkillsDir).filter(f => fs.statSync(path.join(scaffoldSkillsDir, f)).isDirectory());
184
+ } catch { /* fallback to default */ }
185
+ }
186
+
187
+ for (const skill of skillsToSeed) {
188
+ const skillSrc = path.join(scaffoldSkillsDir, skill);
189
+ const skillDest = path.join(agentDir, 'skills', skill);
190
+
191
+ if (!fs.existsSync(skillSrc)) {
192
+ logWarn(`${skill} skill source not found — skipping.`);
193
+ } else {
194
+ copyDirMerge(skillSrc, skillDest, opts.dryRun);
195
+ logOk(`${skill} skill installed`);
196
+ }
197
+ }
198
+
199
+ // ── Step 2.5: Create data directories inside the meta-skill (brain dir) ──
200
+ logStep('2.5/4', 'Creating user data directories inside meta-skill');
201
+
202
+ const vaultCategories = [
203
+ 'invariants', 'patterns', 'anti-patterns',
204
+ 'preferences', 'decisions', 'concepts'
205
+ ];
206
+
207
+ const dataDirs = [
208
+ path.join(brainDir, 'memory-derived'),
209
+ path.join(brainDir, 'memory-inbox', 'pending'),
210
+ path.join(brainDir, 'memory-inbox', 'conflicts'),
211
+ path.join(brainDir, 'sessions'),
212
+ ...vaultCategories.map(c => path.join(brainDir, 'memory-vault', c))
213
+ ];
214
+
215
+ let dataCreated = 0;
216
+ for (const dir of dataDirs) {
217
+ const wasCreated = ensureDir(dir, opts.dryRun);
218
+ if (wasCreated) {
219
+ if (opts.dryRun) log(` mkdir ${path.relative(cwd, dir)}`);
220
+ dataCreated++;
221
+ }
222
+ }
223
+ logOk(`Data directories ready (${dataCreated} created inside meta-skill)`);
224
+
225
+ // ── Step 3: Copy default memory vault nodes ──
226
+ logStep('3/4', 'Seeding default memory vault nodes');
227
+
228
+ const scaffoldVaultSrc = path.join(ROOT, 'scaffold', '.agent', 'skills', 'total-recall', 'memory-vault');
229
+ const localVaultDest = path.join(brainDir, 'memory-vault');
230
+
231
+ if (!fs.existsSync(scaffoldVaultSrc)) {
232
+ logWarn('Scaffold memory vault not found — skipping.');
233
+ } else {
234
+ copyDirMerge(scaffoldVaultSrc, localVaultDest, opts.dryRun);
235
+ logOk('Default memory vault seeded');
236
+ }
237
+
238
+ // ── Step 3.5: Seed the onboarding interview task ──
239
+ logStep('3.5/4', 'Seeding onboarding interview into scheduler queue');
240
+
241
+ const queueDir = path.join(brainDir, 'scheduler', 'queue');
242
+ const interviewDest = path.join(queueDir, 'onboarding-interview.md');
243
+ const interviewSrc = path.join(ROOT, 'templates', 'onboarding-interview.md');
244
+
245
+ if (!fs.existsSync(interviewDest)) {
246
+ if (!opts.dryRun) {
247
+ fs.mkdirSync(queueDir, { recursive: true });
248
+ if (fs.existsSync(interviewSrc)) {
249
+ const now = new Date().toISOString();
250
+ const content = fs.readFileSync(interviewSrc, 'utf8').replace(/\{\{CREATED_AT\}\}/g, now);
251
+ fs.writeFileSync(interviewDest, content);
252
+ logOk('Onboarding interview task queued — agent will conduct it on first chat');
253
+ } else {
254
+ logWarn('Interview template not found — skipping');
255
+ }
256
+ } else {
257
+ log(` Would write ${path.relative(cwd, interviewDest)}`);
258
+ }
259
+ } else {
260
+ logSkip('onboarding-interview.md already in queue');
261
+ }
262
+
263
+ // ── Restore credentials from backup if present ──
264
+ let restoredPasswordHash = null;
265
+ if (!opts.dryRun) {
266
+ const backupSecretsPath = path.join(agentDir, 'secrets.enc');
267
+ if (fs.existsSync(backupSecretsPath)) {
268
+ try {
269
+ const configDir = path.join(brainDir, 'config');
270
+ fs.mkdirSync(configDir, { recursive: true });
271
+
272
+ // Copy secrets.enc to brainDir/config/secrets.enc
273
+ const destSecretsPath = path.join(configDir, 'secrets.enc');
274
+ fs.copyFileSync(backupSecretsPath, destSecretsPath);
275
+
276
+ // Read to see if password hash is present
277
+ const secretsObj = JSON.parse(fs.readFileSync(destSecretsPath, 'utf8') || '{}');
278
+ if (secretsObj.dashboard_password_hash) {
279
+ restoredPasswordHash = secretsObj.dashboard_password_hash;
280
+ // Strip password hash from the copied secrets.enc to keep secrets.enc clean
281
+ delete secretsObj.dashboard_password_hash;
282
+ fs.writeFileSync(destSecretsPath, JSON.stringify(secretsObj, null, 2), { encoding: 'utf8', mode: 0o600 });
283
+ }
284
+ logOk('Restored saved API keys and credentials from persistent backup!');
285
+ } catch (err) {
286
+ logWarn(`Failed to restore credentials from backup: ${err.message}`);
287
+ }
288
+ }
289
+ }
290
+
291
+ // ── Step 3.6: Ensure Default Dashboard Password ──
292
+ if (!opts.dryRun) {
293
+ const configDir = path.join(brainDir, "config");
294
+ const securityPath = path.join(configDir, "security.yml");
295
+ try {
296
+ fs.mkdirSync(configDir, { recursive: true });
297
+ const { default: bcrypt } = await import("bcrypt");
298
+ const { default: yaml } = await import("yaml");
299
+
300
+ let config = {};
301
+ if (fs.existsSync(securityPath)) {
302
+ try {
303
+ config = yaml.parse(fs.readFileSync(securityPath, "utf8")) || {};
304
+ } catch {
305
+ config = {};
306
+ }
307
+ }
308
+
309
+ config.dashboard ??= {};
310
+ config.api ??= { pats: [], allow_static_pats: false };
311
+ config.network ??= { require_https: true, public_health: false, allowed_origins: [] };
312
+ config.bind ??= { host: "127.0.0.1", port: 3000, allow_public_bind: false };
313
+
314
+ if (restoredPasswordHash && !config.dashboard.password_hash) {
315
+ config.dashboard.password_hash = restoredPasswordHash;
316
+ }
317
+
318
+ if (!config.dashboard.password_hash) {
319
+ fs.writeFileSync(securityPath, yaml.stringify(config), { encoding: "utf8", mode: 0o600 });
320
+ passwordMessage = "\n 🔑 Dashboard Access: Unconfigured\n (You will be prompted to set your password upon first browser access)";
321
+ } else {
322
+ fs.writeFileSync(securityPath, yaml.stringify(config), { encoding: 'utf8', mode: 0o600 });
323
+ passwordMessage = '\n 🔑 Dashboard credentials preserved (custom password hash already configured)';
324
+ }
325
+ } catch (err) {
326
+ logWarn(`Failed to seed default dashboard credentials: ${err.message}`);
327
+ }
328
+ }
329
+
330
+ // ── Step 3.7: Select UI Deploy Location ──
331
+ let deployMode = opts.deployMode;
332
+ let domain = opts.domain;
333
+ let tunnelName = opts.tunnelName;
334
+ let tunnelCredentials = opts.tunnelCredentials;
335
+
336
+ const isInteractive = process.stdin.isTTY && !opts.yes && !opts.dryRun;
337
+
338
+ if (isInteractive && !deployMode) {
339
+ const rl = (await import('node:readline')).createInterface({
340
+ input: process.stdin,
341
+ output: process.stdout
342
+ });
343
+ const ask = (query) => new Promise(resolve => rl.question(query, resolve));
344
+
345
+ console.error('\n [3.7] How would you like to access your dashboard?');
346
+ console.error(' 1. Local only (http://localhost:3000)');
347
+ console.error(' 2. Cloudflare Quick Tunnel (random public URL, changes on restart)');
348
+ console.error(' 3. Cloudflare Named Tunnel (permanent subdomain, requires cloudflare auth)');
349
+ console.error(' 4. Custom domain (you provide the domain, uses Caddy for TLS)');
350
+
351
+ let choice = '';
352
+ while (true) {
353
+ const answer = (await ask('\n Choice [1]: ')).trim();
354
+ if (!answer) {
355
+ choice = '1';
356
+ break;
357
+ }
358
+ if (['1', '2', '3', '4'].includes(answer)) {
359
+ choice = answer;
360
+ break;
361
+ }
362
+ console.error(' Invalid choice. Please enter 1, 2, 3, or 4.');
363
+ }
364
+
365
+ if (choice === '1') {
366
+ deployMode = 'local';
367
+ } else if (choice === '2') {
368
+ deployMode = 'quick-tunnel';
369
+ } else if (choice === '3') {
370
+ deployMode = 'named-tunnel';
371
+ while (!tunnelName) {
372
+ tunnelName = (await ask(' Enter Cloudflare Tunnel Name: ')).trim();
373
+ }
374
+ while (!tunnelCredentials) {
375
+ tunnelCredentials = (await ask(' Enter Cloudflare Tunnel Credentials Path: ')).trim();
376
+ }
377
+ while (!domain) {
378
+ domain = (await ask(' Enter Public Domain mapped to this tunnel: ')).trim();
379
+ }
380
+ } else if (choice === '4') {
381
+ deployMode = 'custom-domain';
382
+ while (!domain) {
383
+ domain = (await ask(' Enter Custom Domain (e.g. brain.mydomain.com): ')).trim();
384
+ }
385
+ }
386
+ rl.close();
387
+ } else if (!deployMode) {
388
+ deployMode = 'local';
389
+ }
390
+
391
+ // Verify cloudflared binary dependency if a tunnel mode is selected
392
+ if (['quick-tunnel', 'named-tunnel'].includes(deployMode)) {
393
+ let hasCf = false;
394
+ try {
395
+ const whichCf = spawnSync('which', ['cloudflared'], { encoding: 'utf8' });
396
+ if (whichCf.status === 0 && whichCf.stdout?.trim()) {
397
+ hasCf = true;
398
+ } else if (fs.existsSync('/usr/local/bin/cloudflared')) {
399
+ hasCf = true;
400
+ }
401
+ } catch {
402
+ if (fs.existsSync('/usr/local/bin/cloudflared')) {
403
+ hasCf = true;
404
+ }
405
+ }
406
+
407
+ if (!hasCf) {
408
+ logWarn('Cloudflare Tunnel mode selected, but `cloudflared` was not found in your PATH.');
409
+ if (process.platform === 'darwin') {
410
+ console.error('\n To install via Homebrew, run:');
411
+ console.error(' brew install cloudflare/cloudflare/cloudflared\n');
412
+ } else {
413
+ console.error('\n To install, please refer to Cloudflare\'s guide:');
414
+ console.error(' https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/\n');
415
+ }
416
+
417
+ if (isInteractive) {
418
+ const rl = (await import('node:readline')).createInterface({
419
+ input: process.stdin,
420
+ output: process.stdout
421
+ });
422
+ const ask = (query) => new Promise(resolve => rl.question(query, resolve));
423
+ const proceed = (await ask(' Proceed anyway? (y/N): ')).trim().toLowerCase();
424
+ rl.close();
425
+ if (proceed !== 'y' && proceed !== 'yes') {
426
+ console.error(' Aborting setup.');
427
+ process.exit(1);
428
+ }
429
+ } else {
430
+ logWarn('Proceeding in non-interactive mode. Tunnels will fail to spawn until cloudflared is installed.');
431
+ }
432
+ }
433
+ }
434
+
435
+ // Persist UI deploy settings to wizard-config.json
436
+ if (!opts.dryRun) {
437
+ const configDir = path.join(brainDir, 'config');
438
+ const configFile = path.join(configDir, 'wizard-config.json');
439
+ fs.mkdirSync(configDir, { recursive: true });
440
+
441
+ let current = {};
442
+ if (fs.existsSync(configFile)) {
443
+ try {
444
+ current = JSON.parse(fs.readFileSync(configFile, 'utf8') || '{}');
445
+ } catch {}
446
+ }
447
+
448
+ current['deploy-mode'] = deployMode;
449
+ current['tunnel-auto-start'] = ['quick-tunnel', 'named-tunnel'].includes(deployMode);
450
+
451
+ if (deployMode === 'named-tunnel') {
452
+ current['cfg-cloudflare-tunnel-name'] = tunnelName;
453
+ current['cfg-cloudflare-tunnel-credentials'] = tunnelCredentials;
454
+ if (domain) {
455
+ current['cfg-domain'] = domain;
456
+ current['cfg-api-url'] = `https://${domain}`;
457
+ current['cfg-dash-url'] = `https://${domain}/dashboard`;
458
+ current['cfg-health-url'] = `https://${domain}/health`;
459
+ }
460
+ } else if (deployMode === 'custom-domain') {
461
+ current['cfg-domain'] = domain;
462
+ current['cfg-api-url'] = `https://${domain}`;
463
+ current['cfg-dash-url'] = `https://${domain}/dashboard`;
464
+ current['cfg-health-url'] = `https://${domain}/health`;
465
+ } else if (deployMode === 'local') {
466
+ current['cfg-domain'] = 'localhost';
467
+ current['cfg-api-url'] = 'http://localhost:3000';
468
+ current['cfg-dash-url'] = 'http://localhost:3000/dashboard';
469
+ current['cfg-health-url'] = 'http://localhost:3000/health';
470
+ }
471
+
472
+ fs.writeFileSync(configFile, JSON.stringify(current, null, 2), { encoding: 'utf8', mode: 0o600 });
473
+ logOk('UI deploy configuration persisted to wizard-config.json!');
474
+ }
475
+
476
+ // ── Symlink nested brain skills to top-level .agent/skills/ ──
477
+ // Skills inside the brain (e.g. .agent/skills/total-recall/skills/ssss/) are
478
+ // invisible to IDEs because they only scan .agent/skills/<name>/ one level
479
+ // deep. Create symlinks so they appear as slash commands.
480
+ const nestedSkillsDir = path.join(brainDir, 'skills');
481
+ if (fs.existsSync(nestedSkillsDir)) {
482
+ const topSkillsDir = path.join(agentDir, 'skills');
483
+ for (const entry of fs.readdirSync(nestedSkillsDir, { withFileTypes: true })) {
484
+ if (!entry.isDirectory()) continue;
485
+ const target = path.join(topSkillsDir, entry.name);
486
+ const source = path.join(nestedSkillsDir, entry.name);
487
+ if (fs.existsSync(target)) continue; // don't clobber existing top-level skills
488
+ if (!opts.dryRun) {
489
+ try {
490
+ fs.symlinkSync(source, target);
491
+ logOk(`Symlinked nested skill: ${entry.name} → IDE-visible`);
492
+ } catch (err) {
493
+ logWarn(`Could not symlink skill "${entry.name}": ${err.message}`);
494
+ }
495
+ } else {
496
+ log(` Would symlink ${entry.name} → ${target}`);
497
+ }
498
+ }
499
+ }
500
+
501
+ // ── Step 4: Run compile to inject memory block into existing IDE files ──
502
+ logStep('4/4', 'Compiling vault and injecting into IDE instruction files');
503
+
504
+ if (opts.dryRun) {
505
+ log(' Would run: total-recall compile (targeting current directory)');
506
+ logOk('Dry run complete. Run without --dry-run to apply changes.');
507
+ return;
508
+ }
509
+
510
+ const instructionsFile = path.join(cwd, 'INSTRUCTIONS.md');
511
+ const vaultDir = path.join(brainDir, 'memory-vault');
512
+ const skillsDir = path.join(agentDir, 'skills');
513
+ const derivedDir = path.join(brainDir, 'memory-derived');
514
+
515
+ try {
516
+ const { compileSurface } = await import('../core/surface.mjs');
517
+ const result = await compileSurface({ vaultDir, skillsDir, derivedDir, instructionsFile });
518
+ if (result.semanticUnavailable) {
519
+ logWarn("Semantic index build is temporarily unavailable (missing API credentials or Ollama offline).");
520
+ logWarn("Please set GOOGLE_API_KEY or OPENAI_API_KEY, or run 'npx total-recall setup' later to enable full semantic search.");
521
+ }
522
+ logOk(`Compile complete — ${result.nodesProcessed} nodes processed`);
523
+ } catch (err) {
524
+ logWarn(`❌ Initialization aborted due to critical error: ${err.message}`);
525
+ process.exit(1);
526
+ }
527
+
528
+ // ── Optional: register a remote brain for hybrid mode ──
529
+ if (opts.brain) {
530
+ const cfgDir = path.join(brainDir, 'config');
531
+ fs.mkdirSync(cfgDir, { recursive: true });
532
+ const brainCfg = { url: opts.brain };
533
+ if (opts.token) brainCfg.token = opts.token;
534
+ fs.writeFileSync(path.join(cfgDir, 'brain.json'), JSON.stringify(brainCfg, null, 2), { mode: 0o600 });
535
+ logOk(`Registered brain at ${opts.brain}. Run \`npx total-recall sync\` to pull instructions.`);
536
+ }
537
+
538
+ let dashboardUrl = '';
539
+
540
+ // ── Automatic Cloudflare Quick Tunnel Spawner ──
541
+ if (deployMode === 'quick-tunnel') {
542
+ let hasCloudflared = false;
543
+ try {
544
+ const whichCf = spawnSync('which', ['cloudflared'], { encoding: 'utf8' });
545
+ if (whichCf.status === 0 && whichCf.stdout?.trim()) {
546
+ hasCloudflared = true;
547
+ } else if (fs.existsSync('/usr/local/bin/cloudflared')) {
548
+ hasCloudflared = true;
549
+ }
550
+ } catch {
551
+ if (fs.existsSync('/usr/local/bin/cloudflared')) {
552
+ hasCloudflared = true;
553
+ }
554
+ }
555
+
556
+ if (hasCloudflared) {
557
+ log('Cloudflared detected. Spawning background Quick Tunnel...');
558
+ const logsDir = path.join(brainDir, 'logs');
559
+ if (!opts.dryRun) {
560
+ fs.mkdirSync(logsDir, { recursive: true });
561
+ }
562
+ const cfLogPath = path.join(logsDir, 'cloudflared.log');
563
+
564
+ if (!opts.dryRun) {
565
+ try {
566
+ if (fs.existsSync(cfLogPath)) {
567
+ fs.unlinkSync(cfLogPath);
568
+ }
569
+ } catch {}
570
+
571
+ const logStream = fs.openSync(cfLogPath, 'w');
572
+ let port = 3000;
573
+ try {
574
+ const { default: cfg } = await import('../core/config.mjs');
575
+ if (cfg.port) port = cfg.port;
576
+ } catch {}
577
+
578
+ const cfProcess = spawn('cloudflared', ['tunnel', '--url', `http://localhost:${port}`], {
579
+ detached: true,
580
+ stdio: ['ignore', logStream, logStream]
581
+ });
582
+
583
+ const cfPidPath = path.join(logsDir, 'cloudflared.pid');
584
+ fs.writeFileSync(cfPidPath, String(cfProcess.pid), 'utf8');
585
+
586
+ cfProcess.unref();
587
+
588
+ log('Waiting for Cloudflare Quick Tunnel URL allocation...');
589
+ let tunnelUrl = '';
590
+ for (let attempt = 0; attempt < 20; attempt++) {
591
+ await new Promise(resolve => setTimeout(resolve, 500));
592
+ if (fs.existsSync(cfLogPath)) {
593
+ const logs = fs.readFileSync(cfLogPath, 'utf8');
594
+ const match = logs.match(/https:\/\/[a-zA-Z0-9-]+\.trycloudflare\.com/);
595
+ if (match) {
596
+ tunnelUrl = match[0];
597
+ break;
598
+ }
599
+ }
600
+ }
601
+
602
+ if (tunnelUrl) {
603
+ logOk(`Cloudflare Quick Tunnel Active: ${tunnelUrl}`);
604
+
605
+ const configDir = path.join(brainDir, 'config');
606
+ const configFile = path.join(configDir, 'wizard-config.json');
607
+ fs.mkdirSync(configDir, { recursive: true });
608
+
609
+ let current = {};
610
+ if (fs.existsSync(configFile)) {
611
+ try {
612
+ current = JSON.parse(fs.readFileSync(configFile, 'utf8') || '{}');
613
+ } catch {}
614
+ }
615
+ current['cfg-domain'] = tunnelUrl.replace('https://', '');
616
+ current['cfg-api-url'] = tunnelUrl;
617
+ current['cfg-dash-url'] = `${tunnelUrl}/dashboard`;
618
+ current['cfg-health-url'] = `${tunnelUrl}/health`;
619
+
620
+ fs.writeFileSync(configFile, JSON.stringify(current, null, 2), { encoding: 'utf8', mode: 0o600 });
621
+ logOk('Registered Quick Tunnel URL in wizard-config.json!');
622
+ dashboardUrl = `${tunnelUrl}/`;
623
+ } else {
624
+ logWarn('Could not extract Cloudflare Quick Tunnel URL. Falling back to local configuration.');
625
+ }
626
+ }
627
+ }
628
+ }
629
+
630
+ // Resolve dashboard URL based on deployMode preference
631
+ if (!dashboardUrl) {
632
+ if ((deployMode === 'custom-domain' || deployMode === 'named-tunnel') && domain) {
633
+ dashboardUrl = `https://${domain}/`;
634
+ } else if (opts.brain) {
635
+ dashboardUrl = opts.brain;
636
+ } else if (process.env.TR_BRAIN) {
637
+ dashboardUrl = process.env.TR_BRAIN;
638
+ } else {
639
+ const wizardConfigPath = path.join(brainDir, 'config', 'wizard-config.json');
640
+ if (fs.existsSync(wizardConfigPath)) {
641
+ try {
642
+ const wizardCfg = JSON.parse(fs.readFileSync(wizardConfigPath, 'utf8'));
643
+ if (wizardCfg['cfg-dash-url']) {
644
+ dashboardUrl = wizardCfg['cfg-dash-url'];
645
+ } else if (wizardCfg['cfg-api-url']) {
646
+ dashboardUrl = wizardCfg['cfg-api-url'];
647
+ }
648
+ } catch {}
649
+ }
650
+ }
651
+ }
652
+
653
+ if (!dashboardUrl) {
654
+ const brainJsonPath = path.join(brainDir, 'config', 'brain.json');
655
+ if (fs.existsSync(brainJsonPath)) {
656
+ try {
657
+ const brainCfg = JSON.parse(fs.readFileSync(brainJsonPath, 'utf8'));
658
+ if (brainCfg.url) {
659
+ dashboardUrl = brainCfg.url;
660
+ }
661
+ } catch {}
662
+ }
663
+ }
664
+
665
+ if (!dashboardUrl) {
666
+ try {
667
+ const { default: cfg } = await import('../core/config.mjs');
668
+ const displayHost = cfg.host === '127.0.0.1' || cfg.host === '0.0.0.0' ? 'localhost' : cfg.host;
669
+ dashboardUrl = `http://${displayHost}:${cfg.port}/`;
670
+ } catch {
671
+ dashboardUrl = 'http://localhost:3000/';
672
+ }
673
+ }
674
+
675
+ // Normalize dashboardUrl
676
+ if (dashboardUrl && !dashboardUrl.endsWith('/')) {
677
+ dashboardUrl += '/';
678
+ }
679
+
680
+ // ── Register project brain in global project registry ──
681
+ if (isProject) {
682
+ try {
683
+ const registryDir = path.join(globalBrainDir, 'config');
684
+ fs.mkdirSync(registryDir, { recursive: true });
685
+ const registryPath = path.join(registryDir, 'project-registry.json');
686
+ let registry = [];
687
+ if (fs.existsSync(registryPath)) {
688
+ try { registry = JSON.parse(fs.readFileSync(registryPath, 'utf8')); } catch { registry = []; }
689
+ }
690
+ const projectName = path.basename(cwd);
691
+ const existingIdx = registry.findIndex(p => p.path === cwd);
692
+ const entry = {
693
+ name: projectName,
694
+ path: cwd,
695
+ brainDir: brainDir,
696
+ registered_at: new Date().toISOString(),
697
+ last_compiled: new Date().toISOString(),
698
+ };
699
+ if (existingIdx >= 0) {
700
+ registry[existingIdx] = { ...registry[existingIdx], ...entry };
701
+ } else {
702
+ registry.push(entry);
703
+ }
704
+ fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2));
705
+ logOk(`Registered project "${projectName}" in global brain registry`);
706
+ } catch (err) {
707
+ logWarn(`Failed to register project: ${err.message}`);
708
+ }
709
+ }
710
+
711
+ console.error(`
712
+ ✅ Total Recall initialized! (${layerLabel} brain)
713
+ ${passwordMessage}
714
+
715
+ Your ${isProject ? 'project' : 'global'} brain is ready at:
716
+ ${brainDir}
717
+
718
+ IDE instruction files have been updated with the Total Recall memory block.
719
+ Existing content in GEMINI.md, .cursorrules, CLAUDE.md etc. was preserved.
720
+
721
+ 🖥️ Dashboard UI:
722
+ ${dashboardUrl}
723
+
724
+ Next steps:
725
+ 1. Run \`npx total-recall compile\` any time to rebuild the memory surface.
726
+ 2. Use \`npx total-recall remember <category> "..."\ to add memories.
727
+ 3. Run \`npx total-recall daemon start\` to enable the background Dream Cycle.${isProject ? '\n 4. Your project brain is registered in the global brain\'s project registry.' : ''}
728
+ `);
729
+
730
+ // Automatically open the Dashboard UI in the user's browser
731
+ log("Launching Dashboard UI Walkthrough in your browser...");
732
+ const startCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
733
+ exec(`${startCmd} "${dashboardUrl}"`, (err) => {
734
+ // Silently ignore browser opening failures
735
+ });
736
+ }