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,613 @@
1
+ /**
2
+ * total-recall uninstall
3
+ *
4
+ * Completely stop and remove all Total Recall services, launchd agents,
5
+ * cron jobs, VFS global directories, and local workspace shims.
6
+ *
7
+ * Usage:
8
+ * npx total-recall uninstall
9
+ *
10
+ * ╔══════════════════════════════════════════════════════════════════════════╗
11
+ * ║ ⛔⛔⛔ CRITICAL WARNING — DO NOT IMPLEMENT --no-backup FLAG ⛔⛔⛔ ║
12
+ * ║ ║
13
+ * ║ There must NEVER be a --no-backup, --skip-backup, or ANY flag that ║
14
+ * ║ allows skipping the backup step before uninstallation. ║
15
+ * ║ ║
16
+ * ║ A --no-backup flag was previously implemented by an AI agent and ║
17
+ * ║ caused CATASTROPHIC, IRRECOVERABLE DATA LOSS of the user's entire ║
18
+ * ║ brain (memory vault). This must NEVER happen again. ║
19
+ * ║ ║
20
+ * ║ The backup step is MANDATORY and NON-NEGOTIABLE. If the backup ║
21
+ * ║ fails, the uninstall MUST abort. There are NO exceptions. ║
22
+ * ║ ║
23
+ * ║ DO NOT ADD: --no-backup, --skip-backup, --force, noBackup, ║
24
+ * ║ skipBackup, or ANY variation that bypasses the backup. ║
25
+ * ╚══════════════════════════════════════════════════════════════════════════╝
26
+ */
27
+
28
+ import fs from 'node:fs';
29
+ import os from 'node:os';
30
+ import path from 'node:path';
31
+ import { spawnSync, execFileSync } from 'node:child_process';
32
+ import backup from './backup.mjs';
33
+
34
+ // ╔══════════════════════════════════════════════════════════════════════════╗
35
+ // ║ FORBIDDEN FLAGS — These patterns must NEVER be accepted as arguments ║
36
+ // ║ See top-of-file warning for full context on the data loss incident. ║
37
+ // ╚══════════════════════════════════════════════════════════════════════════╝
38
+ const FORBIDDEN_FLAGS = [
39
+ '--no-backup', '--nobackup', '--skip-backup', '--skipbackup',
40
+ '--no_backup', '--skip_backup',
41
+ ];
42
+
43
+ // ─── Logging Helpers ────────────────────────────────────────────────────────
44
+ function log(msg) { console.error(` ${msg}`); }
45
+ function logStep(n, msg) { console.error(`\n [${n}] ${msg}`); }
46
+ function logOk(msg) { console.error(` ✅ ${msg}`); }
47
+ function logSkip(msg) { console.error(` ⏭ ${msg}`); }
48
+ function logWarn(msg) { console.error(` ⚠️ ${msg}`); }
49
+
50
+ function commandExists(cmd) {
51
+ if (!cmd || !/^[a-zA-Z0-9_-]+$/.test(cmd)) {
52
+ return false;
53
+ }
54
+ try {
55
+ execFileSync('which', [cmd], { stdio: 'ignore' });
56
+ return true;
57
+ } catch { return false; }
58
+ }
59
+
60
+ function runCommand(cmd, args, opts = {}) {
61
+ try {
62
+ const result = spawnSync(cmd, args, { stdio: 'ignore', ...opts });
63
+ return result.status === 0;
64
+ } catch {
65
+ return false;
66
+ }
67
+ }
68
+
69
+ function hasLaunchd() {
70
+ return os.platform() === 'darwin';
71
+ }
72
+
73
+ function hasSystemd() {
74
+ return os.platform() === 'linux' && commandExists('systemctl');
75
+ }
76
+
77
+ // ─── Main ───────────────────────────────────────────────────────────────────
78
+ export default async function uninstall(args = []) {
79
+
80
+ // ⛔ RUNTIME GUARD: Reject any forbidden flags that skip backup.
81
+ // This guard exists because a --no-backup flag was previously added by an
82
+ // AI agent and caused catastrophic data loss. It must NEVER be bypassed.
83
+ for (const arg of args) {
84
+ const normalized = arg.toLowerCase().replace(/_/g, '-');
85
+ if (FORBIDDEN_FLAGS.includes(normalized)) {
86
+ console.error(`\n ⛔ FATAL: The "${arg}" flag is PERMANENTLY FORBIDDEN.`);
87
+ console.error(` Backups are MANDATORY before uninstall. This flag was removed`);
88
+ console.error(` after it caused catastrophic data loss. Aborting.\n`);
89
+ process.exit(1);
90
+ }
91
+ }
92
+
93
+ const HOME = os.homedir();
94
+ console.error(`
95
+ ┌─────────────────────────────────────────────────────────┐
96
+ │ Total Recall Uninstaller │
97
+ │ Performing a complete system teardown... │
98
+ └─────────────────────────────────────────────────────────┘
99
+ `);
100
+
101
+ const globalAgentDir = path.join(HOME, '.agent');
102
+ const localAgentDir = path.join(process.cwd(), '.agent');
103
+
104
+ let pushGit = null;
105
+ let purge = false;
106
+
107
+ for (let i = 0; i < args.length; i++) {
108
+ if (args[i] === '--push-git') {
109
+ pushGit = args[++i];
110
+ } else if (args[i] === '--purge') {
111
+ purge = true;
112
+ }
113
+ }
114
+
115
+ // Detect configured backup git remote if present
116
+ let detectedRemote = null;
117
+ const SKILL_DIR = path.join(localAgentDir, 'skills', 'total-recall');
118
+ if (!pushGit && fs.existsSync(SKILL_DIR)) {
119
+ try {
120
+ const remoteCheck = spawnSync('git', ['remote', 'get-url', 'backup'], { cwd: SKILL_DIR, stdio: 'pipe' });
121
+ if (remoteCheck.status === 0) {
122
+ detectedRemote = remoteCheck.stdout.toString().trim();
123
+ } else {
124
+ const originCheck = spawnSync('git', ['remote', 'get-url', 'origin'], { cwd: SKILL_DIR, stdio: 'pipe' });
125
+ if (originCheck.status === 0) {
126
+ detectedRemote = originCheck.stdout.toString().trim();
127
+ }
128
+ }
129
+ } catch {
130
+ // Ignored
131
+ }
132
+ }
133
+
134
+ const targetRemote = pushGit || detectedRemote;
135
+ if (targetRemote) {
136
+ logStep('0/4', `Automatically backing up brain to remote: ${targetRemote}`);
137
+ try {
138
+ await backup(['--push-git', targetRemote]);
139
+ logOk('Pre-uninstall backup successfully pushed to remote!');
140
+ } catch (err) {
141
+ logWarn(`Git backup push failed: ${err.message}`);
142
+ logWarn('Falling back to local tarball backup...');
143
+ try {
144
+ await backup(['--no-encrypt']);
145
+ logOk('Local tarball backup created successfully.');
146
+ } catch (tarErr) {
147
+ logWarn(`Local tarball backup also failed: ${tarErr.message}`);
148
+ logWarn('Teardown aborted to protect your brain. Resolve backup issues and try again.');
149
+ return;
150
+ }
151
+ }
152
+ } else if (fs.existsSync(SKILL_DIR)) {
153
+ logStep('0/4', 'No git remote or Obsidian vault configured — creating local tarball backup');
154
+ try {
155
+ await backup(['--no-encrypt']);
156
+ logOk('Local tarball backup created before uninstall.');
157
+ } catch (err) {
158
+ logWarn(`Local tarball backup failed: ${err.message}`);
159
+ logWarn('Teardown aborted to protect your brain. Resolve backup issues and try again.');
160
+ return;
161
+ }
162
+ } else if (fs.existsSync(localAgentDir) || fs.existsSync(globalAgentDir)) {
163
+ // ⛔ SAFETY NET: No skill dir found, but .agent/ directories exist with
164
+ // potential data (memory-vault, config, secrets, etc.). Back up the raw
165
+ // directory before proceeding so we never silently delete user data.
166
+ const agentDirToBackup = fs.existsSync(localAgentDir) ? localAgentDir : globalAgentDir;
167
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
168
+ const fallbackTarball = path.join(HOME, `total-recall-backup-${stamp}.tar.gz`);
169
+ logStep('0/4', `No skill dir found but .agent/ exists — creating safety backup of ${agentDirToBackup}`);
170
+ try {
171
+ const parentDir = path.dirname(agentDirToBackup);
172
+ const baseName = path.basename(agentDirToBackup);
173
+ const result = spawnSync('tar', ['-czf', fallbackTarball, '-C', parentDir, baseName], { stdio: 'pipe' });
174
+ if (result.status !== 0) {
175
+ throw new Error(`tar exited with status ${result.status}`);
176
+ }
177
+ logOk(`Safety backup created: ${fallbackTarball}`);
178
+ } catch (err) {
179
+ logWarn(`Safety backup failed: ${err.message}`);
180
+ logWarn('Teardown aborted to protect your data. Resolve backup issues and try again.');
181
+ return;
182
+ }
183
+ }
184
+
185
+ // ── Step 1: Stop and disable background services ──
186
+ logStep('1/4', 'Stopping and disabling background services');
187
+
188
+ if (hasLaunchd()) {
189
+ const plists = [
190
+ 'com.totalrecall.daemon',
191
+ 'com.totalrecall.server',
192
+ 'com.totalrecall.relay',
193
+ 'com.totalrecall.duckdns',
194
+ 'com.totalrecall.backup',
195
+ 'com.totalrecall.obsidian-backup'
196
+ ];
197
+
198
+ const launchAgentsDir = path.join(HOME, 'Library', 'LaunchAgents');
199
+
200
+ for (const plist of plists) {
201
+ const plistPath = path.join(launchAgentsDir, `${plist}.plist`);
202
+ if (fs.existsSync(plistPath)) {
203
+ log(`Unloading ${plist}...`);
204
+ runCommand('launchctl', ['unload', '-w', plistPath]);
205
+ try {
206
+ fs.unlinkSync(plistPath);
207
+ logOk(`Removed LaunchAgent plist: ${plist}.plist`);
208
+ } catch (err) {
209
+ logWarn(`Failed to remove plist file: ${err.message}`);
210
+ }
211
+ } else {
212
+ log(`No launchd agent file found for ${plist}`);
213
+ }
214
+ }
215
+ } else if (hasSystemd()) {
216
+ const userServices = [
217
+ 'total-recall-daemon.service',
218
+ 'total-recall-server.service',
219
+ 'total-recall-relay.service'
220
+ ];
221
+
222
+ for (const svc of userServices) {
223
+ log(`Stopping user systemd service ${svc}...`);
224
+ runCommand('systemctl', ['--user', 'disable', '--now', svc]);
225
+ const svcPath = path.join(HOME, '.config', 'systemd', 'user', svc);
226
+ if (fs.existsSync(svcPath)) {
227
+ try {
228
+ fs.unlinkSync(svcPath);
229
+ logOk(`Removed systemd service file: ${svc}`);
230
+ } catch (err) {
231
+ logWarn(`Failed to remove service file: ${err.message}`);
232
+ }
233
+ }
234
+ }
235
+ runCommand('systemctl', ['--user', 'daemon-reload']);
236
+
237
+ // Check system services
238
+ const rootServices = [
239
+ 'total-recall-daemon.service',
240
+ 'total-recall-server.service',
241
+ 'searxng.service'
242
+ ];
243
+ let reloadedRootSystemd = false;
244
+ for (const svc of rootServices) {
245
+ const dest = `/etc/systemd/system/${svc}`;
246
+ if (fs.existsSync(dest)) {
247
+ log(`Stopping system-wide systemd service ${svc} (requires sudo)...`);
248
+ spawnSync('sudo', ['systemctl', 'disable', '--now', svc], { stdio: 'inherit' });
249
+ try {
250
+ spawnSync('sudo', ['rm', '-f', dest], { stdio: 'inherit' });
251
+ logOk(`Removed system service file: ${dest}`);
252
+ reloadedRootSystemd = true;
253
+ } catch (err) {
254
+ logWarn(`Failed to remove system service file: ${err.message}`);
255
+ }
256
+ }
257
+ }
258
+ if (reloadedRootSystemd) {
259
+ spawnSync('sudo', ['systemctl', 'daemon-reload'], { stdio: 'inherit' });
260
+ }
261
+
262
+ // Cron jobs
263
+ const cronJobs = [
264
+ '/etc/cron.d/total-recall-duckdns',
265
+ '/etc/cron.d/total-recall-backup',
266
+ '/etc/cron.d/total-recall-obsidian-backup'
267
+ ];
268
+ for (const job of cronJobs) {
269
+ if (fs.existsSync(job)) {
270
+ log(`Removing cron job: ${job} (requires sudo)...`);
271
+ try {
272
+ spawnSync('sudo', ['rm', '-f', job], { stdio: 'inherit' });
273
+ logOk(`Removed cron file: ${job}`);
274
+ } catch (err) {
275
+ logWarn(`Failed to remove cron file: ${err.message}`);
276
+ }
277
+ }
278
+ }
279
+ } else {
280
+ log('No systemd or launchd detected. Moving to direct process management.');
281
+ }
282
+
283
+ // ── Step 2: Stop detached background Node.js processes ──
284
+ logStep('2/4', 'Stopping any standalone background processes');
285
+
286
+ // Stop SearXNG background processes if running via nohup/direct python command
287
+ try {
288
+ const listProc = spawnSync('ps', ['ax'], { encoding: 'utf8' });
289
+ if (listProc.status === 0) {
290
+ const lines = listProc.stdout.split('\n');
291
+ for (const line of lines) {
292
+ if (line.includes('searx.webapp') && !line.includes('grep')) {
293
+ const parts = line.trim().split(/\s+/);
294
+ const pid = parseInt(parts[0], 10);
295
+ if (!isNaN(pid)) {
296
+ log(`Stopping background SearXNG process (PID ${pid})...`);
297
+ process.kill(pid, 'SIGTERM');
298
+ logOk(`Stopped SearXNG background process (PID ${pid})`);
299
+ }
300
+ }
301
+ }
302
+ }
303
+ } catch (err) {
304
+ // Non-fatal
305
+ }
306
+
307
+ // Stop Cloudflare quick tunnels spawned by deploy/init
308
+ try {
309
+ const listProc = spawnSync('ps', ['ax'], { encoding: 'utf8' });
310
+ if (listProc.status === 0) {
311
+ const lines = listProc.stdout.split('\n');
312
+ for (const line of lines) {
313
+ if (line.includes('cloudflared tunnel --url') && !line.includes('grep')) {
314
+ const parts = line.trim().split(/\s+/);
315
+ const pid = parseInt(parts[0], 10);
316
+ if (!isNaN(pid)) {
317
+ log(`Stopping background cloudflared quick tunnel (PID ${pid})...`);
318
+ process.kill(pid, 'SIGTERM');
319
+ logOk(`Stopped cloudflared quick tunnel (PID ${pid})`);
320
+ }
321
+ }
322
+ }
323
+ }
324
+ } catch (err) {
325
+ // Non-fatal
326
+ }
327
+
328
+ const dirsToScan = [
329
+ path.join(globalAgentDir, 'skills', 'total-recall'),
330
+ path.join(localAgentDir, 'skills', 'total-recall'),
331
+ ];
332
+ for (const dir of dirsToScan) {
333
+ if (!fs.existsSync(dir)) continue;
334
+ const logsDir = path.join(dir, 'logs');
335
+ if (!fs.existsSync(logsDir)) continue;
336
+
337
+ const pidFiles = ['daemon.pid', 'relay.pid'];
338
+ for (const file of pidFiles) {
339
+ const p = path.join(logsDir, file);
340
+ if (fs.existsSync(p)) {
341
+ try {
342
+ const pid = parseInt(fs.readFileSync(p, 'utf8').trim(), 10);
343
+ if (!isNaN(pid)) {
344
+ // Check if alive
345
+ try {
346
+ process.kill(pid, 0);
347
+ log(`Sending SIGTERM to detached process (PID ${pid})...`);
348
+ process.kill(pid, 'SIGTERM');
349
+ // Give it a brief moment to clean up
350
+ let killed = false;
351
+ for (let i = 0; i < 5; i++) {
352
+ try {
353
+ process.kill(pid, 0);
354
+ spawnSync('sleep', ['0.2']);
355
+ } catch {
356
+ killed = true;
357
+ break;
358
+ }
359
+ }
360
+ if (!killed) {
361
+ logWarn(`Process ${pid} did not exit. Sending SIGKILL...`);
362
+ process.kill(pid, 'SIGKILL');
363
+ }
364
+ logOk(`Stopped process ${pid} (${file})`);
365
+ } catch {
366
+ log(`Process PID ${pid} is already stopped.`);
367
+ }
368
+ }
369
+ fs.unlinkSync(p);
370
+ } catch (err) {
371
+ logWarn(`Could not cleanly stop process from PID file ${file}: ${err.message}`);
372
+ }
373
+ }
374
+ }
375
+ }
376
+
377
+ // ── Step 3: Clean up workspace shims ──
378
+ logStep('3/4', 'Cleaning up active workspace shims in current directory');
379
+
380
+ const shimFiles = [
381
+ '.clauderules',
382
+ 'CLAUDE.md',
383
+ '.cursorrules',
384
+ 'AGENTS.md',
385
+ 'GEMINI.md',
386
+ '.github/copilot-instructions.md',
387
+ '.vscode/copilot-instructions.md',
388
+ '.windsurfrules',
389
+ 'WINDSURF.md',
390
+ 'INSTRUCTIONS.md'
391
+ ];
392
+
393
+ for (const file of shimFiles) {
394
+ const filePath = path.join(process.cwd(), file);
395
+ if (!fs.existsSync(filePath)) continue;
396
+
397
+ try {
398
+ const stat = fs.lstatSync(filePath);
399
+ if (stat.isSymbolicLink()) {
400
+ const target = fs.readlinkSync(filePath);
401
+ if (target.includes('.agent') || target.includes('INSTRUCTIONS.md')) {
402
+ fs.unlinkSync(filePath);
403
+ logOk(`Removed symlink: ${file} → ${target}`);
404
+ } else {
405
+ logSkip(`Preserved symlink ${file} (points to ${target}, not part of Total Recall)`);
406
+ }
407
+ } else {
408
+ // Normal file
409
+ let content = fs.readFileSync(filePath, 'utf8');
410
+ const INJECT_PATTERNS = [
411
+ /<!-- BEGIN INJECTED MEMORY:.*?-->[\s\S]*?<!-- END INJECTED MEMORY -->\n?/g,
412
+ /<!-- BEGIN INJECTED ACTIVE DIRECTIVES:.*?-->[\s\S]*?<!-- END INJECTED ACTIVE DIRECTIVES -->\n?/g,
413
+ /<!-- BEGIN INJECTED MEMORY -->[\s\S]*?<!-- END INJECTED MEMORY -->\n?/g,
414
+ /<!-- BEGIN INJECTED ACTIVE DIRECTIVES -->[\s\S]*?<!-- END INJECTED ACTIVE DIRECTIVES -->\n?/g,
415
+ ];
416
+
417
+ let cleanedContent = content;
418
+ let changed = false;
419
+ for (const pat of INJECT_PATTERNS) {
420
+ const replaced = cleanedContent.replace(pat, '');
421
+ if (replaced !== cleanedContent) {
422
+ cleanedContent = replaced;
423
+ changed = true;
424
+ }
425
+ }
426
+
427
+ cleanedContent = cleanedContent.trim();
428
+
429
+ // Also remove "Tier 1 Invariants" headers if injected by total-recall init/compile
430
+ if (cleanedContent.startsWith('# Tier 1 Invariants (Total Recall Hot Memory)')) {
431
+ const nextHeading = cleanedContent.indexOf('\n#', 40);
432
+ if (nextHeading !== -1) {
433
+ cleanedContent = cleanedContent.slice(nextHeading).trim();
434
+ } else {
435
+ cleanedContent = '';
436
+ }
437
+ changed = true;
438
+ }
439
+
440
+ const baseline = 'Read and follow .agent/skills/total-recall/SKILL.md on every turn.';
441
+ if (changed) {
442
+ if (cleanedContent === '' || cleanedContent === baseline) {
443
+ fs.unlinkSync(filePath);
444
+ logOk(`Removed fully empty or baseline-only rule file: ${file}`);
445
+ } else {
446
+ fs.writeFileSync(filePath, cleanedContent, 'utf8');
447
+ logOk(`Cleaned injected Total Recall block from: ${file}`);
448
+ }
449
+ } else if (file === 'INSTRUCTIONS.md' && (content.includes('# Invariant Memory') || content.includes('# Tier 1 Invariants'))) {
450
+ // If INSTRUCTIONS.md is primarily our file, delete it
451
+ fs.unlinkSync(filePath);
452
+ logOk(`Removed compiled instructions file: ${file}`);
453
+ } else {
454
+ logSkip(`No Total Recall injected blocks found in: ${file}`);
455
+ }
456
+ }
457
+ } catch (err) {
458
+ logWarn(`Failed to audit/clean rule file ${file}: ${err.message}`);
459
+ }
460
+ }
461
+
462
+ // ── Step 4: Purge Total Recall data ──
463
+ logStep('4/4', 'Removing Total Recall brain and runtime artifacts');
464
+
465
+ // ── Unregister project brain from global project registry ──
466
+ try {
467
+ const globalBrainDir = path.join(HOME, '.agent', 'skills', 'total-recall');
468
+ const registryPath = path.join(globalBrainDir, 'config', 'project-registry.json');
469
+ if (fs.existsSync(registryPath)) {
470
+ let registry = [];
471
+ try { registry = JSON.parse(fs.readFileSync(registryPath, 'utf8')); } catch { registry = []; }
472
+ const initialLen = registry.length;
473
+ registry = registry.filter(p => p.path !== process.cwd());
474
+ if (registry.length < initialLen) {
475
+ fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2));
476
+ logOk(`Unregistered project "${path.basename(process.cwd())}" from global brain registry`);
477
+ }
478
+ }
479
+ } catch (err) {
480
+ logWarn(`Failed to unregister project: ${err.message}`);
481
+ }
482
+
483
+ // Purge optional SearXNG directories if --purge is specified
484
+ if (purge) {
485
+ log('Purging optional systems (SearXNG directories)...');
486
+ try {
487
+ if (fs.existsSync('/opt/searxng') || fs.existsSync('/etc/searxng')) {
488
+ log('Removing SearXNG configuration and install directories (requires sudo)...');
489
+ spawnSync('sudo', ['rm', '-rf', '/opt/searxng', '/etc/searxng'], { stdio: 'inherit' });
490
+ logOk('Removed SearXNG system directories (/opt/searxng and /etc/searxng)');
491
+ }
492
+ } catch (err) {
493
+ logWarn(`Failed to purge SearXNG system directories: ${err.message}`);
494
+ }
495
+ }
496
+
497
+ // What we remove:
498
+ // .agent/skills/total-recall/ — the brain (already backed up)
499
+ // .agent/INSTRUCTIONS.md, AGENTS.md, etc. — compiled shims
500
+ // What we NEVER touch:
501
+ // .agent/ — always stays
502
+ // .agent/skills/ — always stays (user may have other skills)
503
+ // .agent/skills/<other>/ — other user skills, never ours to delete
504
+
505
+ // ── Preserve credentials before purge ──
506
+ for (const agentDir of [globalAgentDir, localAgentDir]) {
507
+ if (!fs.existsSync(agentDir)) continue;
508
+ try {
509
+ const brainPath = path.join(agentDir, 'skills', 'total-recall');
510
+ const secretsPath = path.join(brainPath, 'config', 'secrets.enc');
511
+ const securityPath = path.join(brainPath, 'config', 'security.yml');
512
+
513
+ let secretsObj = {};
514
+ if (fs.existsSync(secretsPath)) {
515
+ try {
516
+ secretsObj = JSON.parse(fs.readFileSync(secretsPath, 'utf8') || '{}');
517
+ } catch {}
518
+ }
519
+
520
+ if (fs.existsSync(securityPath)) {
521
+ try {
522
+ const { default: yaml } = await import('yaml');
523
+ const securityObj = yaml.parse(fs.readFileSync(securityPath, 'utf8')) || {};
524
+ if (securityObj.dashboard?.password_hash) {
525
+ secretsObj.dashboard_password_hash = securityObj.dashboard.password_hash;
526
+ }
527
+ } catch {}
528
+ }
529
+
530
+ if (Object.keys(secretsObj).length > 0) {
531
+ fs.mkdirSync(agentDir, { recursive: true });
532
+ fs.writeFileSync(
533
+ path.join(agentDir, 'secrets.enc'),
534
+ JSON.stringify(secretsObj, null, 2),
535
+ { encoding: 'utf8', mode: 0o600 }
536
+ );
537
+ const label = agentDir === globalAgentDir ? 'Global' : 'Local';
538
+ logOk(`${label}: Preserved credentials to ${path.relative(process.cwd(), path.join(agentDir, 'secrets.enc'))}`);
539
+ }
540
+ } catch (err) {
541
+ logWarn(`Failed to preserve credentials for ${agentDir}: ${err.message}`);
542
+ }
543
+ }
544
+
545
+ for (const agentDir of [globalAgentDir, localAgentDir]) {
546
+ if (!fs.existsSync(agentDir)) {
547
+ logSkip(`${agentDir === globalAgentDir ? 'Global' : 'Local'} .agent VFS is not present`);
548
+ continue;
549
+ }
550
+ const label = agentDir === globalAgentDir ? 'Global' : 'Local';
551
+ log(`Cleaning ${label} .agent at ${agentDir}...`);
552
+
553
+
554
+ // 1. Remove the brain: .agent/skills/total-recall/
555
+ const brainPath = path.join(agentDir, 'skills', 'total-recall');
556
+ if (fs.existsSync(brainPath)) {
557
+ try {
558
+ fs.rmSync(brainPath, { recursive: true, force: true });
559
+ logOk(`${label}: Removed brain (skills/total-recall/)`);
560
+ } catch (err) {
561
+ logWarn(`${label}: Failed to remove brain: ${err.message}`);
562
+ }
563
+ }
564
+
565
+ // 2. Strip Total Recall injected blocks from IDE instruction files.
566
+ // These files belong to the IDE ecosystem, NOT to Total Recall.
567
+ // TR only injects managed blocks between comment markers.
568
+ const shimFiles = ['INSTRUCTIONS.md', 'AGENTS.md', 'CLAUDE.md', 'GEMINI.md',
569
+ 'CODEX.md', '.clauderules', '.cursorrules', '.codexrules',
570
+ 'WINDSURF.md', '.windsurfrules'];
571
+ const INJECT_PATTERNS = [
572
+ /<!-- BEGIN INJECTED MEMORY:.*?-->[\s\S]*?<!-- END INJECTED MEMORY -->\n?/g,
573
+ /<!-- BEGIN INJECTED ACTIVE DIRECTIVES:.*?-->[\s\S]*?<!-- END INJECTED ACTIVE DIRECTIVES -->\n?/g,
574
+ /<!-- BEGIN INJECTED MEMORY -->[\s\S]*?<!-- END INJECTED MEMORY -->\n?/g,
575
+ /<!-- BEGIN INJECTED ACTIVE DIRECTIVES -->[\s\S]*?<!-- END INJECTED ACTIVE DIRECTIVES -->\n?/g,
576
+ ];
577
+ // Check both .agent/ level and project root
578
+ const shimDirs = [agentDir, agentDir === localAgentDir ? path.dirname(localAgentDir) : null].filter(Boolean);
579
+ for (const dir of shimDirs) {
580
+ for (const file of shimFiles) {
581
+ const p = path.join(dir, file);
582
+ if (!fs.existsSync(p)) continue;
583
+ try {
584
+ let content = fs.readFileSync(p, 'utf8');
585
+ let changed = false;
586
+ for (const pat of INJECT_PATTERNS) {
587
+ const cleaned = content.replace(pat, '');
588
+ if (cleaned !== content) { content = cleaned; changed = true; }
589
+ }
590
+ if (changed) {
591
+ const trimmed = content.trim();
592
+ const baseline = 'Read and follow .agent/skills/total-recall/SKILL.md on every turn.';
593
+ // If only whitespace or the baseline pointer remains, delete the file completely
594
+ if (trimmed.length === 0 || trimmed === baseline) {
595
+ fs.rmSync(p, { force: true });
596
+ } else {
597
+ fs.writeFileSync(p, content, 'utf8');
598
+ }
599
+ }
600
+ } catch {}
601
+ }
602
+ }
603
+ logOk(`${label}: Stripped Total Recall injections from IDE instruction files`);
604
+ }
605
+
606
+ console.error(`
607
+ ┌─────────────────────────────────────────────────────────┐
608
+ │ ✅ Uninstall Complete! │
609
+ │ Total Recall has been successfully removed from your │
610
+ │ system, workspaces, and background agents. │
611
+ └─────────────────────────────────────────────────────────┘
612
+ `);
613
+ }