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,518 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* total-recall relay
|
|
3
|
+
*
|
|
4
|
+
* Local background daemon. Watches all IDE session directories on the user's
|
|
5
|
+
* machine (Claude Code, Codex, Antigravity, VS Code Copilot, Cursor) and ships
|
|
6
|
+
* new conversation sessions to the remote Total Recall brain via the REST API.
|
|
7
|
+
* The brain's dream cycle then distills sessions into SSSS memory nodes.
|
|
8
|
+
*
|
|
9
|
+
* This runs on the USER'S LOCAL MACHINE, not on the server. It bridges the gap
|
|
10
|
+
* between local IDE sessions and a remote brain.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* npx total-recall relay start Start the relay daemon
|
|
14
|
+
* npx total-recall relay stop Stop the relay daemon
|
|
15
|
+
* npx total-recall relay status Show relay status + last sync times
|
|
16
|
+
* npx total-recall relay once Single scan (no watch loop) — good for testing
|
|
17
|
+
* npx total-recall relay --help Show this help
|
|
18
|
+
*
|
|
19
|
+
* Config (read from ~/.agent/skills/total-recall/config/brain.json):
|
|
20
|
+
* { "url": "https://yourdomain.duckdns.org", "token": "tr_..." }
|
|
21
|
+
*
|
|
22
|
+
* Install as system service:
|
|
23
|
+
* npx total-recall relay install Install launchd (macOS) or systemd (Linux) service
|
|
24
|
+
* npx total-recall relay uninstall Remove the service
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import fs from 'node:fs';
|
|
28
|
+
import os from 'node:os';
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
import crypto from 'node:crypto';
|
|
31
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
32
|
+
import { fileURLToPath } from 'node:url';
|
|
33
|
+
import { resolveAgentDir } from './agent-dir.mjs';
|
|
34
|
+
import { agentDir as configAgentDir, brainDir as configBrainDir } from '../core/config.mjs';
|
|
35
|
+
|
|
36
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
37
|
+
const ROOT = path.join(__dirname, '..', '..');
|
|
38
|
+
const HOME = os.homedir();
|
|
39
|
+
|
|
40
|
+
// ─── Config ───────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
function loadBrainConfig(agentDir) {
|
|
43
|
+
const p = path.join(agentDir, 'config', 'brain.json');
|
|
44
|
+
if (!fs.existsSync(p)) return null;
|
|
45
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function brainUrl(config) {
|
|
49
|
+
return (config?.url || 'http://localhost:3000').replace(/\/$/, '');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function brainHeaders(config) {
|
|
53
|
+
const h = { 'Content-Type': 'application/json' };
|
|
54
|
+
if (config?.token) h['Authorization'] = `Bearer ${config.token}`;
|
|
55
|
+
return h;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ─── State: last-seen file mtimes ─────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
function stateFile(agentDir) {
|
|
61
|
+
return path.join(agentDir, 'logs', 'relay-state.json');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function loadState(agentDir) {
|
|
65
|
+
const p = stateFile(agentDir);
|
|
66
|
+
if (!fs.existsSync(p)) return {};
|
|
67
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return {}; }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function saveState(agentDir, state) {
|
|
71
|
+
const p = stateFile(agentDir);
|
|
72
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
73
|
+
fs.writeFileSync(p, JSON.stringify(state, null, 2), 'utf8');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── IDE source definitions (mirrors session-watcher SOURCES) ─────────────────
|
|
77
|
+
|
|
78
|
+
function getSources() {
|
|
79
|
+
return [
|
|
80
|
+
{
|
|
81
|
+
name: 'claude-code',
|
|
82
|
+
root: path.join(HOME, '.claude', 'projects'),
|
|
83
|
+
filter: (f) => f.endsWith('.jsonl'),
|
|
84
|
+
dirFilter: null,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'codex',
|
|
88
|
+
root: path.join(HOME, '.codex', 'sessions'),
|
|
89
|
+
filter: (f) => f.endsWith('.jsonl'),
|
|
90
|
+
dirFilter: null,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'antigravity',
|
|
94
|
+
root: path.join(HOME, '.gemini', 'antigravity', 'brain'),
|
|
95
|
+
filter: (f) => f === 'overview.txt' || f === 'transcript.jsonl',
|
|
96
|
+
dirFilter: null,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'vscode',
|
|
100
|
+
root: path.join(HOME, 'Library', 'Application Support', 'Code', 'User', 'workspaceStorage'),
|
|
101
|
+
filter: (f) => f.endsWith('.jsonl'),
|
|
102
|
+
dirFilter: (d) => d === 'chatSessions',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'cursor',
|
|
106
|
+
root: path.join(HOME, '.cursor', 'projects'),
|
|
107
|
+
filter: (f) => f.endsWith('.jsonl'),
|
|
108
|
+
dirFilter: null,
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ─── File walker ──────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
function findFiles(root, filter, dirFilter = null, maxDepth = 5) {
|
|
116
|
+
const results = [];
|
|
117
|
+
if (!fs.existsSync(root)) return results;
|
|
118
|
+
|
|
119
|
+
function walk(dir, depth) {
|
|
120
|
+
if (depth > maxDepth) return;
|
|
121
|
+
let entries;
|
|
122
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
123
|
+
for (const entry of entries) {
|
|
124
|
+
const full = path.join(dir, entry.name);
|
|
125
|
+
if (entry.isDirectory()) {
|
|
126
|
+
if (!dirFilter || depth === 0 || dirFilter(entry.name)) walk(full, depth + 1);
|
|
127
|
+
} else if (entry.isFile() && filter(entry.name)) {
|
|
128
|
+
results.push(full);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
walk(root, 0);
|
|
134
|
+
return results;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ─── Ship a file to the brain ─────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
async function shipFile(filePath, sourceName, brainBaseUrl, headers) {
|
|
140
|
+
let content;
|
|
141
|
+
try { content = fs.readFileSync(filePath, 'utf8'); } catch { return false; }
|
|
142
|
+
if (!content.trim()) return false;
|
|
143
|
+
|
|
144
|
+
// Detect project name from the file path by looking for a parent git repo
|
|
145
|
+
let project = null;
|
|
146
|
+
try {
|
|
147
|
+
let dir = path.dirname(filePath);
|
|
148
|
+
for (let i = 0; i < 8 && dir !== path.dirname(dir); i++) {
|
|
149
|
+
if (fs.existsSync(path.join(dir, '.git'))) {
|
|
150
|
+
project = path.basename(dir);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
dir = path.dirname(dir);
|
|
154
|
+
}
|
|
155
|
+
} catch { /* non-fatal */ }
|
|
156
|
+
|
|
157
|
+
const payload = {
|
|
158
|
+
id: crypto.createHash('sha256').update(filePath).digest('hex').slice(0, 12),
|
|
159
|
+
source: sourceName,
|
|
160
|
+
path: filePath,
|
|
161
|
+
project,
|
|
162
|
+
content: content.slice(0, 40_000_000), // cap at 40MB to stay well under the server's 50MB JSON body limit
|
|
163
|
+
sha256: crypto.createHash('sha256').update(content).digest('hex'),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const res = await fetch(`${brainBaseUrl}/api/sessions/ingest`, {
|
|
168
|
+
method: 'POST',
|
|
169
|
+
headers,
|
|
170
|
+
body: JSON.stringify(payload),
|
|
171
|
+
signal: AbortSignal.timeout(30_000),
|
|
172
|
+
});
|
|
173
|
+
return res.ok;
|
|
174
|
+
} catch {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ─── Single scan pass ─────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
async function runScan(agentDir, { verbose = false } = {}) {
|
|
182
|
+
const config = loadBrainConfig(agentDir);
|
|
183
|
+
if (!config) {
|
|
184
|
+
if (verbose) console.error('[relay] No brain.json config found. Run: npx total-recall connect claude-code --brain <url> --token <pat>');
|
|
185
|
+
return { shipped: 0, skipped: 0, errors: 0 };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const base = brainUrl(config);
|
|
189
|
+
const headers = brainHeaders(config);
|
|
190
|
+
const state = loadState(agentDir);
|
|
191
|
+
const sources = getSources();
|
|
192
|
+
|
|
193
|
+
let shipped = 0, skipped = 0, errors = 0;
|
|
194
|
+
|
|
195
|
+
for (const source of sources) {
|
|
196
|
+
const files = findFiles(source.root, source.filter, source.dirFilter);
|
|
197
|
+
for (const filePath of files) {
|
|
198
|
+
let mtime;
|
|
199
|
+
try { mtime = fs.statSync(filePath).mtimeMs; } catch { continue; }
|
|
200
|
+
|
|
201
|
+
const key = filePath;
|
|
202
|
+
if (state[key] && state[key] >= mtime) {
|
|
203
|
+
skipped++;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const ok = await shipFile(filePath, source.name, base, headers);
|
|
208
|
+
if (ok) {
|
|
209
|
+
state[key] = mtime;
|
|
210
|
+
shipped++;
|
|
211
|
+
if (verbose) console.error(`[relay] shipped ${source.name}: ${path.basename(filePath)}`);
|
|
212
|
+
} else {
|
|
213
|
+
errors++;
|
|
214
|
+
if (verbose) console.error(`[relay] failed ${source.name}: ${path.basename(filePath)}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
saveState(agentDir, state);
|
|
220
|
+
return { shipped, skipped, errors };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ─── Watch loop ───────────────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
async function runLoop(agentDir, intervalMs = 60_000) {
|
|
226
|
+
console.error(`[relay] started — watching all IDEs, shipping to brain every ${intervalMs / 1000}s`);
|
|
227
|
+
const logFile = path.join(agentDir, 'logs', 'relay.log');
|
|
228
|
+
fs.mkdirSync(path.dirname(logFile), { recursive: true });
|
|
229
|
+
|
|
230
|
+
const log = (msg) => {
|
|
231
|
+
const line = `${new Date().toISOString()} ${msg}\n`;
|
|
232
|
+
process.stderr.write(line);
|
|
233
|
+
try { fs.appendFileSync(logFile, line); } catch {}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// eslint-disable-next-line no-constant-condition
|
|
237
|
+
while (true) {
|
|
238
|
+
try {
|
|
239
|
+
const { shipped, skipped, errors } = await runScan(agentDir, { verbose: false });
|
|
240
|
+
if (shipped > 0 || errors > 0) {
|
|
241
|
+
log(`scan: shipped=${shipped} skipped=${skipped} errors=${errors}`);
|
|
242
|
+
}
|
|
243
|
+
} catch (e) {
|
|
244
|
+
log(`scan error: ${e.message}`);
|
|
245
|
+
}
|
|
246
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ─── Process management ───────────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
function pidFile(agentDir) {
|
|
253
|
+
return path.join(agentDir, 'logs', 'relay.pid');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function readPid(agentDir) {
|
|
257
|
+
const p = pidFile(agentDir);
|
|
258
|
+
if (!fs.existsSync(p)) return null;
|
|
259
|
+
const raw = fs.readFileSync(p, 'utf8').trim();
|
|
260
|
+
const pid = parseInt(raw, 10);
|
|
261
|
+
return isNaN(pid) ? null : pid;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function isRunning(pid) {
|
|
265
|
+
if (!pid) return false;
|
|
266
|
+
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ─── launchd / systemd service installation ───────────────────────────────────
|
|
270
|
+
|
|
271
|
+
function installService(agentDir) {
|
|
272
|
+
const platform = os.platform();
|
|
273
|
+
const nodeBin = process.execPath;
|
|
274
|
+
const scriptPath = path.join(ROOT, 'bin', 'total-recall.mjs');
|
|
275
|
+
|
|
276
|
+
if (platform === 'darwin') {
|
|
277
|
+
const plistName = 'com.totalrecall.relay';
|
|
278
|
+
const plistDest = path.join(HOME, 'Library', 'LaunchAgents', `${plistName}.plist`);
|
|
279
|
+
const logOut = path.join(agentDir, 'logs', 'relay.log');
|
|
280
|
+
const logErr = path.join(agentDir, 'logs', 'relay-err.log');
|
|
281
|
+
|
|
282
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
283
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
284
|
+
<plist version="1.0">
|
|
285
|
+
<dict>
|
|
286
|
+
<key>Label</key> <string>${plistName}</string>
|
|
287
|
+
<key>ProgramArguments</key>
|
|
288
|
+
<array>
|
|
289
|
+
<string>${nodeBin}</string>
|
|
290
|
+
<string>${scriptPath}</string>
|
|
291
|
+
<string>relay</string>
|
|
292
|
+
<string>_loop</string>
|
|
293
|
+
</array>
|
|
294
|
+
<key>RunAtLoad</key> <true/>
|
|
295
|
+
<key>KeepAlive</key> <true/>
|
|
296
|
+
<key>StandardOutPath</key> <string>${logOut}</string>
|
|
297
|
+
<key>StandardErrorPath</key> <string>${logErr}</string>
|
|
298
|
+
<key>EnvironmentVariables</key>
|
|
299
|
+
<dict>
|
|
300
|
+
<key>AGENT_DIR</key><string>${agentDir}</string>
|
|
301
|
+
</dict>
|
|
302
|
+
</dict>
|
|
303
|
+
</plist>`;
|
|
304
|
+
|
|
305
|
+
fs.mkdirSync(path.dirname(plistDest), { recursive: true });
|
|
306
|
+
fs.writeFileSync(plistDest, plist, 'utf8');
|
|
307
|
+
spawnSync('launchctl', ['unload', plistDest], { stdio: 'ignore' });
|
|
308
|
+
const r = spawnSync('launchctl', ['load', plistDest], { stdio: 'inherit' });
|
|
309
|
+
if (r.status === 0) {
|
|
310
|
+
console.log(` ✅ Relay service installed and started (launchd)`);
|
|
311
|
+
console.log(` Plist: ${plistDest}`);
|
|
312
|
+
console.log(` Logs: ${logOut}`);
|
|
313
|
+
} else {
|
|
314
|
+
console.error(' ⚠️ launchctl load failed — try: launchctl load ' + plistDest);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
} else if (platform === 'linux') {
|
|
318
|
+
const unitName = 'total-recall-relay';
|
|
319
|
+
const unitDest = path.join(HOME, '.config', 'systemd', 'user', `${unitName}.service`);
|
|
320
|
+
const logOut = path.join(agentDir, 'logs', 'relay.log');
|
|
321
|
+
|
|
322
|
+
const unit = `[Unit]
|
|
323
|
+
Description=Total Recall IDE Session Relay
|
|
324
|
+
After=network.target
|
|
325
|
+
|
|
326
|
+
[Service]
|
|
327
|
+
Type=simple
|
|
328
|
+
ExecStart=${nodeBin} ${scriptPath} relay _loop
|
|
329
|
+
Restart=always
|
|
330
|
+
RestartSec=10
|
|
331
|
+
StandardOutput=append:${logOut}
|
|
332
|
+
StandardError=append:${logOut}
|
|
333
|
+
Environment=AGENT_DIR=${agentDir}
|
|
334
|
+
|
|
335
|
+
[Install]
|
|
336
|
+
WantedBy=default.target
|
|
337
|
+
`;
|
|
338
|
+
|
|
339
|
+
fs.mkdirSync(path.dirname(unitDest), { recursive: true });
|
|
340
|
+
fs.writeFileSync(unitDest, unit, 'utf8');
|
|
341
|
+
spawnSync('systemctl', ['--user', 'daemon-reload'], { stdio: 'ignore' });
|
|
342
|
+
const r = spawnSync('systemctl', ['--user', 'enable', '--now', unitName], { stdio: 'inherit' });
|
|
343
|
+
if (r.status === 0) {
|
|
344
|
+
console.log(` ✅ Relay service installed and started (systemd --user)`);
|
|
345
|
+
console.log(` Unit: ${unitDest}`);
|
|
346
|
+
console.log(` Logs: journalctl --user -u ${unitName} -f`);
|
|
347
|
+
} else {
|
|
348
|
+
console.error(' ⚠️ systemctl failed — try: systemctl --user enable --now ' + unitName);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
} else {
|
|
352
|
+
console.error(' ⚠️ Auto-install not supported on this platform. Run manually:');
|
|
353
|
+
console.error(` ${nodeBin} ${scriptPath} relay _loop`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function uninstallService() {
|
|
358
|
+
const platform = os.platform();
|
|
359
|
+
if (platform === 'darwin') {
|
|
360
|
+
const plistDest = path.join(HOME, 'Library', 'LaunchAgents', 'com.totalrecall.relay.plist');
|
|
361
|
+
if (fs.existsSync(plistDest)) {
|
|
362
|
+
spawnSync('launchctl', ['unload', plistDest], { stdio: 'ignore' });
|
|
363
|
+
fs.unlinkSync(plistDest);
|
|
364
|
+
console.log(' ✅ Relay service removed');
|
|
365
|
+
} else {
|
|
366
|
+
console.log(' No relay service installed');
|
|
367
|
+
}
|
|
368
|
+
} else if (platform === 'linux') {
|
|
369
|
+
const unitName = 'total-recall-relay';
|
|
370
|
+
spawnSync('systemctl', ['--user', 'disable', '--now', unitName], { stdio: 'ignore' });
|
|
371
|
+
const unitDest = path.join(HOME, '.config', 'systemd', 'user', `${unitName}.service`);
|
|
372
|
+
if (fs.existsSync(unitDest)) fs.unlinkSync(unitDest);
|
|
373
|
+
spawnSync('systemctl', ['--user', 'daemon-reload'], { stdio: 'ignore' });
|
|
374
|
+
console.log(' ✅ Relay service removed');
|
|
375
|
+
} else {
|
|
376
|
+
console.log(' Nothing to uninstall on this platform');
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ─── CLI ──────────────────────────────────────────────────────────────────────
|
|
381
|
+
|
|
382
|
+
function parseArgs(args) {
|
|
383
|
+
const opts = { command: null, help: false };
|
|
384
|
+
for (const arg of args) {
|
|
385
|
+
if (arg === '--help' || arg === '-h') { opts.help = true; break; }
|
|
386
|
+
if (!opts.command) opts.command = arg;
|
|
387
|
+
}
|
|
388
|
+
return opts;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function printHelp() {
|
|
392
|
+
console.log(`
|
|
393
|
+
total-recall relay — Local IDE session relay to remote brain
|
|
394
|
+
|
|
395
|
+
Usage: npx total-recall relay <command>
|
|
396
|
+
|
|
397
|
+
Commands:
|
|
398
|
+
start Start relay in background (detached process)
|
|
399
|
+
stop Stop background relay
|
|
400
|
+
status Show relay status + sources found on this machine
|
|
401
|
+
once Single scan pass (no loop) — for testing
|
|
402
|
+
install Install as launchd (macOS) or systemd (Linux) service
|
|
403
|
+
uninstall Remove the service
|
|
404
|
+
|
|
405
|
+
The relay watches:
|
|
406
|
+
Claude Code ~/.claude/projects/
|
|
407
|
+
Codex ~/.codex/sessions/
|
|
408
|
+
Antigravity ~/.gemini/antigravity/brain/
|
|
409
|
+
VS Code ~/Library/Application Support/Code/User/workspaceStorage/*/chatSessions/
|
|
410
|
+
Cursor ~/.cursor/projects/
|
|
411
|
+
|
|
412
|
+
New/updated session files are shipped to the brain at:
|
|
413
|
+
POST <brain_url>/api/sessions/ingest
|
|
414
|
+
|
|
415
|
+
Config: ~/.agent/skills/total-recall/config/brain.json (written by: npx total-recall connect)
|
|
416
|
+
`);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export default async function relay(args) {
|
|
420
|
+
const opts = parseArgs(args);
|
|
421
|
+
if (opts.help || !opts.command) { printHelp(); return; }
|
|
422
|
+
|
|
423
|
+
const brDir = configBrainDir;
|
|
424
|
+
fs.mkdirSync(path.join(brDir, 'logs'), { recursive: true });
|
|
425
|
+
|
|
426
|
+
switch (opts.command) {
|
|
427
|
+
|
|
428
|
+
case '_loop': {
|
|
429
|
+
// Internal: run the watch loop (called by launchd/systemd or `start`)
|
|
430
|
+
const pid = process.pid;
|
|
431
|
+
fs.writeFileSync(pidFile(brDir), String(pid), 'utf8');
|
|
432
|
+
process.on('SIGTERM', () => { try { fs.unlinkSync(pidFile(brDir)); } catch {} process.exit(0); });
|
|
433
|
+
process.on('SIGINT', () => { try { fs.unlinkSync(pidFile(brDir)); } catch {} process.exit(0); });
|
|
434
|
+
await runLoop(brDir);
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
case 'start': {
|
|
439
|
+
const pid = readPid(brDir);
|
|
440
|
+
if (isRunning(pid)) {
|
|
441
|
+
console.log(` Relay already running (PID ${pid})`);
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
const child = spawn(process.execPath, [
|
|
445
|
+
path.join(ROOT, 'bin', 'total-recall.mjs'), 'relay', '_loop',
|
|
446
|
+
], {
|
|
447
|
+
detached: true,
|
|
448
|
+
stdio: ['ignore',
|
|
449
|
+
fs.openSync(path.join(brDir, 'logs', 'relay.log'), 'a'),
|
|
450
|
+
fs.openSync(path.join(brDir, 'logs', 'relay-err.log'), 'a'),
|
|
451
|
+
],
|
|
452
|
+
env: { ...process.env, AGENT_DIR: configAgentDir },
|
|
453
|
+
});
|
|
454
|
+
child.unref();
|
|
455
|
+
fs.writeFileSync(pidFile(brDir), String(child.pid), 'utf8');
|
|
456
|
+
console.log(` ✅ Relay started (PID ${child.pid})`);
|
|
457
|
+
console.log(` Log: ${path.join(brDir, 'logs', 'relay.log')}`);
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
case 'stop': {
|
|
462
|
+
const pid = readPid(brDir);
|
|
463
|
+
if (!isRunning(pid)) {
|
|
464
|
+
console.log(' Relay is not running');
|
|
465
|
+
try { fs.unlinkSync(pidFile(brDir)); } catch {}
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
process.kill(pid, 'SIGTERM');
|
|
469
|
+
try { fs.unlinkSync(pidFile(brDir)); } catch {}
|
|
470
|
+
console.log(` ✅ Relay stopped (PID ${pid})`);
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
case 'status': {
|
|
475
|
+
const pid = readPid(brDir);
|
|
476
|
+
const running = isRunning(pid);
|
|
477
|
+
console.log(`\n Relay: ${running ? `running (PID ${pid})` : 'stopped'}`);
|
|
478
|
+
|
|
479
|
+
const config = loadBrainConfig(brDir);
|
|
480
|
+
if (config) {
|
|
481
|
+
console.log(` Brain: ${brainUrl(config)}`);
|
|
482
|
+
console.log(` Auth: ${config.token ? 'PAT configured' : 'no token'}`);
|
|
483
|
+
} else {
|
|
484
|
+
console.log(' Brain: not configured (run: npx total-recall connect --brain <url> --token <pat>)');
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
console.log('\n Sources on this machine:');
|
|
488
|
+
for (const src of getSources()) {
|
|
489
|
+
const exists = fs.existsSync(src.root);
|
|
490
|
+
const files = exists ? findFiles(src.root, src.filter, src.dirFilter).length : 0;
|
|
491
|
+
console.log(` ${exists ? '✅' : '○ '} ${src.name.padEnd(14)} ${exists ? `${files} session files` : 'not found'}`);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const state = loadState(brDir);
|
|
495
|
+
const tracked = Object.keys(state).length;
|
|
496
|
+
if (tracked > 0) {
|
|
497
|
+
const latest = Object.values(state).sort((a, b) => b - a)[0];
|
|
498
|
+
console.log(`\n Tracked: ${tracked} files, last sync ${new Date(latest).toLocaleString()}`);
|
|
499
|
+
}
|
|
500
|
+
console.log();
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
case 'once': {
|
|
505
|
+
console.log(' Running single scan...');
|
|
506
|
+
const { shipped, skipped, errors } = await runScan(brDir, { verbose: true });
|
|
507
|
+
console.log(`\n Done: shipped=${shipped} skipped=${skipped} errors=${errors}`);
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
case 'install': installService(brDir); break;
|
|
512
|
+
case 'uninstall': uninstallService(); break;
|
|
513
|
+
|
|
514
|
+
default:
|
|
515
|
+
console.error(` Unknown command: ${opts.command}`);
|
|
516
|
+
printHelp();
|
|
517
|
+
}
|
|
518
|
+
}
|