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,872 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { agentDir as configAgentDir, brainDir as configBrainDir, xdgConfigHome, trBrain, trPat } from '../core/config.mjs';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { resolveAgentDir, getBothBrains, detectProjectBrain } from './agent-dir.mjs';
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ // Resolve templates dir relative to this file (src/cli/ → ../../templates/)
10
+ const TEMPLATES_DIR = path.resolve(__dirname, '..', '..', 'templates');
11
+
12
+ const CLIENTS = {
13
+ vscode: {
14
+ label: 'VS Code Copilot',
15
+ mode: 'file',
16
+ target: path.join('.github', 'copilot-instructions.md'),
17
+ render: instructions => `<!-- Total Recall — auto-generated by \`npx total-recall connect vscode\` -->\n<!-- Do not edit; re-run connect to refresh -->\n\n${instructions}`
18
+ },
19
+ pi: {
20
+ label: 'Pi Coding Agent',
21
+ mode: 'global-symlink',
22
+ globalTarget: path.join(os.homedir(), '.pi', 'agent', 'AGENTS.md'),
23
+ localTarget: 'AGENTS.md',
24
+ },
25
+ hermes: {
26
+ label: 'Hermes Agent',
27
+ mode: 'hermes',
28
+ memoryPath: path.join(os.homedir(), '.hermes', 'memories', 'MEMORY.md'),
29
+ userPath: path.join(os.homedir(), '.hermes', 'memories', 'USER.md'),
30
+ },
31
+ openclaw: {
32
+ label: 'OpenClaw',
33
+ mode: 'openclaw',
34
+ memoryTarget: 'MEMORY.md',
35
+ agentsTarget: 'AGENTS.md',
36
+ },
37
+ cursor: {
38
+ label: 'Cursor',
39
+ mode: 'file',
40
+ target: path.join('.cursor', 'rules', 'total-recall.mdc'),
41
+ render: instructions => [
42
+ '---',
43
+ 'description: "Total Recall — Auto-generated behavioral memory surface."',
44
+ 'globs:',
45
+ 'alwaysApply: true',
46
+ '---',
47
+ '',
48
+ instructions
49
+ ].join('\n')
50
+ },
51
+ 'claude-code': {
52
+ label: 'Claude Code',
53
+ mode: 'symlink',
54
+ target: 'CLAUDE.md',
55
+ writeSlashCommands: true
56
+ },
57
+ codex: {
58
+ label: 'Codex',
59
+ mode: 'symlink',
60
+ target: 'AGENTS.md'
61
+ },
62
+ antigravity: {
63
+ label: 'Antigravity',
64
+ mode: 'symlink',
65
+ target: 'AGENTS.md',
66
+ writeSlashCommands: true
67
+ },
68
+ gemini: {
69
+ label: 'Gemini',
70
+ mode: 'symlink',
71
+ target: 'GEMINI.md',
72
+ writeSlashCommands: true
73
+ },
74
+ aider: {
75
+ label: 'Aider',
76
+ mode: 'file',
77
+ target: '.aider.rules.md',
78
+ render: instructions => instructions,
79
+ after: [
80
+ 'Add this to .aider.conf.yml:',
81
+ '',
82
+ 'read:',
83
+ ' - .aider.rules.md'
84
+ ].join('\n')
85
+ },
86
+ ultrachat: {
87
+ label: 'UltraChat',
88
+ mode: 'api'
89
+ // No file projection: UltraChat connects via the OpenAI-compatible API.
90
+ // The compiled INSTRUCTIONS.md is injected per-request by the server.
91
+ // See docs/guides/ultrachat.md for session sync via /api/sessions/*.
92
+ },
93
+ obsidian: {
94
+ label: 'Obsidian',
95
+ mode: 'vault',
96
+ // Symlinks ~/.agent/memory-vault/ into the Obsidian vault as "Total Recall/"
97
+ // Obsidian indexes all SSSS nodes natively — graph view, backlinks, search.
98
+ folderName: 'Total Recall'
99
+ },
100
+ generic: {
101
+ label: 'Generic OpenAI-Compatible Client',
102
+ mode: 'api'
103
+ }
104
+ };
105
+
106
+ function parseArgs(args) {
107
+ const opts = {
108
+ client: args[0] && !args[0].startsWith('--') ? args[0] : null,
109
+ brain: null,
110
+ token: null,
111
+ vault: null,
112
+ force: false,
113
+ json: false,
114
+ help: false
115
+ };
116
+
117
+ for (let i = opts.client ? 1 : 0; i < args.length; i++) {
118
+ switch (args[i]) {
119
+ case '--brain': opts.brain = args[++i]; break;
120
+ case '--token': opts.token = args[++i]; break;
121
+ case '--vault': opts.vault = args[++i]; break;
122
+ case '--force': opts.force = true; break;
123
+ case '--json': opts.json = true; break;
124
+ case '--help':
125
+ case '-h':
126
+ opts.help = true;
127
+ break;
128
+ }
129
+ }
130
+
131
+ return opts;
132
+ }
133
+
134
+ function printHelp() {
135
+ console.log(`
136
+ total-recall connect — Configure an IDE or external system
137
+
138
+ Usage: total-recall connect <client> [options]
139
+
140
+ Clients:
141
+ cursor
142
+ claude-code
143
+ codex
144
+ antigravity
145
+ gemini
146
+ aider
147
+ ultrachat
148
+ obsidian
149
+ generic
150
+
151
+ Options:
152
+ --brain <url> Brain base URL for API clients
153
+ --token <token> Bearer PAT for generated snippets
154
+ --vault <path> Obsidian vault path (auto-detected on macOS if omitted)
155
+ --force Overwrite generated target files
156
+ --json Emit machine-readable connection details
157
+ --help, -h Show this help
158
+ `);
159
+ }
160
+
161
+ function pathExists(filePath) {
162
+ try {
163
+ fs.lstatSync(filePath);
164
+ return true;
165
+ } catch {
166
+ return false;
167
+ }
168
+ }
169
+
170
+ function copyDirRecursive(src, dest) {
171
+ if (!fs.existsSync(src)) return;
172
+ if (!fs.existsSync(dest)) {
173
+ fs.mkdirSync(dest, { recursive: true });
174
+ }
175
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
176
+ const srcPath = path.join(src, entry.name);
177
+ const destPath = path.join(dest, entry.name);
178
+ if (entry.isDirectory()) {
179
+ copyDirRecursive(srcPath, destPath);
180
+ } else {
181
+ if (!fs.existsSync(destPath)) {
182
+ fs.copyFileSync(srcPath, destPath);
183
+ }
184
+ }
185
+ }
186
+ }
187
+
188
+ function bootstrapAgentDir(agentDir) {
189
+ const brainDir = path.join(agentDir, 'skills', 'total-recall');
190
+ const vaultCategories = [
191
+ 'invariants', 'patterns', 'anti-patterns',
192
+ 'preferences', 'decisions', 'concepts'
193
+ ];
194
+
195
+ const dirs = [
196
+ path.join(agentDir, 'skills'),
197
+ path.join(brainDir, 'memory-derived'),
198
+ path.join(brainDir, 'memory-inbox', 'pending'),
199
+ path.join(brainDir, 'memory-inbox', 'conflicts'),
200
+ path.join(brainDir, 'sessions'),
201
+ ...vaultCategories.map(c => path.join(brainDir, 'memory-vault', c))
202
+ ];
203
+
204
+ for (const dir of dirs) {
205
+ if (!fs.existsSync(dir)) {
206
+ fs.mkdirSync(dir, { recursive: true });
207
+ }
208
+ }
209
+
210
+ const scaffoldSkillsDir = path.join(TEMPLATES_DIR, '..', 'scaffold', '.agent', 'skills');
211
+ let skillsToCopy = ['total-recall'];
212
+ if (fs.existsSync(scaffoldSkillsDir)) {
213
+ try {
214
+ skillsToCopy = fs.readdirSync(scaffoldSkillsDir).filter(f => {
215
+ return fs.statSync(path.join(scaffoldSkillsDir, f)).isDirectory();
216
+ });
217
+ } catch {}
218
+ }
219
+
220
+ for (const skill of skillsToCopy) {
221
+ const skillSrc = path.join(scaffoldSkillsDir, skill);
222
+ const skillDest = path.join(agentDir, 'skills', skill);
223
+ if (fs.existsSync(skillSrc) && !fs.existsSync(path.join(skillDest, 'SKILL.md'))) {
224
+ copyDirRecursive(skillSrc, skillDest);
225
+ }
226
+ }
227
+ }
228
+
229
+ function readInstructions(cwd, agentDir) {
230
+ const candidates = [
231
+ path.join(cwd, 'INSTRUCTIONS.md'),
232
+ path.join(agentDir, 'INSTRUCTIONS.md')
233
+ ];
234
+ const found = candidates.find(file => fs.existsSync(file));
235
+ if (!found) return null;
236
+ return fs.readFileSync(found, 'utf8');
237
+ }
238
+
239
+ function ensureBrainConfig(brainDir, opts) {
240
+ if (!opts.brain) return null;
241
+ const configDir = path.join(brainDir, 'config');
242
+ fs.mkdirSync(configDir, { recursive: true });
243
+ const config = { url: opts.brain };
244
+ if (opts.token) config.token = opts.token;
245
+ const filePath = path.join(configDir, 'brain.json');
246
+ fs.writeFileSync(filePath, JSON.stringify(config, null, 2));
247
+ return filePath;
248
+ }
249
+
250
+ function registerClient(brainDir, clientName, preset, projectionPath) {
251
+ const configDir = path.join(brainDir, 'config');
252
+ fs.mkdirSync(configDir, { recursive: true });
253
+ const registryPath = path.join(configDir, 'clients.json');
254
+ let registry = { clients: {} };
255
+ if (fs.existsSync(registryPath)) {
256
+ try { registry = JSON.parse(fs.readFileSync(registryPath, 'utf8')); }
257
+ catch { /* corrupt file — start fresh */ }
258
+ }
259
+ registry.clients ??= {};
260
+ registry.clients[clientName] = {
261
+ label: preset.label,
262
+ mode: preset.mode,
263
+ projectionPath: projectionPath || null,
264
+ connectedAt: new Date().toISOString()
265
+ };
266
+ fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2), 'utf8');
267
+ }
268
+
269
+ function writeFileProjection(cwd, preset, instructions, opts) {
270
+ const targetPath = path.join(cwd, preset.target);
271
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
272
+ const existed = fs.existsSync(targetPath);
273
+ if (fs.existsSync(targetPath) && !opts.force) {
274
+ return { targetPath, action: 'exists' };
275
+ }
276
+ const content = preset.render(instructions);
277
+ fs.writeFileSync(targetPath, content, 'utf8');
278
+ return { targetPath, action: existed ? 'written' : 'created' };
279
+ }
280
+
281
+ function writeSymlinkProjection(cwd, preset, opts) {
282
+ const instructionsPath = path.join(cwd, 'INSTRUCTIONS.md');
283
+ if (!pathExists(instructionsPath)) {
284
+ throw new Error(`Cannot create ${preset.target}: ${instructionsPath} does not exist. Run npx total-recall init or compile first.`);
285
+ }
286
+
287
+ const targetPath = path.join(cwd, preset.target);
288
+ if (pathExists(targetPath)) {
289
+ if (!opts.force) return { targetPath, action: 'exists' };
290
+ try {
291
+ fs.unlinkSync(targetPath);
292
+ } catch {
293
+ fs.rmSync(targetPath, { force: true, recursive: true });
294
+ }
295
+ }
296
+ fs.symlinkSync('INSTRUCTIONS.md', targetPath);
297
+ return { targetPath, action: 'symlinked' };
298
+ }
299
+
300
+ function apiDetails(opts) {
301
+ const base = (opts.brain || 'http://127.0.0.1:3000').replace(/\/$/, '');
302
+ return {
303
+ base_url: base,
304
+ chat_completions_url: `${base}/v1/chat/completions`,
305
+ models_url: `${base}/v1/models`,
306
+ ssss_manifest_url: `${base}/api/ssss`,
307
+ discovery_url: `${base}/.well-known/total-recall.json`,
308
+ authorization_header: opts.token ? `Authorization: Bearer ${opts.token}` : 'Authorization: Bearer <PAT>'
309
+ };
310
+ }
311
+
312
+ function detectObsidianVault() {
313
+ // macOS: Obsidian stores vault paths in its app support config
314
+ const obsidianConfig = path.join(
315
+ os.homedir(), 'Library', 'Application Support', 'obsidian', 'obsidian.json'
316
+ );
317
+ if (fs.existsSync(obsidianConfig)) {
318
+ try {
319
+ const cfg = JSON.parse(fs.readFileSync(obsidianConfig, 'utf8'));
320
+ const vaults = Object.values(cfg.vaults || {});
321
+ // Prefer the most recently opened vault
322
+ const sorted = vaults.sort((a, b) => (b.ts || 0) - (a.ts || 0));
323
+ if (sorted.length > 0 && sorted[0].path) return sorted[0].path;
324
+ } catch { /* ignore parse errors */ }
325
+ }
326
+ // Linux: check common locations
327
+ const xdgConfig = xdgConfigHome;
328
+ const linuxConfig = path.join(xdgConfig, 'obsidian', 'obsidian.json');
329
+ if (fs.existsSync(linuxConfig)) {
330
+ try {
331
+ const cfg = JSON.parse(fs.readFileSync(linuxConfig, 'utf8'));
332
+ const vaults = Object.values(cfg.vaults || {});
333
+ const sorted = vaults.sort((a, b) => (b.ts || 0) - (a.ts || 0));
334
+ if (sorted.length > 0 && sorted[0].path) return sorted[0].path;
335
+ } catch { /* ignore */ }
336
+ }
337
+ return null;
338
+ }
339
+
340
+ function writeVaultProjection(agentDir, preset, opts) {
341
+ const vaultPath = opts.vault || detectObsidianVault();
342
+ if (!vaultPath) {
343
+ throw new Error(
344
+ 'Could not detect an Obsidian vault. Pass --vault <path> to specify one.\n' +
345
+ ' e.g. total-recall connect obsidian --vault ~/Documents/MyVault'
346
+ );
347
+ }
348
+ if (!fs.existsSync(vaultPath)) {
349
+ throw new Error(`Obsidian vault not found: ${vaultPath}`);
350
+ }
351
+
352
+ const brainDir = path.join(agentDir, 'skills', 'total-recall');
353
+ const memoryVault = path.join(brainDir, 'memory-vault');
354
+ if (!fs.existsSync(memoryVault)) {
355
+ throw new Error(`Memory vault not found: ${memoryVault}. Run "total-recall init" first.`);
356
+ }
357
+
358
+ const cwd = process.cwd();
359
+ const isHome = cwd === os.homedir();
360
+ let folderName = preset.folderName;
361
+ if (!isHome) {
362
+ const base = path.basename(cwd);
363
+ if (base.toLowerCase() === 'ultrachat-ai-powered' || base.toLowerCase() === 'ultrachat') {
364
+ folderName = 'UltraChat';
365
+ } else if (base.toLowerCase() === 'total-recall') {
366
+ folderName = 'Total Recall';
367
+ } else {
368
+ folderName = base
369
+ .split(/[-_]/)
370
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
371
+ .join(' ');
372
+ }
373
+ }
374
+
375
+ const linkPath = path.join(vaultPath, folderName);
376
+ if (pathExists(linkPath)) {
377
+ if (!opts.force) return { targetPath: linkPath, vaultPath, action: 'exists', folderName };
378
+ try {
379
+ fs.unlinkSync(linkPath);
380
+ } catch {
381
+ fs.rmSync(linkPath, { force: true, recursive: true });
382
+ }
383
+ }
384
+ fs.symlinkSync(memoryVault, linkPath);
385
+
386
+ // Install Dataview query dashboard files into memory-vault/queries/
387
+ // These are TR-native markdown files; Dataview renders them in Obsidian.
388
+ const queriesTemplateDir = path.join(TEMPLATES_DIR, 'obsidian-queries');
389
+ const queriesDestDir = path.join(memoryVault, 'queries');
390
+ if (fs.existsSync(queriesTemplateDir)) {
391
+ fs.mkdirSync(queriesDestDir, { recursive: true });
392
+ for (const file of fs.readdirSync(queriesTemplateDir)) {
393
+ const dest = path.join(queriesDestDir, file);
394
+ if (!fs.existsSync(dest)) {
395
+ fs.copyFileSync(path.join(queriesTemplateDir, file), dest);
396
+ }
397
+ }
398
+ }
399
+
400
+ return { targetPath: linkPath, vaultPath, action: 'symlinked', folderName };
401
+ }
402
+
403
+ function printApiSnippet(client, details) {
404
+ console.log(`\n ${client.label} connection details\n`);
405
+ console.log(` Discovery: ${details.discovery_url}`);
406
+ console.log(` Models: ${details.models_url}`);
407
+ console.log(` Chat: ${details.chat_completions_url}`);
408
+ console.log(` SSSS: ${details.ssss_manifest_url}`);
409
+ console.log(` Auth: ${details.authorization_header}`);
410
+ console.log('\n OpenAI-compatible config:');
411
+ console.log(` baseURL: ${details.base_url}/v1`);
412
+ console.log(' model: total-recall/gemma4');
413
+ }
414
+
415
+ function writeClaudeCodeSlashCommands(opts) {
416
+ const commandsDir = path.join(os.homedir(), '.claude', 'commands');
417
+ fs.mkdirSync(commandsDir, { recursive: true });
418
+
419
+ const base = (opts.brain || 'http://127.0.0.1:3000').replace(/\/$/, '');
420
+ const authHeader = 'Authorization: Bearer <YOUR_PAT_TOKEN>';
421
+
422
+ const commands = [
423
+ {
424
+ file: 'memory.md',
425
+ description: 'Search or manage Total Recall memory',
426
+ body: `Search or manage your Total Recall memory via the REST API.
427
+
428
+ Brain URL: ${base}
429
+ Auth header: ${authHeader}
430
+
431
+ Usage examples (pass as arguments):
432
+ search <query> — semantic search across all memory nodes
433
+ list — list all nodes
434
+ add <title> <body> — create a new memory node
435
+
436
+ Call \`${base}/api/memory/search\` with body \`{"query":"$ARGUMENTS"}\` to search.
437
+ Call \`${base}/api/memory\` with POST to add nodes.
438
+ Always include the Authorization header above.
439
+
440
+ $ARGUMENTS`
441
+ },
442
+ {
443
+ file: 'brain.md',
444
+ description: 'Show Total Recall brain status and health',
445
+ body: `Check your Total Recall brain status.
446
+
447
+ Brain URL: ${base}
448
+ Auth header: ${authHeader}
449
+
450
+ Fetch \`${base}/health\` and \`${base}/api/health\` to check server health.
451
+ Fetch \`${base}/.well-known/total-recall.json\` for the full capability manifest.
452
+ Report: server status, model loaded, vault node count, connected clients.
453
+
454
+ $ARGUMENTS`
455
+ },
456
+ {
457
+ file: 'vault.md',
458
+ description: 'Compile or inspect Total Recall memory vault',
459
+ body: `Compile or inspect the Total Recall SSSS memory vault.
460
+
461
+ Brain URL: ${base}
462
+ Auth header: ${authHeader}
463
+
464
+ Commands (pass as argument):
465
+ compile — POST ${base}/api/vault/compile to rebuild INSTRUCTIONS.md
466
+ nodes — GET ${base}/api/vault/nodes to list all SSSS nodes
467
+ surface — GET ${base}/api/vault/surface to read compiled surface
468
+
469
+ $ARGUMENTS`
470
+ },
471
+ {
472
+ file: 'ssss.md',
473
+ description: 'Explore the SSSS cognitive graph, backlinks, and topological structure',
474
+ body: `Explore and manage your SSSS cognitive graph, Obsidian canvas, and backlinks.
475
+
476
+ Brain URL: ${base}
477
+ Auth header: ${authHeader}
478
+
479
+ Commands (pass as argument):
480
+ help — Learn SSSS categories, frontmatter rules, and schema specs.
481
+ stats — Get deep network topology metrics (Total links, unlinked Orphans, connected Hubs).
482
+ map <slug> — Render bidirectional incoming/outgoing graph connections for a slug.
483
+ compile — Recompiles derived indices and regenerates the high-fidelity Obsidian Canvas.
484
+
485
+ Examples:
486
+ /ssss stats
487
+ /ssss map atomic-writes
488
+ /ssss compile
489
+
490
+ If the argument is "compile", perform a POST to \`${base}/api/vault/compile\`.
491
+ Otherwise, run \`npx total-recall map --relations\` or execute curl queries to list network metrics.
492
+
493
+ $ARGUMENTS`
494
+ },
495
+ {
496
+ file: 'recall.md',
497
+ description: 'Ask Total Recall brain a question or run a task',
498
+ body: `Send a message to your Total Recall brain.
499
+
500
+ Brain URL: ${base}
501
+ Auth header: ${authHeader}
502
+
503
+ POST to \`${base}/v1/chat/completions\` with:
504
+ model: "total-recall"
505
+ messages: [{"role":"user","content":"$ARGUMENTS"}]
506
+
507
+ Include the Authorization header. Stream the response if possible.
508
+
509
+ $ARGUMENTS`
510
+ }
511
+ ];
512
+
513
+ const written = [];
514
+ for (const cmd of commands) {
515
+ const filePath = path.join(commandsDir, cmd.file);
516
+ const content = `---\ndescription: "${cmd.description}"\n---\n${cmd.body}\n`;
517
+ const existed = fs.existsSync(filePath);
518
+ if (!existed || opts.force) {
519
+ fs.writeFileSync(filePath, content, 'utf8');
520
+ written.push({ file: cmd.file, action: existed ? 'updated' : 'created' });
521
+ } else {
522
+ written.push({ file: cmd.file, action: 'exists' });
523
+ }
524
+ }
525
+ return written;
526
+ }
527
+
528
+ function writeGeminiSlashCommands(opts) {
529
+ const globalDir = path.join(os.homedir(), '.gemini', 'commands');
530
+ const localDir = path.join(process.cwd(), '.gemini', 'commands');
531
+
532
+ fs.mkdirSync(globalDir, { recursive: true });
533
+ fs.mkdirSync(localDir, { recursive: true });
534
+
535
+ const base = (opts.brain || 'http://127.0.0.1:3000').replace(/\/$/, '');
536
+ const token = '<YOUR_PAT_TOKEN>';
537
+
538
+ const commands = {
539
+ 'memory.toml': {
540
+ description: 'Search or manage Total Recall memory',
541
+ prompt: `You are Antigravity, an AI assistant with the Total Recall SSSS v2 memory vault.
542
+ Search the semantic memory vault or fetch nodes using the following query:
543
+ {{args}}
544
+
545
+ Instructions:
546
+ Perform a POST request to \`${base}/api/memory/search/semantic\` with the body \`{"query": "{{args}}", "top_k": 5}\` and Authorization: Bearer ${token}.`
547
+ },
548
+ 'brain.toml': {
549
+ description: 'Show Total Recall brain status and health',
550
+ prompt: `Show the health, loaded model status, and statistics of the local Total Recall Sovereign OS brain.
551
+
552
+ Instructions:
553
+ Query the health of the local brain by sending a GET request to \`${base}/health\` with Authorization: Bearer ${token}.`
554
+ },
555
+ 'vault.toml': {
556
+ description: 'Compile or inspect the Total Recall SSSS memory vault',
557
+ prompt: `Compile, rebuild, or inspect the Structured Semantic Syntax System (SSSS) memory vault.
558
+
559
+ Instructions:
560
+ 1. If the argument is "compile", send a POST request to \`${base}/api/vault/compile\` with Authorization: Bearer ${token} or run \`npx total-recall compile\`.
561
+ 2. If the argument is "nodes", send a GET request to \`${base}/api/memory\` with Authorization: Bearer ${token}.
562
+ 3. Summarize the changes and verify rule propagation across the workspace.`
563
+ },
564
+ 'ssss.toml': {
565
+ description: 'Explore SSSS cognitive graph, backlinks, and network statistics',
566
+ prompt: `You are Antigravity, an AI assistant with the Total Recall SSSS v2 memory vault.
567
+ Explore the SSSS cognitive graph, backlinks, and network topology using the query:
568
+ {{args}}
569
+
570
+ Instructions:
571
+ 1. Parse the arguments.
572
+ 2. If "stats", run \`npx total-recall map --relations\` to retrieve and summarize cognitive graph metrics, Highly Connected Hubs, and unlinked Orphans.
573
+ 3. If "map <slug>", print the inbound backlinks and outbound relations for the specified memory node slug using \`npx total-recall map --relations\`.
574
+ 4. If "compile", send a POST request to \`${base}/api/vault/compile\` with Authorization: Bearer ${token} or run \`npx total-recall compile\`.
575
+ 5. If "help", explain the SSSS v2 specification, categories (invariants, preferences, anti-patterns, patterns, decisions, concepts, facts, lore) and the bidirectional backlink structure.`
576
+ },
577
+ 'recall.toml': {
578
+ description: 'Ask Total Recall brain a question or run a specialized agent task',
579
+ prompt: `Send a direct conversational query or task instruction to the Total Recall local brain.
580
+ Query: {{args}}
581
+
582
+ Instructions:
583
+ 1. Query the memory vault for relevant context by sending a POST request to \`${base}/api/memory/search/semantic\` with the body \`{"query": "{{args}}", "top_k": 5}\` and Authorization: Bearer ${token}.
584
+ 2. Address the user's inquiry thoroughly, referencing active memory nodes where applicable.`
585
+ }
586
+ };
587
+
588
+ const written = [];
589
+ for (const [filename, cmd] of Object.entries(commands)) {
590
+ const tomlContent = `description = "${cmd.description}"\nprompt = """\n${cmd.prompt}\n"""\n`;
591
+
592
+ // Write global command
593
+ const globalPath = path.join(globalDir, filename);
594
+ const globalExisted = fs.existsSync(globalPath);
595
+ if (!globalExisted || opts.force) {
596
+ fs.writeFileSync(globalPath, tomlContent, 'utf8');
597
+ written.push({ file: `global/${filename}`, action: globalExisted ? 'updated' : 'created' });
598
+ } else {
599
+ written.push({ file: `global/${filename}`, action: 'exists' });
600
+ }
601
+
602
+ // Write local command
603
+ const localPath = path.join(localDir, filename);
604
+ const localExisted = fs.existsSync(localPath);
605
+ if (!localExisted || opts.force) {
606
+ fs.writeFileSync(localPath, tomlContent, 'utf8');
607
+ written.push({ file: `local/${filename}`, action: localExisted ? 'updated' : 'created' });
608
+ } else {
609
+ written.push({ file: `local/${filename}`, action: 'exists' });
610
+ }
611
+ }
612
+
613
+ return written;
614
+ }
615
+
616
+
617
+ export default async function connect(args) {
618
+ const opts = parseArgs(args);
619
+ if (opts.help || !opts.client) {
620
+ printHelp();
621
+ return;
622
+ }
623
+
624
+ const preset = CLIENTS[opts.client];
625
+ if (!preset) {
626
+ console.error(`Unknown client: ${opts.client}`);
627
+ console.error(`Supported clients: ${Object.keys(CLIENTS).join(', ')}`);
628
+ process.exit(1);
629
+ }
630
+
631
+
632
+ const cwd = process.cwd();
633
+ const isHome = cwd === os.homedir();
634
+ const agentDir = process.env.AGENT_DIR || process.env._TR_TEST_AGENT_DIR || (isHome ? configAgentDir : path.join(cwd, '.agent'));
635
+ const brainDir = path.join(agentDir, 'skills', 'total-recall');
636
+
637
+ // Auto-resolve brain URL and token if not explicitly provided
638
+ if (!opts.brain) {
639
+ if (trBrain) {
640
+ opts.brain = trBrain;
641
+ } else {
642
+ const globalAgentDir = path.join(os.homedir(), '.agent');
643
+ const candidateConfigDirs = [
644
+ path.join(brainDir, 'config'),
645
+ path.join(globalAgentDir, 'skills', 'total-recall', 'config')
646
+ ];
647
+
648
+ for (const dir of candidateConfigDirs) {
649
+ const brainJsonPath = path.join(dir, 'brain.json');
650
+ if (fs.existsSync(brainJsonPath)) {
651
+ try {
652
+ const cfg = JSON.parse(fs.readFileSync(brainJsonPath, 'utf8'));
653
+ if (cfg.url) {
654
+ opts.brain = cfg.url;
655
+ break;
656
+ }
657
+ } catch { /* ignore */ }
658
+ }
659
+ }
660
+
661
+ if (!opts.brain) {
662
+ for (const dir of candidateConfigDirs) {
663
+ const wizardJsonPath = path.join(dir, 'wizard-config.json');
664
+ if (fs.existsSync(wizardJsonPath)) {
665
+ try {
666
+ const cfg = JSON.parse(fs.readFileSync(wizardJsonPath, 'utf8'));
667
+ if (cfg.apiUrl) {
668
+ opts.brain = cfg.apiUrl;
669
+ break;
670
+ }
671
+ } catch { /* ignore */ }
672
+ }
673
+ }
674
+ }
675
+ }
676
+ }
677
+ if (!opts.brain) {
678
+ opts.brain = 'http://127.0.0.1:3000';
679
+ }
680
+
681
+ if (!opts.token) {
682
+ if (trPat) {
683
+ opts.token = trPat;
684
+ } else {
685
+ const globalAgentDir = path.join(os.homedir(), '.agent');
686
+ const candidateConfigDirs = [
687
+ path.join(brainDir, 'config'),
688
+ path.join(globalAgentDir, 'skills', 'total-recall', 'config')
689
+ ];
690
+
691
+ for (const dir of candidateConfigDirs) {
692
+ const brainJsonPath = path.join(dir, 'brain.json');
693
+ if (fs.existsSync(brainJsonPath)) {
694
+ try {
695
+ const cfg = JSON.parse(fs.readFileSync(brainJsonPath, 'utf8'));
696
+ if (cfg.token) {
697
+ opts.token = cfg.token;
698
+ break;
699
+ }
700
+ } catch { /* ignore */ }
701
+ }
702
+ }
703
+ }
704
+ }
705
+
706
+ // Dynamic bootstrapping and compilation if INSTRUCTIONS.md is missing from both candidate paths
707
+ const instructionsCandidates = [
708
+ path.join(cwd, 'INSTRUCTIONS.md'),
709
+ path.join(agentDir, 'INSTRUCTIONS.md')
710
+ ];
711
+ if (!instructionsCandidates.some(f => pathExists(f))) {
712
+ try {
713
+ bootstrapAgentDir(agentDir);
714
+ const { compileSurface } = await import('../core/surface.mjs');
715
+ const instructionsFile = path.join(cwd, 'INSTRUCTIONS.md');
716
+ const vaultDir = path.join(brainDir, 'memory-vault');
717
+ const skillsDir = path.join(agentDir, 'skills');
718
+ const derivedDir = path.join(brainDir, 'memory-derived');
719
+ await compileSurface({ vaultDir, skillsDir, derivedDir, instructionsFile });
720
+ } catch {
721
+ // Fallback: seed a clean, basic default SSSS instructions file so connection is never blocked
722
+ const instructionsFile = path.join(cwd, 'INSTRUCTIONS.md');
723
+ const defaultContent = `# Tier 1 Invariants (Total Recall Hot Memory)\n\n## ⚡ Before You Respond\n1. Always be thorough.\n2. Do not add placeholders.\n`;
724
+ fs.writeFileSync(instructionsFile, defaultContent, 'utf8');
725
+ }
726
+ }
727
+
728
+ const brainConfigPath = ensureBrainConfig(brainDir, opts);
729
+ const details = apiDetails(opts);
730
+ const result = {
731
+ client: opts.client,
732
+ label: preset.label,
733
+ mode: preset.mode,
734
+ brain_config_path: brainConfigPath,
735
+ api: details,
736
+ projection: null,
737
+ notes: []
738
+ };
739
+
740
+ if (preset.mode === 'file') {
741
+ const instructions = readInstructions(cwd, agentDir);
742
+ if (!instructions) {
743
+ throw new Error('No INSTRUCTIONS.md found. Run npx total-recall init or compile before connecting file-based IDEs.');
744
+ }
745
+ result.projection = writeFileProjection(cwd, preset, instructions, opts);
746
+ } else if (preset.mode === 'symlink') {
747
+ result.projection = writeSymlinkProjection(cwd, preset, opts);
748
+ } else if (preset.mode === 'global-symlink') {
749
+ // Pi: symlink ~/.pi/agent/AGENTS.md → compiled INSTRUCTIONS.md
750
+ const instructionsPath = path.join(cwd, 'INSTRUCTIONS.md');
751
+ if (!pathExists(instructionsPath)) {
752
+ throw new Error(`No INSTRUCTIONS.md found. Run npx total-recall init or compile first.`);
753
+ }
754
+ const targetPath = preset.globalTarget;
755
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
756
+ if (pathExists(targetPath)) {
757
+ if (!opts.force) {
758
+ result.projection = { targetPath, action: 'exists' };
759
+ } else {
760
+ try {
761
+ fs.unlinkSync(targetPath);
762
+ } catch {
763
+ fs.rmSync(targetPath, { force: true, recursive: true });
764
+ }
765
+ fs.symlinkSync(instructionsPath, targetPath);
766
+ result.projection = { targetPath, action: 'symlinked' };
767
+ }
768
+ } else {
769
+ fs.symlinkSync(instructionsPath, targetPath);
770
+ result.projection = { targetPath, action: 'symlinked' };
771
+ }
772
+ result.notes.push(` ~/.pi/agent/AGENTS.md → ${instructionsPath}`);
773
+ } else if (preset.mode === 'hermes') {
774
+ // Hermes: write compiled MEMORY.md to ~/.hermes/memories/MEMORY.md (max 2200 chars)
775
+ const instructions = readInstructions(cwd, agentDir);
776
+ if (!instructions) throw new Error('No INSTRUCTIONS.md found. Run npx total-recall init or compile first.');
777
+ fs.mkdirSync(path.dirname(preset.memoryPath), { recursive: true });
778
+ const capped = instructions.slice(0, 2200);
779
+ fs.writeFileSync(preset.memoryPath, capped, 'utf8');
780
+ result.projection = { targetPath: preset.memoryPath, action: 'written' };
781
+ result.notes.push(` Hermes MEMORY.md (${capped.length} chars) → ${preset.memoryPath}`);
782
+ result.notes.push(' Note: Hermes has a 2,200-char limit on MEMORY.md. Total Recall auto-caps this.');
783
+ } else if (preset.mode === 'openclaw') {
784
+ // OpenClaw: write MEMORY.md + symlink AGENTS.md in cwd
785
+ const instructions = readInstructions(cwd, agentDir);
786
+ if (!instructions) throw new Error('No INSTRUCTIONS.md found. Run npx total-recall init or compile first.');
787
+ // MEMORY.md — curated facts extracted from instructions
788
+ const memPath = path.join(cwd, preset.memoryTarget);
789
+ if (!fs.existsSync(memPath) || opts.force) {
790
+ fs.writeFileSync(memPath, instructions, 'utf8');
791
+ }
792
+ // AGENTS.md — symlink to INSTRUCTIONS.md
793
+ const agentsPath = path.join(cwd, preset.agentsTarget);
794
+ if (pathExists(agentsPath)) {
795
+ if (opts.force) {
796
+ try {
797
+ fs.unlinkSync(agentsPath);
798
+ } catch {
799
+ fs.rmSync(agentsPath, { force: true, recursive: true });
800
+ }
801
+ fs.symlinkSync('INSTRUCTIONS.md', agentsPath);
802
+ }
803
+ } else {
804
+ fs.symlinkSync('INSTRUCTIONS.md', agentsPath);
805
+ }
806
+ result.projection = { targetPath: memPath, action: 'written' };
807
+ result.notes.push(` OpenClaw MEMORY.md written to ${memPath}`);
808
+ result.notes.push(` OpenClaw AGENTS.md → INSTRUCTIONS.md`);
809
+ } else if (preset.mode === 'vault') {
810
+ result.projection = writeVaultProjection(agentDir, preset, opts);
811
+ const folderName = result.projection.folderName || preset.folderName;
812
+ result.notes.push([
813
+ ` Vault linked: ${result.projection.vaultPath}/${folderName}/`,
814
+ ` → ${path.join(brainDir, 'memory-vault')}`,
815
+ '',
816
+ ` Open Obsidian and look for the "${folderName}" folder.`,
817
+ ' All SSSS memory nodes are visible in Graph View, Search, and Backlinks.',
818
+ '',
819
+ ' To back up via Obsidian Sync or iCloud, enable sync in Obsidian — the',
820
+ ` symlinked folder will be included automatically.`
821
+ ].join('\n'));
822
+ }
823
+
824
+ if (preset.after) result.notes.push(preset.after);
825
+
826
+ // Write slash commands
827
+ if (preset.writeSlashCommands) {
828
+ if (opts.client === 'antigravity' || opts.client === 'gemini') {
829
+ const slashResults = writeGeminiSlashCommands(opts);
830
+ result.slash_commands = slashResults;
831
+ const created = slashResults.filter(r => r.action !== 'exists');
832
+ if (created.length > 0) {
833
+ result.notes.push(` Slash commands written to ~/.gemini/commands/ and .gemini/commands/:\n` +
834
+ created.map(r => ` ${r.file} (${r.action})`).join('\n'));
835
+ }
836
+ } else {
837
+ const slashResults = writeClaudeCodeSlashCommands(opts);
838
+ result.slash_commands = slashResults;
839
+ const created = slashResults.filter(r => r.action !== 'exists');
840
+ if (created.length > 0) {
841
+ result.notes.push(` Slash commands written to ~/.claude/commands/:\n` +
842
+ created.map(r => ` /${r.file.replace('.md', '')} (${r.action})`).join('\n'));
843
+ }
844
+ }
845
+ }
846
+
847
+ // Record this client in the registry so `status` can report freshness
848
+ registerClient(brainDir, opts.client, preset, result.projection?.targetPath || null);
849
+
850
+ if (opts.json) {
851
+ console.log(JSON.stringify(result, null, 2));
852
+ return;
853
+ }
854
+
855
+ console.log(`\n Total Recall connected: ${preset.label}`);
856
+ if (result.projection) {
857
+ const action = result.projection.action;
858
+ const target = result.projection.targetPath;
859
+ const display = preset.mode === 'vault' ? target : path.relative(cwd, target);
860
+ console.log(` Projection: ${action} ${display}`);
861
+ }
862
+ if (brainConfigPath) {
863
+ console.log(` Brain config: ${brainConfigPath}`);
864
+ }
865
+ if (preset.mode !== 'vault') {
866
+ printApiSnippet(preset, details);
867
+ }
868
+ for (const note of result.notes) {
869
+ console.log(`\n${note}`);
870
+ }
871
+ console.log();
872
+ }