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,296 @@
1
+ /**
2
+ * total-recall status
3
+ *
4
+ * Show brain connection status, last sync time, local vs. remote vault hash
5
+ * comparison, and stale rule count.
6
+ *
7
+ * Usage:
8
+ * npx total-recall status [options]
9
+ *
10
+ * Options:
11
+ * --json Emit machine-readable JSON
12
+ * --brain <url> Override the configured brain URL
13
+ * --token <token> Bearer PAT for the brain (overrides config / env)
14
+ * --help, -h Show this help
15
+ */
16
+
17
+ import os from 'node:os';
18
+ import path from 'node:path';
19
+ import fs from 'node:fs';
20
+ import crypto from 'node:crypto';
21
+ import { loadQueue } from '../core/research-queue.mjs';
22
+
23
+ import { agentDir, brainDir, totalRecallToken } from '../core/config.mjs';
24
+
25
+ const AGENT_DIR = agentDir;
26
+ const BRAIN_CONFIG = path.join(brainDir, 'config', 'brain.json');
27
+ const SYNC_STATE = path.join(brainDir, 'config', 'sync-state.json');
28
+ const CLIENTS_REGISTRY = path.join(brainDir, 'config', 'clients.json');
29
+ const INSTRUCTIONS_FILE = path.join(AGENT_DIR, 'INSTRUCTIONS.md');
30
+
31
+ function parseArgs(args) {
32
+ const opts = { json: false, brain: null, token: null, help: false };
33
+ for (let i = 0; i < args.length; i++) {
34
+ switch (args[i]) {
35
+ case '--json': opts.json = true; break;
36
+ case '--brain': opts.brain = args[++i]; break;
37
+ case '--token': opts.token = args[++i]; break;
38
+ case '--help': case '-h': opts.help = true; break;
39
+ }
40
+ }
41
+ return opts;
42
+ }
43
+
44
+ function printHelp() {
45
+ console.log(`
46
+ total-recall status — Show brain connection and sync state
47
+
48
+ Usage: total-recall status [options]
49
+
50
+ Options:
51
+ --json Emit machine-readable JSON
52
+ --brain <url> Override the configured brain URL
53
+ --token <token> Bearer PAT for the brain (overrides config / env)
54
+ --help, -h Show this help
55
+ `);
56
+ }
57
+
58
+ function loadBrainConfig() {
59
+ if (!fs.existsSync(BRAIN_CONFIG)) return null;
60
+ try { return JSON.parse(fs.readFileSync(BRAIN_CONFIG, 'utf8')); }
61
+ catch { return null; }
62
+ }
63
+
64
+ function loadSyncState() {
65
+ if (!fs.existsSync(SYNC_STATE)) return {};
66
+ try { return JSON.parse(fs.readFileSync(SYNC_STATE, 'utf8')); }
67
+ catch { return {}; }
68
+ }
69
+
70
+ function loadClientsRegistry() {
71
+ if (!fs.existsSync(CLIENTS_REGISTRY)) return {};
72
+ try { return JSON.parse(fs.readFileSync(CLIENTS_REGISTRY, 'utf8'))?.clients || {}; }
73
+ catch { return {}; }
74
+ }
75
+
76
+ function projectionsStatus(clients, instructionsMtime) {
77
+ return Object.entries(clients).map(([name, info]) => {
78
+ let fresh = null;
79
+ if (info.projectionPath && instructionsMtime) {
80
+ try {
81
+ const projMtime = fs.statSync(info.projectionPath).mtimeMs;
82
+ fresh = projMtime >= instructionsMtime;
83
+ } catch { fresh = false; }
84
+ }
85
+ return { name, label: info.label, mode: info.mode, connectedAt: info.connectedAt, projectionPath: info.projectionPath, fresh };
86
+ });
87
+ }
88
+
89
+ function sha256(buf) {
90
+ return crypto.createHash('sha256').update(buf).digest('hex');
91
+ }
92
+
93
+ async function probeBrain(brainUrl, token) {
94
+ const headers = {};
95
+ if (token) headers.Authorization = `Bearer ${token}`;
96
+ const out = { reachable: false, instructions_sha256: null, instructions_bytes: null, error: null };
97
+ try {
98
+ const res = await fetch(`${brainUrl.replace(/\/$/, '')}/api/instructions`, { headers });
99
+ if (!res.ok) {
100
+ out.error = `HTTP ${res.status}`;
101
+ return out;
102
+ }
103
+ const body = await res.json();
104
+ out.reachable = true;
105
+ out.instructions_sha256 = body.sha256 || null;
106
+ out.instructions_bytes = body.bytes || null;
107
+ out.modified = body.modified || null;
108
+ } catch (err) {
109
+ out.error = err.message;
110
+ }
111
+ return out;
112
+ }
113
+
114
+ export default async function statusCmd(args) {
115
+ const opts = parseArgs(args);
116
+ if (opts.help) { printHelp(); return; }
117
+
118
+ const brainConfig = loadBrainConfig();
119
+ const syncState = loadSyncState();
120
+ const clientsRegistry = loadClientsRegistry();
121
+ const brainUrl = opts.brain || brainConfig?.url || null;
122
+ const token = opts.token || brainConfig?.token || totalRecallToken || null;
123
+
124
+ const wizardConfigPath = path.join(brainDir, 'config', 'wizard-config.json');
125
+ let deployMode = 'unknown';
126
+ let dashboardUrl = null;
127
+ let tunnelPid = null;
128
+ let tunnelAlive = false;
129
+
130
+ if (fs.existsSync(wizardConfigPath)) {
131
+ try {
132
+ const wizardCfg = JSON.parse(fs.readFileSync(wizardConfigPath, 'utf8'));
133
+ deployMode = wizardCfg['deploy-mode'] || 'local';
134
+ dashboardUrl = wizardCfg['cfg-dash-url'] || null;
135
+ } catch {}
136
+ }
137
+
138
+ const logsDir = path.join(brainDir, 'logs');
139
+ const cfPidPath = path.join(logsDir, 'cloudflared.pid');
140
+ if (fs.existsSync(cfPidPath)) {
141
+ try {
142
+ const pid = parseInt(fs.readFileSync(cfPidPath, 'utf8').trim(), 10);
143
+ if (pid) {
144
+ tunnelPid = pid;
145
+ process.kill(pid, 0);
146
+ tunnelAlive = true;
147
+ }
148
+ } catch {
149
+ tunnelAlive = false;
150
+ }
151
+ }
152
+
153
+ // Live server /health check
154
+ let serverHealth = 'offline';
155
+ let serverUptime = null;
156
+ try {
157
+ let healthPort = 3000;
158
+ try {
159
+ const { default: cfg } = await import('../core/config.mjs');
160
+ if (cfg.port) healthPort = cfg.port;
161
+ } catch {}
162
+ const res = await fetch(`http://localhost:${healthPort}/health`, { signal: AbortSignal.timeout(2000) });
163
+ if (res.ok) {
164
+ const hData = await res.json();
165
+ serverHealth = hData.status || 'healthy';
166
+ serverUptime = hData.uptime_seconds || null;
167
+ }
168
+ } catch {}
169
+
170
+ // Daemon status
171
+ let daemonStatus = 'unknown';
172
+ try {
173
+ const { getDaemonStatus } = await import('../core/daemon-control.mjs');
174
+ daemonStatus = getDaemonStatus();
175
+ } catch {}
176
+
177
+ let localInstructions = null;
178
+ if (fs.existsSync(INSTRUCTIONS_FILE)) {
179
+ const buf = fs.readFileSync(INSTRUCTIONS_FILE);
180
+ const stat = fs.statSync(INSTRUCTIONS_FILE);
181
+ localInstructions = {
182
+ path: INSTRUCTIONS_FILE,
183
+ sha256: sha256(buf),
184
+ bytes: stat.size,
185
+ modified: stat.mtime.toISOString()
186
+ };
187
+ }
188
+
189
+ let remote = null;
190
+ if (brainUrl) {
191
+ remote = await probeBrain(brainUrl, token);
192
+ }
193
+
194
+ const inSync = !!(localInstructions && remote?.instructions_sha256 &&
195
+ remote.instructions_sha256 === localInstructions.sha256);
196
+
197
+ const instructionsMtime = localInstructions
198
+ ? fs.statSync(INSTRUCTIONS_FILE).mtimeMs
199
+ : null;
200
+ const clientProjections = projectionsStatus(clientsRegistry, instructionsMtime);
201
+
202
+ const report = {
203
+ server: {
204
+ status: serverHealth,
205
+ uptime_seconds: serverUptime
206
+ },
207
+ deploy: {
208
+ mode: deployMode,
209
+ dashboard_url: dashboardUrl,
210
+ tunnel_pid: tunnelPid,
211
+ tunnel_alive: tunnelAlive
212
+ },
213
+ daemon: daemonStatus,
214
+ brain: {
215
+ url: brainUrl,
216
+ configured: !!brainConfig,
217
+ reachable: remote?.reachable || false,
218
+ error: remote?.error || null
219
+ },
220
+ instructions: {
221
+ local: localInstructions,
222
+ remote: remote ? {
223
+ sha256: remote.instructions_sha256,
224
+ bytes: remote.instructions_bytes,
225
+ modified: remote.modified
226
+ } : null,
227
+ in_sync: inSync
228
+ },
229
+ last_sync: syncState.last_sync || null,
230
+ targets: syncState.targets ? Object.keys(syncState.targets) : [],
231
+ clients: clientProjections,
232
+ research: []
233
+ };
234
+
235
+ // Load ongoing research agenda
236
+ let ongoingResearch = [];
237
+ try {
238
+ ongoingResearch = loadQueue().filter(item => item.status === 'in_progress' || item.status === 'pending');
239
+ report.research = ongoingResearch.map(item => ({
240
+ id: item.id,
241
+ topic: item.topic,
242
+ status: item.status,
243
+ research_phase: item.research_phase,
244
+ updated_at: item.updated_at
245
+ }));
246
+ } catch (err) {
247
+ // Ignore queue loading errors if not configured or file doesn't exist
248
+ }
249
+
250
+ if (opts.json) {
251
+ console.log(JSON.stringify(report, null, 2));
252
+ return;
253
+ }
254
+
255
+ console.log('\n Total Recall — Status\n');
256
+ console.log(` Brain Server: ${serverHealth === 'offline' ? '🔴 Offline' : `🟢 Online (${serverUptime ? `${Math.floor(serverUptime / 60)}m uptime` : 'healthy'})`}`);
257
+ console.log(` Deploy Mode: ${deployMode}`);
258
+ console.log(` Dashboard UI: ${dashboardUrl || '(not resolved)'}`);
259
+ console.log(` Remote Brain: ${report.brain.url || '(none)'}`);
260
+ console.log(` Tunnel Process: ${tunnelPid ? (tunnelAlive ? `🟢 Active (PID ${tunnelPid})` : '🔴 Stale PID file') : '⚪ Inactive'}`);
261
+ console.log(` Daemon Status: ${daemonStatus === 'running' ? '🟢 Running' : daemonStatus === 'dead' ? '🔴 Dead' : '⚪ Not Running'}`);
262
+ console.log(` Reachable: ${report.brain.reachable ? 'yes' : 'no'}${report.brain.error ? ` (${report.brain.error})` : ''}`);
263
+ console.log(` Local INSTR.: ${localInstructions ? `${localInstructions.bytes} bytes, sha=${localInstructions.sha256.slice(0, 12)}…` : '(not compiled)'}`);
264
+ if (remote?.instructions_sha256) {
265
+ console.log(` Remote INSTR.: ${remote.instructions_bytes} bytes, sha=${remote.instructions_sha256.slice(0, 12)}…`);
266
+ }
267
+ console.log(` In sync: ${inSync ? 'yes' : 'no'}`);
268
+ console.log(` Last sync: ${report.last_sync || 'never'}`);
269
+ console.log(` Sync targets: ${report.targets.length ? report.targets.join(', ') : '(none)'}`);
270
+
271
+ if (clientProjections.length) {
272
+ console.log('\n Connected clients:');
273
+ for (const c of clientProjections) {
274
+ let freshnessTag = '';
275
+ if (c.mode === 'file' || c.mode === 'symlink') {
276
+ freshnessTag = c.fresh === null ? ' (no projection)' : c.fresh ? ' ✅ fresh' : ' ⚠️ stale — run: total-recall connect ' + c.name;
277
+ }
278
+ console.log(` ${c.label.padEnd(18)} [${c.mode}]${freshnessTag}`);
279
+ }
280
+ } else {
281
+ console.log(' Connected clients: (none — run total-recall connect <client>)');
282
+ }
283
+
284
+ if (ongoingResearch.length) {
285
+ console.log('\n Ongoing Autonomous Research:');
286
+ for (const item of ongoingResearch) {
287
+ const badge = item.status === 'in_progress' ? '\x1b[36m🧪 [IN PROGRESS]\x1b[0m' : '\x1b[33m⏳ [PENDING]\x1b[0m';
288
+ const phase = item.research_phase || 'acquisition';
289
+ console.log(` ${badge} "${item.topic}"`);
290
+ console.log(` Phase: \x1b[36m${phase}\x1b[0m | ID: ${item.id.slice(0, 8)}…`);
291
+ }
292
+ } else {
293
+ console.log('\n Ongoing Autonomous Research: (none)');
294
+ }
295
+ console.log();
296
+ }
@@ -0,0 +1,383 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import matter from 'gray-matter';
6
+ import { protectIDEInstructions } from '../core/protect-instructions.mjs';
7
+ import { totalRecallToken } from '../core/config.mjs';
8
+
9
+ const INJECTION_BEGIN = '<!-- BEGIN INJECTED MEMORY: do not edit by hand; rebuilt by total-recall surface -->';
10
+ const INJECTION_END = '<!-- END INJECTED MEMORY -->';
11
+ const SHIMS = [
12
+ '.cursorrules',
13
+ 'CLAUDE.md',
14
+ '.clauderules',
15
+ 'AGENTS.md',
16
+ 'GEMINI.md',
17
+ '.github/copilot-instructions.md',
18
+ '.vscode/copilot-instructions.md'
19
+ ];
20
+
21
+ function parseArgs(args) {
22
+ const opts = {
23
+ brain: null,
24
+ token: null,
25
+ watch: false,
26
+ push: false,
27
+ intervalSeconds: 60,
28
+ help: false
29
+ };
30
+
31
+ for (let i = 0; i < args.length; i++) {
32
+ switch (args[i]) {
33
+ case '--brain': opts.brain = args[++i]; break;
34
+ case '--token': opts.token = args[++i]; break;
35
+ case '--watch': opts.watch = true; break;
36
+ case '--push': opts.push = true; break;
37
+ case '--interval': opts.intervalSeconds = Number(args[++i]) || 60; break;
38
+ case '--help':
39
+ case '-h':
40
+ opts.help = true;
41
+ break;
42
+ }
43
+ }
44
+
45
+ return opts;
46
+ }
47
+
48
+ function printHelp() {
49
+ console.log(`
50
+ total-recall sync — Sync brain instructions and back up vault to GitHub
51
+
52
+ Usage: total-recall sync [options]
53
+
54
+ Options:
55
+ --brain <url> Brain base URL. Defaults to .agent/config/brain.json
56
+ --token <token> Bearer PAT. Defaults to brain.json or TOTAL_RECALL_TOKEN
57
+ --watch Keep syncing on an interval
58
+ --interval <sec> Watch interval in seconds (default: 60)
59
+ --push Back up vault to your GitHub fork (git add -f + push)
60
+ --help, -h Show this help
61
+
62
+ Backup workflow (--push):
63
+ Commits .agent/skills/total-recall/memory-vault/, INSTRUCTIONS.md,
64
+ .agent/skills/total-recall/config/brain.json, and encrypted credentials (.agent/skills/total-recall/config/secrets.enc)
65
+ to your private fork via force-add (bypasses .gitignore) to ensure seamless portability across machines.
66
+ Run 'npx total-recall setup' to configure your GitHub fork first.
67
+ `);
68
+ }
69
+
70
+ function workspaceAgentDir(cwd = process.cwd()) {
71
+ return path.join(cwd, '.agent');
72
+ }
73
+
74
+ function workspaceBrainDir(cwd = process.cwd()) {
75
+ return path.join(cwd, '.agent', 'skills', 'total-recall');
76
+ }
77
+
78
+ function loadBrainConfig(agentDir) {
79
+ const local = path.join(agentDir, 'skills', 'total-recall', 'config', 'brain.json');
80
+ const global = path.join(os.homedir(), '.agent', 'skills', 'total-recall', 'config', 'brain.json');
81
+ for (const file of [local, global]) {
82
+ if (!fs.existsSync(file)) continue;
83
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); }
84
+ catch { return null; }
85
+ }
86
+ return null;
87
+ }
88
+
89
+ function extractManagedBlock(content) {
90
+ const start = content.indexOf(INJECTION_BEGIN);
91
+ const end = content.indexOf(INJECTION_END);
92
+ if (start === -1 || end === -1 || end < start) return content.trim();
93
+ return content.slice(start, end + INJECTION_END.length).trim();
94
+ }
95
+
96
+ function replaceManagedBlock(existing, block) {
97
+ const start = existing.indexOf(INJECTION_BEGIN);
98
+ const end = existing.indexOf(INJECTION_END);
99
+ if (start === -1 || end === -1 || end < start) {
100
+ return `${existing.trimEnd()}\n\n${block}\n`;
101
+ }
102
+ return `${existing.slice(0, start)}${block}${existing.slice(end + INJECTION_END.length)}`;
103
+ }
104
+
105
+ function writeProjection(cwd, instructions) {
106
+ const instructionsFile = path.join(cwd, 'INSTRUCTIONS.md');
107
+ fs.writeFileSync(instructionsFile, instructions.endsWith('\n') ? instructions : `${instructions}\n`, 'utf8');
108
+
109
+ const block = extractManagedBlock(instructions);
110
+ const targets = {};
111
+ for (const shim of SHIMS) {
112
+ const target = path.join(cwd, shim);
113
+ const targetDir = path.dirname(target);
114
+ try {
115
+ if (fs.existsSync(target)) {
116
+ const stat = fs.lstatSync(target);
117
+ if (!stat.isSymbolicLink()) {
118
+ const existing = fs.readFileSync(target, 'utf8');
119
+ fs.writeFileSync(target, replaceManagedBlock(existing, block), 'utf8');
120
+ targets[shim] = 'updated';
121
+ } else {
122
+ targets[shim] = 'symlink';
123
+ }
124
+ } else {
125
+ // Ensure parent directory of the shim exists before writing/symlinking
126
+ if (!fs.existsSync(targetDir)) {
127
+ fs.mkdirSync(targetDir, { recursive: true });
128
+ }
129
+ // Use relative path target for symlink so it's correct from the subdirectory
130
+ const relativeTarget = path.relative(targetDir, instructionsFile);
131
+ fs.symlinkSync(relativeTarget, target);
132
+ targets[shim] = 'symlinked';
133
+ }
134
+ } catch (err) {
135
+ targets[shim] = `skipped: ${err.message}`;
136
+ }
137
+ }
138
+
139
+ return { instructionsFile, targets };
140
+ }
141
+
142
+ function writeSyncState(agentDir, state) {
143
+ const configDir = path.join(agentDir, 'skills', 'total-recall', 'config');
144
+ fs.mkdirSync(configDir, { recursive: true });
145
+ const stateFile = path.join(configDir, 'sync-state.json');
146
+ fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), 'utf8');
147
+ return stateFile;
148
+ }
149
+
150
+ async function pullInstructions(brainUrl, token) {
151
+ const headers = {};
152
+ if (token) headers.Authorization = `Bearer ${token}`;
153
+ const res = await fetch(`${brainUrl.replace(/\/$/, '')}/api/instructions`, { headers });
154
+ if (!res.ok) {
155
+ throw new Error(`Failed to pull instructions: HTTP ${res.status}`);
156
+ }
157
+ return res.json();
158
+ }
159
+
160
+ async function runOnce(opts) {
161
+ const cwd = process.cwd();
162
+ const agentDir = workspaceAgentDir(cwd);
163
+ const brainConfig = loadBrainConfig(agentDir);
164
+ const brainUrl = opts.brain || brainConfig?.url;
165
+ const token = opts.token || brainConfig?.token || totalRecallToken;
166
+
167
+ if (!brainUrl) {
168
+ throw new Error('No brain URL configured. Pass --brain <url> or create .agent/config/brain.json.');
169
+ }
170
+
171
+ // Auto-protect manual IDE edits locally and sync them to the brain first
172
+ const vaultDir = path.join(workspaceBrainDir(cwd), 'memory-vault');
173
+ const derivedDir = path.join(workspaceBrainDir(cwd), 'memory-derived');
174
+ const instructionsFile = path.join(cwd, 'INSTRUCTIONS.md');
175
+
176
+ // Ensure local directories exist so we can track and protect direct IDE manual edits
177
+ fs.mkdirSync(vaultDir, { recursive: true });
178
+ fs.mkdirSync(derivedDir, { recursive: true });
179
+
180
+ try {
181
+ const result = protectIDEInstructions({ vaultDir, derivedDir, instructionsFile });
182
+ if (result && result.imported > 0) {
183
+ console.error(` 🛡️ [total-recall] Auto-captured ${result.imported} manual IDE instruction edit(s) locally.`);
184
+
185
+ const headers = { 'Content-Type': 'application/json' };
186
+ if (token) headers.Authorization = `Bearer ${token}`;
187
+
188
+ for (const slug of result.slugs) {
189
+ const nodeFile = path.join(vaultDir, 'invariants', `${slug}.md`);
190
+ if (!fs.existsSync(nodeFile)) continue;
191
+
192
+ const raw = fs.readFileSync(nodeFile, 'utf8');
193
+ const { data, content } = matter(raw);
194
+
195
+ const payload = {
196
+ slug: data.slug,
197
+ title: data.title,
198
+ category: data.category,
199
+ content: content.trim(),
200
+ tags: data.tags,
201
+ priority: data.priority,
202
+ modality: data.modality,
203
+ confidence: data.confidence,
204
+ importance: data.importance,
205
+ status: data.status,
206
+ created: data.created,
207
+ updated: data.updated
208
+ };
209
+
210
+ console.error(` 🛡️ Pushing new invariant to remote brain: ${slug}...`);
211
+ const postRes = await fetch(`${brainUrl.replace(/\/$/, '')}/api/memory`, {
212
+ method: 'POST',
213
+ headers,
214
+ body: JSON.stringify(payload)
215
+ });
216
+
217
+ if (!postRes.ok) {
218
+ if (postRes.status === 409) {
219
+ console.error(` ℹ️ Invariant already exists on brain, updating: ${slug}...`);
220
+ const putRes = await fetch(`${brainUrl.replace(/\/$/, '')}/api/memory/${slug}`, {
221
+ method: 'PUT',
222
+ headers,
223
+ body: JSON.stringify(payload)
224
+ });
225
+ if (!putRes.ok) {
226
+ console.error(` ⚠️ Failed to update invariant on brain: HTTP ${putRes.status}`);
227
+ }
228
+ } else {
229
+ console.error(` ⚠️ Failed to push invariant to brain: HTTP ${postRes.status}`);
230
+ }
231
+ }
232
+ }
233
+
234
+ // Trigger remote recompile so the pulled instructions contain the new changes
235
+ console.error(` ⚙️ Triggering remote recompile on brain...`);
236
+ const compileHeaders = {};
237
+ if (token) compileHeaders.Authorization = `Bearer ${token}`;
238
+ const compileRes = await fetch(`${brainUrl.replace(/\/$/, '')}/api/vault/compile`, {
239
+ method: 'POST',
240
+ headers: compileHeaders
241
+ });
242
+
243
+ if (!compileRes.ok) {
244
+ console.error(` ⚠️ Failed to trigger remote compile: HTTP ${compileRes.status}`);
245
+ } else {
246
+ console.error(` ✅ Remote compile triggered successfully.`);
247
+ }
248
+ }
249
+ } catch (err) {
250
+ console.error(` ⚠️ [total-recall] Failed to run local instruction protection: ${err.message}`);
251
+ }
252
+
253
+ const remote = await pullInstructions(brainUrl, token);
254
+ if (!remote.content) {
255
+ throw new Error('Remote /api/instructions response did not include content.');
256
+ }
257
+
258
+ const projection = writeProjection(cwd, remote.content);
259
+
260
+ // Save pristine system compiled output locally for IDE edit diffing in subsequent sync cycles
261
+ try {
262
+ const backupFile = path.join(derivedDir, 'INSTRUCTIONS.system.md');
263
+ fs.writeFileSync(backupFile, remote.content, 'utf8');
264
+ } catch (err) {
265
+ // Non-fatal
266
+ }
267
+ const stateFile = writeSyncState(agentDir, {
268
+ last_sync: new Date().toISOString(),
269
+ brain_url: brainUrl,
270
+ remote: {
271
+ sha256: remote.sha256 || null,
272
+ bytes: remote.bytes || null,
273
+ modified: remote.modified || null
274
+ },
275
+ targets: projection.targets
276
+ });
277
+
278
+ return {
279
+ brain_url: brainUrl,
280
+ instructions_file: projection.instructionsFile,
281
+ sync_state_file: stateFile,
282
+ remote_sha256: remote.sha256 || null,
283
+ targets: projection.targets
284
+ };
285
+ }
286
+
287
+ // ─── GitHub fork backup ──────────────────────────────────────────────────────
288
+
289
+ // Paths to force-add to the fork (bypasses .gitignore).
290
+ // Encrypted secrets.enc are force-committed to enable seamless portability.
291
+ const VAULT_FORCE_ADD = [
292
+ '.agent/skills/total-recall/memory-vault',
293
+ 'INSTRUCTIONS.md',
294
+ '.agent/skills/total-recall/config/brain.json',
295
+ '.agent/skills/total-recall/config/secrets.enc',
296
+ '.agent/secrets.enc',
297
+ ];
298
+
299
+ function gitRun(args, opts = {}) {
300
+ const result = spawnSync('git', args, {
301
+ stdio: opts.silent ? 'pipe' : 'inherit',
302
+ encoding: 'utf8',
303
+ });
304
+ if (result.status !== 0 && !opts.ignoreErrors) {
305
+ throw new Error(`git ${args[0]} failed: ${(result.stderr || '').trim()}`);
306
+ }
307
+ return (result.stdout || '').trim();
308
+ }
309
+
310
+ function pushToFork() {
311
+ // Verify we're in a git repo
312
+ const root = gitRun(['rev-parse', '--show-toplevel'], { silent: true, ignoreErrors: true });
313
+ if (!root) throw new Error('Not inside a git repository.');
314
+
315
+ // Warn if origin appears to be the upstream (not a personal fork)
316
+ const originUrl = gitRun(['remote', 'get-url', 'origin'], { silent: true, ignoreErrors: true });
317
+ if (originUrl.includes('gregiteen/total-recall')) {
318
+ console.error(' ⚠️ origin is the upstream repo, not your personal fork.');
319
+ console.error(' Set up your fork first:');
320
+ console.error(' git remote rename origin upstream');
321
+ console.error(' git remote add origin https://github.com/YOUR_USERNAME/total-recall.git');
322
+ throw new Error('Configure your fork as origin before using sync --push.');
323
+ }
324
+
325
+ // Force-add vault files (bypasses .gitignore)
326
+ for (const target of VAULT_FORCE_ADD) {
327
+ if (fs.existsSync(path.join(root, target))) {
328
+ gitRun(['add', '-f', target], { ignoreErrors: true });
329
+ }
330
+ }
331
+
332
+ // Nothing staged? Already up to date.
333
+ const status = gitRun(['status', '--porcelain'], { silent: true });
334
+ if (!status) {
335
+ console.error(' ℹ Vault is already up to date with origin — nothing to push.');
336
+ return { committed: false };
337
+ }
338
+
339
+ const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
340
+ gitRun(['commit', '-m', `vault: backup ${timestamp}`]);
341
+ gitRun(['push', 'origin', 'main']);
342
+ console.error(` ✅ Vault backed up to origin at ${timestamp}`);
343
+
344
+ return { committed: true, timestamp };
345
+ }
346
+
347
+ // ─── Main ────────────────────────────────────────────────────────────────────
348
+
349
+ export default async function sync(args) {
350
+ const opts = parseArgs(args);
351
+ if (opts.help) {
352
+ printHelp();
353
+ return;
354
+ }
355
+
356
+ // --push mode: back up vault to GitHub fork
357
+ if (opts.push) {
358
+ pushToFork();
359
+ return;
360
+ }
361
+
362
+ async function tick() {
363
+ const result = await runOnce(opts);
364
+ console.error(` Synced ${result.instructions_file}`);
365
+ console.error(` Remote sha: ${result.remote_sha256 || 'unknown'}`);
366
+ return result;
367
+ }
368
+
369
+ await tick();
370
+ if (!opts.watch) return;
371
+
372
+ const intervalMs = Math.max(5, opts.intervalSeconds) * 1000;
373
+ console.error(` Watching every ${Math.round(intervalMs / 1000)}s. Press Ctrl+C to stop.`);
374
+ await new Promise((resolve) => {
375
+ const timer = setInterval(() => {
376
+ tick().catch(err => console.error(` Sync failed: ${err.message}`));
377
+ }, intervalMs);
378
+ process.on('SIGINT', () => {
379
+ clearInterval(timer);
380
+ resolve();
381
+ });
382
+ });
383
+ }