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.
- package/LICENSE +21 -0
- package/README.md +173 -0
- package/bin/antigravity.mjs +212 -0
- package/bin/init.mjs +36 -0
- package/bin/oci-sniper.sh +69 -0
- package/bin/total-recall.mjs +229 -0
- package/docs/ARCHITECTURE.md +251 -0
- package/docs/IDE_INTEGRATION.md +112 -0
- package/package.json +71 -0
- package/scaffold/.agent/skills/total-recall/SKILL.md +195 -0
- package/scaffold/.agent/skills/total-recall/evals/evals.json +20 -0
- package/scaffold/.agent/skills/total-recall/memory-vault/concepts/cli-help-reference.md +145 -0
- package/scaffold/.agent/skills/total-recall/memory-vault/invariants/operating-instructions.md +75 -0
- package/scaffold/.agent/skills/total-recall/memory-vault/preferences/always-websearch-gap.md +16 -0
- package/scaffold/.agent/skills/total-recall/memory-vault/preferences/topic-research-sop.md +17 -0
- package/scaffold/.agent/skills/total-recall/references/api-reference.md +187 -0
- package/scaffold/.agent/skills/total-recall/references/architecture-reference.md +81 -0
- package/scaffold/.agent/skills/total-recall/references/cli-reference.md +103 -0
- package/scaffold/.agent/skills/total-recall/references/ssss-reference.md +88 -0
- package/scaffold/.agent/skills/total-recall/references/troubleshooting.md +68 -0
- package/scaffold/.agent/skills/total-recall/scripts/sync-repo.mjs +83 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/SKILL.md +122 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/agents.yml +27 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/antigravity.txt +20 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/claude.txt +58 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/codex.txt +81 -0
- package/scaffold/.agent/skills/total-recall/skills/cli-agents/references/gemini.txt +44 -0
- package/scaffold/.agent/skills/total-recall/skills/research/SKILL.md +337 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/SKILL.md +281 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/assets/skill-template/SKILL.md +20 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/evals/evals.json +20 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/hooks/on-skill-add.sh +14 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/hooks/on-skill-delete.sh +14 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/learnings/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/references/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/create-skill.sh +65 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/enforce-skill-optimization.mjs +135 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/find-skills.mjs +124 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/scan-skill.mjs +207 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/scripts/skill-watcher.mjs +64 -0
- package/scaffold/.agent/skills/total-recall/skills/skill/subagents/audit-all-skills.md +62 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/SKILL.md +27 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/evals/evals.json +17 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/references/ssss-spec.md +117 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/scripts/validate-schema.mjs +54 -0
- package/scaffold/.agent/skills/total-recall/skills/ssss/subagents/audit-ssss-node.md +9 -0
- package/scaffold/.agent/skills/total-recall/subagents/total-recall-diagnostician.md +13 -0
- package/src/cli/agent-dir.mjs +105 -0
- package/src/cli/backup.mjs +313 -0
- package/src/cli/brain.mjs +224 -0
- package/src/cli/chat.mjs +74 -0
- package/src/cli/config.mjs +247 -0
- package/src/cli/connect.mjs +872 -0
- package/src/cli/daemon.mjs +228 -0
- package/src/cli/deploy-ui.mjs +1793 -0
- package/src/cli/deploy.mjs +1726 -0
- package/src/cli/doctor.mjs +254 -0
- package/src/cli/dream.mjs +80 -0
- package/src/cli/forget.mjs +92 -0
- package/src/cli/friction.mjs +21 -0
- package/src/cli/generate-pat.mjs +65 -0
- package/src/cli/hash-password.mjs +22 -0
- package/src/cli/help.mjs +153 -0
- package/src/cli/import-rules.mjs +102 -0
- package/src/cli/ingest/index.mjs +318 -0
- package/src/cli/ingest/parsers/chrome-bookmarks.mjs +238 -0
- package/src/cli/ingest/parsers/google-keep.mjs +147 -0
- package/src/cli/ingest/parsers/search-history.mjs +142 -0
- package/src/cli/ingest/parsers/youtube-history.mjs +181 -0
- package/src/cli/ingest/utils/dedup.mjs +69 -0
- package/src/cli/ingest/utils/takeout-walker.mjs +101 -0
- package/src/cli/ingest.mjs +102 -0
- package/src/cli/init.mjs +736 -0
- package/src/cli/integration-dispatcher.mjs +128 -0
- package/src/cli/lint.mjs +166 -0
- package/src/cli/map.mjs +194 -0
- package/src/cli/migrate.mjs +87 -0
- package/src/cli/rebuild.mjs +88 -0
- package/src/cli/recall.mjs +219 -0
- package/src/cli/relay.mjs +518 -0
- package/src/cli/remember.mjs +306 -0
- package/src/cli/research.mjs +381 -0
- package/src/cli/reset-password.mjs +75 -0
- package/src/cli/restore.mjs +135 -0
- package/src/cli/setup.mjs +616 -0
- package/src/cli/share.mjs +180 -0
- package/src/cli/skill.mjs +313 -0
- package/src/cli/snapshot.mjs +108 -0
- package/src/cli/start.mjs +63 -0
- package/src/cli/status.mjs +296 -0
- package/src/cli/sync.mjs +383 -0
- package/src/cli/uninstall.mjs +613 -0
- package/src/cli/upgrade.mjs +141 -0
- package/src/cli/wizard.html +3682 -0
- package/src/core/blackboard.mjs +54 -0
- package/src/core/clarity-rewriter.mjs +770 -0
- package/src/core/conclusion-writer.mjs +259 -0
- package/src/core/config.mjs +277 -0
- package/src/core/conflict-detector.mjs +536 -0
- package/src/core/crypto.mjs +107 -0
- package/src/core/daemon-control.mjs +158 -0
- package/src/core/daemon-loop.mjs +364 -0
- package/src/core/dream.mjs +347 -0
- package/src/core/drift-detector.mjs +143 -0
- package/src/core/embeddings.mjs +632 -0
- package/src/core/emergency-alerts.mjs +137 -0
- package/src/core/evolution.mjs +107 -0
- package/src/core/fact-seeker.mjs +1602 -0
- package/src/core/friction.mjs +95 -0
- package/src/core/import-rules.mjs +308 -0
- package/src/core/inference-engine.mjs +367 -0
- package/src/core/logger.mjs +75 -0
- package/src/core/memory-layers.mjs +96 -0
- package/src/core/migrate.mjs +116 -0
- package/src/core/notifications.mjs +77 -0
- package/src/core/operation-validator.mjs +272 -0
- package/src/core/optimizer.mjs +330 -0
- package/src/core/pattern_detector.mjs +113 -0
- package/src/core/post-mortem.mjs +413 -0
- package/src/core/protect-instructions.mjs +148 -0
- package/src/core/quick-capture.mjs +145 -0
- package/src/core/research-queue.mjs +431 -0
- package/src/core/research.mjs +461 -0
- package/src/core/runtime.mjs +515 -0
- package/src/core/sandbox.mjs +163 -0
- package/src/core/scheduler.mjs +569 -0
- package/src/core/schema.mjs +333 -0
- package/src/core/search.mjs +219 -0
- package/src/core/semantic-index.mjs +276 -0
- package/src/core/session-watcher.mjs +850 -0
- package/src/core/snapshot.mjs +133 -0
- package/src/core/source-adapters.mjs +1038 -0
- package/src/core/steering.mjs +137 -0
- package/src/core/surface.mjs +444 -0
- package/src/core/task_runner.mjs +125 -0
- package/src/core/tts.mjs +142 -0
- package/src/core/usage-tracker.mjs +349 -0
- package/src/core/vault-cache.mjs +76 -0
- package/src/core/vault.mjs +273 -0
- package/src/core/watchdog.mjs +236 -0
- package/src/server/api.mjs +944 -0
- package/src/server/auth.mjs +410 -0
- package/src/server/index.mjs +868 -0
- package/src/server/keys.mjs +181 -0
- package/src/server/rest.mjs +1652 -0
- package/src/server/route-manifest.json +77 -0
- package/src/server/routes/_shared.mjs +93 -0
- package/src/server/routes/keys.mjs +55 -0
- package/src/server/routes/memory.mjs +249 -0
- package/src/server/routes/sessions.mjs +336 -0
- package/src/server/routes/share.mjs +138 -0
- package/src/server/tools.mjs +618 -0
- package/templates/Caddyfile +37 -0
- package/templates/com.totalrecall.backup.plist +42 -0
- package/templates/com.totalrecall.daemon.plist +40 -0
- package/templates/com.totalrecall.duckdns.plist +23 -0
- package/templates/com.totalrecall.server.plist +46 -0
- package/templates/default-config/budget.yml +6 -0
- package/templates/default-config/runtime.yml +11 -0
- package/templates/default-config/security.yml +53 -0
- package/templates/default-config/voice.yml +34 -0
- package/templates/obsidian-queries/conflicts.md +26 -0
- package/templates/obsidian-queries/daily.md +17 -0
- package/templates/obsidian-queries/index.md +38 -0
- package/templates/obsidian-queries/skills.md +14 -0
- package/templates/onboarding-interview.md +72 -0
- package/templates/skill_engineering.md +40 -0
- package/templates/total-recall-daemon.service +21 -0
- 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
|
+
}
|