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,618 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { execSync, execFileSync } from 'node:child_process';
5
+ import { searxngBaseUrl, display, brainDir } from '../core/config.mjs';
6
+ import { runInSandbox } from '../core/sandbox.mjs';
7
+ import { logger } from '../core/logger.mjs';
8
+
9
+ // ─── Browser Session (persistent across tool calls in a process) ──────────────
10
+
11
+ let _browser = null;
12
+ let _page = null;
13
+
14
+ async function getBrowserPage() {
15
+ if (!_browser) {
16
+ try {
17
+ const { chromium } = await import('playwright');
18
+ _browser = await chromium.launch({
19
+ headless: true,
20
+ args: [
21
+ '--disable-dev-shm-usage',
22
+ // Only disable Chromium sandbox when running as root (where it's required)
23
+ ...(process.getuid?.() === 0 ? ['--no-sandbox', '--disable-setuid-sandbox'] : []),
24
+ ],
25
+ });
26
+ _page = await _browser.newPage();
27
+ await _page.setViewportSize({ width: 1280, height: 900 });
28
+ await _page.setExtraHTTPHeaders({ 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120 Safari/537.36' });
29
+ } catch (err) {
30
+ throw new Error(`Failed to load or spawn Playwright browser. Please ensure playwright is installed and your environment supports browser execution. (Detail: ${err.message})`);
31
+ }
32
+ }
33
+ return _page;
34
+ }
35
+
36
+ async function pageToMarkdown(page) {
37
+ // Extract readable text from current page
38
+ return page.evaluate(() => {
39
+ const clean = (el) => el?.innerText?.trim() || '';
40
+ const title = document.title;
41
+ const url = location.href;
42
+ // Remove nav/footer/script noise
43
+ ['script','style','nav','footer','header','aside'].forEach(tag => {
44
+ document.querySelectorAll(tag).forEach(n => n.remove());
45
+ });
46
+ const body = clean(document.body).slice(0, 6000);
47
+ return `# ${title}\nURL: ${url}\n\n${body}`;
48
+ });
49
+ }
50
+
51
+ // ─── Browser Tools ────────────────────────────────────────────────────────────
52
+
53
+ export async function browserNavigate(url) {
54
+ try {
55
+ const page = await getBrowserPage();
56
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
57
+ await page.waitForTimeout(1000);
58
+ const content = await pageToMarkdown(page);
59
+ return content;
60
+ } catch (err) {
61
+ return `Navigation failed: ${err.message}`;
62
+ }
63
+ }
64
+
65
+ export async function browserSearch(query) {
66
+ try {
67
+ const page = await getBrowserPage();
68
+ const url = `https://duckduckgo.com/?q=${encodeURIComponent(query)}&ia=web`;
69
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
70
+ await page.waitForTimeout(1500);
71
+ // Extract search results
72
+ const results = await page.evaluate(() => {
73
+ const items = [];
74
+ document.querySelectorAll('[data-testid="result"]').forEach((el, i) => {
75
+ if (i >= 6) return;
76
+ const title = el.querySelector('h2')?.innerText?.trim() || '';
77
+ const snippet = el.querySelector('[data-result="snippet"]')?.innerText?.trim() || '';
78
+ const link = el.querySelector('a')?.href || '';
79
+ if (title) items.push(`[${i+1}] ${title}\n${link}\n${snippet}`);
80
+ });
81
+ return items;
82
+ });
83
+ if (results.length === 0) {
84
+ // Fallback: return raw page text
85
+ return `Search results for "${query}":\n\n${(await pageToMarkdown(page)).slice(0, 3000)}`;
86
+ }
87
+ return `Search results for "${query}":\n\n${results.join('\n\n')}`;
88
+ } catch (err) {
89
+ return `Search failed: ${err.message}`;
90
+ }
91
+ }
92
+
93
+ export async function browserScreenshot() {
94
+ try {
95
+ const page = await getBrowserPage();
96
+ const buf = await page.screenshot({ type: 'png', fullPage: false });
97
+ const b64 = buf.toString('base64');
98
+ const url = page.url();
99
+ return `Screenshot taken of: ${url}\n[image/png base64]: ${b64.slice(0, 200)}... (${Math.round(b64.length / 1024)}KB)`;
100
+ } catch (err) {
101
+ return `Screenshot failed: ${err.message}`;
102
+ }
103
+ }
104
+
105
+ export async function browserClick(selector) {
106
+ try {
107
+ const page = await getBrowserPage();
108
+ // Try CSS selector first, then visible text
109
+ try {
110
+ await page.click(selector, { timeout: 5000 });
111
+ } catch {
112
+ await page.getByText(selector, { exact: false }).first().click({ timeout: 5000 });
113
+ }
114
+ await page.waitForTimeout(800);
115
+ const content = await pageToMarkdown(page);
116
+ return `Clicked "${selector}". Page is now:\n\n${content.slice(0, 2000)}`;
117
+ } catch (err) {
118
+ return `Click failed: ${err.message}`;
119
+ }
120
+ }
121
+
122
+ export async function browserType(selector, text) {
123
+ try {
124
+ const page = await getBrowserPage();
125
+ try {
126
+ await page.fill(selector, text);
127
+ } catch {
128
+ await page.getByLabel(selector, { exact: false }).first().fill(text);
129
+ }
130
+ return `Typed "${text}" into "${selector}"`;
131
+ } catch (err) {
132
+ return `Type failed: ${err.message}`;
133
+ }
134
+ }
135
+
136
+ export async function browserGetContent() {
137
+ try {
138
+ const page = await getBrowserPage();
139
+ const content = await pageToMarkdown(page);
140
+ return content;
141
+ } catch (err) {
142
+ return `Get content failed: ${err.message}`;
143
+ }
144
+ }
145
+
146
+ export async function browserEval(js) {
147
+ try {
148
+ const page = await getBrowserPage();
149
+ const result = await page.evaluate(js);
150
+ return `Result: ${JSON.stringify(result)}`;
151
+ } catch (err) {
152
+ return `Eval failed: ${err.message}`;
153
+ }
154
+ }
155
+
156
+ // ─── SearXNG Web Search ───────────────────────────────────────────────────────
157
+
158
+ export async function executeWebSearch(query) {
159
+ const searxngUrl = searxngBaseUrl || 'http://127.0.0.1:8888';
160
+ logger.info('search', `Query: "${query}" via ${searxngUrl}`);
161
+ try {
162
+ const url = new URL(`${searxngUrl}/search`);
163
+ url.searchParams.append('q', query);
164
+ url.searchParams.append('format', 'json');
165
+ const res = await fetch(url.toString(), {
166
+ headers: { Accept: 'application/json' },
167
+ signal: AbortSignal.timeout(8000),
168
+ });
169
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
170
+ const data = await res.json();
171
+ const results = (data.results || []).slice(0, 6);
172
+ if (results.length === 0) throw new Error('no results');
173
+ return `Search results for "${query}":\n\n` + results.map((r, i) =>
174
+ `[${i+1}] ${r.title}\n${r.url}\n${r.content || r.snippet || ''}`
175
+ ).join('\n\n');
176
+ } catch (err) {
177
+ logger.warn('search', 'SearXNG failed, falling back to browser search', { err: err.message });
178
+ return browserSearch(query);
179
+ }
180
+ }
181
+
182
+ // ─── Code Execution ───────────────────────────────────────────────────────────
183
+
184
+ export async function executeCode(code) {
185
+ try {
186
+ const tmpDir = path.join(os.tmpdir(), 'total-recall-sandbox');
187
+ fs.mkdirSync(tmpDir, { recursive: true });
188
+ const scriptPath = path.join(tmpDir, `script-${Date.now()}.mjs`);
189
+ fs.writeFileSync(scriptPath, code);
190
+ logger.info('sandbox', `Executing code at ${scriptPath}`);
191
+ const result = await runInSandbox(scriptPath, 15000);
192
+ try { fs.unlinkSync(scriptPath); } catch {}
193
+ return result.success
194
+ ? `Code executed successfully.\nOutput:\n${result.output}`
195
+ : `Code execution failed (exit ${result.code}).\nOutput:\n${result.output}`;
196
+ } catch (err) {
197
+ return `Error executing code: ${err.message}`;
198
+ }
199
+ }
200
+
201
+ // ─── Design Update ────────────────────────────────────────────────────────────
202
+
203
+ export async function updateDesign(markdown) {
204
+ try {
205
+ const configDir = path.join(brainDir, 'config');
206
+ fs.mkdirSync(configDir, { recursive: true });
207
+ fs.writeFileSync(path.join(configDir, 'DESIGN.md'), markdown);
208
+ return 'Successfully updated DESIGN.md.';
209
+ } catch (err) {
210
+ return `Error updating design: ${err.message}`;
211
+ }
212
+ }
213
+
214
+ // ─── Computer Use (Desktop / X11 Control) ────────────────────────────────────
215
+
216
+ let _xDisplay = null;
217
+
218
+ async function ensureDisplay() {
219
+ if (_xDisplay) return _xDisplay;
220
+ if (display) {
221
+ _xDisplay = display;
222
+ return _xDisplay;
223
+ }
224
+ // No DISPLAY — try to start Xvfb on :99
225
+ try {
226
+ execSync('Xvfb :99 -screen 0 1280x900x24 -ac &', { timeout: 3000, stdio: 'ignore' });
227
+ await new Promise(r => setTimeout(r, 1200));
228
+ process.env.DISPLAY = ':99';
229
+ _xDisplay = ':99';
230
+ } catch {
231
+ _xDisplay = ':0';
232
+ }
233
+ return _xDisplay;
234
+ }
235
+
236
+ function xrun(cmd) {
237
+ return execSync(cmd, { timeout: 8000, encoding: 'utf8' }).trim();
238
+ }
239
+
240
+ // Safe version for xdotool commands — uses execFileSync with argument arrays
241
+ function xrunSafe(args, envOverrides = {}) {
242
+ return execFileSync(args[0], args.slice(1), {
243
+ timeout: 8000,
244
+ encoding: 'utf8',
245
+ env: { ...process.env, ...envOverrides },
246
+ }).trim();
247
+ }
248
+
249
+ export async function computerScreenshot() {
250
+ try {
251
+ const display = await ensureDisplay();
252
+ const tmpFile = path.join(os.tmpdir(), `cu-shot-${Date.now()}.png`);
253
+ xrunSafe(['scrot', '-z', tmpFile], { DISPLAY: display });
254
+ const buf = fs.readFileSync(tmpFile);
255
+ const b64 = buf.toString('base64');
256
+ try { fs.unlinkSync(tmpFile); } catch {}
257
+ return `Desktop screenshot taken (${Math.round(b64.length / 1024)}KB)\n[image/png base64]: ${b64.slice(0, 200)}... (${b64.length} chars total)`;
258
+ } catch (err) {
259
+ return `Screenshot failed: ${err.message}`;
260
+ }
261
+ }
262
+
263
+ export async function computerMouseMove(x, y) {
264
+ try {
265
+ const display = await ensureDisplay();
266
+ const nx = parseInt(x, 10), ny = parseInt(y, 10);
267
+ if (isNaN(nx) || isNaN(ny)) throw new Error('Invalid coordinates');
268
+ xrunSafe(['xdotool', 'mousemove', String(nx), String(ny)], { DISPLAY: display });
269
+ return `Mouse moved to (${x}, ${y})`;
270
+ } catch (err) {
271
+ return `Mouse move failed: ${err.message}`;
272
+ }
273
+ }
274
+
275
+ export async function computerLeftClick(x, y) {
276
+ try {
277
+ const display = await ensureDisplay();
278
+ const nx = parseInt(x, 10), ny = parseInt(y, 10);
279
+ if (isNaN(nx) || isNaN(ny)) throw new Error('Invalid coordinates');
280
+ xrunSafe(['xdotool', 'mousemove', String(nx), String(ny), 'click', '1'], { DISPLAY: display });
281
+ return `Left-clicked at (${x}, ${y})`;
282
+ } catch (err) {
283
+ return `Click failed: ${err.message}`;
284
+ }
285
+ }
286
+
287
+ export async function computerDoubleClick(x, y) {
288
+ try {
289
+ const display = await ensureDisplay();
290
+ const nx = parseInt(x, 10), ny = parseInt(y, 10);
291
+ if (isNaN(nx) || isNaN(ny)) throw new Error('Invalid coordinates');
292
+ xrunSafe(['xdotool', 'mousemove', String(nx), String(ny), 'click', '--repeat', '2', '--delay', '100', '1'], { DISPLAY: display });
293
+ return `Double-clicked at (${x}, ${y})`;
294
+ } catch (err) {
295
+ return `Double-click failed: ${err.message}`;
296
+ }
297
+ }
298
+
299
+ export async function computerRightClick(x, y) {
300
+ try {
301
+ const display = await ensureDisplay();
302
+ const nx = parseInt(x, 10), ny = parseInt(y, 10);
303
+ if (isNaN(nx) || isNaN(ny)) throw new Error('Invalid coordinates');
304
+ xrunSafe(['xdotool', 'mousemove', String(nx), String(ny), 'click', '3'], { DISPLAY: display });
305
+ return `Right-clicked at (${x}, ${y})`;
306
+ } catch (err) {
307
+ return `Right-click failed: ${err.message}`;
308
+ }
309
+ }
310
+
311
+ export async function computerType(text) {
312
+ try {
313
+ const display = await ensureDisplay();
314
+ // Write text via temp file to avoid shell-quoting issues
315
+ const tmpFile = path.join(os.tmpdir(), `cu-type-${Date.now()}.txt`);
316
+ fs.writeFileSync(tmpFile, text);
317
+ xrunSafe(['xdotool', 'type', '--clearmodifiers', '--file', tmpFile], { DISPLAY: display });
318
+ try { fs.unlinkSync(tmpFile); } catch {}
319
+ return `Typed: ${text.slice(0, 80)}${text.length > 80 ? '…' : ''}`;
320
+ } catch (err) {
321
+ return `Type failed: ${err.message}`;
322
+ }
323
+ }
324
+
325
+ export async function computerKey(key) {
326
+ try {
327
+ const display = await ensureDisplay();
328
+ // Validate key against safe xdotool key name pattern
329
+ if (!/^[a-zA-Z0-9_+ ]+$/.test(key)) throw new Error('Invalid key name');
330
+ xrunSafe(['xdotool', 'key', key], { DISPLAY: display });
331
+ return `Key pressed: ${key}`;
332
+ } catch (err) {
333
+ return `Key press failed: ${err.message}`;
334
+ }
335
+ }
336
+
337
+ export async function computerScroll(x, y, direction, amount = 3) {
338
+ try {
339
+ const display = await ensureDisplay();
340
+ const button = direction === 'up' ? 4 : 5;
341
+ const nx = parseInt(x, 10), ny = parseInt(y, 10);
342
+ const amt = parseInt(amount, 10) || 3;
343
+ if (isNaN(nx) || isNaN(ny)) throw new Error('Invalid coordinates');
344
+ const clickArgs = [];
345
+ for (let i = 0; i < amt; i++) clickArgs.push('click', String(button));
346
+ xrunSafe(['xdotool', 'mousemove', String(nx), String(ny), ...clickArgs], { DISPLAY: display });
347
+ return `Scrolled ${direction} ${amount}× at (${x}, ${y})`;
348
+ } catch (err) {
349
+ return `Scroll failed: ${err.message}`;
350
+ }
351
+ }
352
+
353
+ // ─── Tool Definitions ─────────────────────────────────────────────────────────
354
+
355
+ export const AVAILABLE_TOOLS = [
356
+ {
357
+ type: 'function',
358
+ function: {
359
+ name: 'search_web',
360
+ description: 'Search the internet for current information, news, or facts. Uses local SearXNG if available, falls back to a real browser search automatically.',
361
+ parameters: {
362
+ type: 'object',
363
+ properties: {
364
+ query: { type: 'string', description: 'The search query.' },
365
+ },
366
+ required: ['query'],
367
+ },
368
+ },
369
+ },
370
+ {
371
+ type: 'function',
372
+ function: {
373
+ name: 'browser_navigate',
374
+ description: 'Navigate the browser to a URL and return the page content as markdown. Use this to read any webpage.',
375
+ parameters: {
376
+ type: 'object',
377
+ properties: {
378
+ url: { type: 'string', description: 'Full URL to navigate to (include https://).' },
379
+ },
380
+ required: ['url'],
381
+ },
382
+ },
383
+ },
384
+ {
385
+ type: 'function',
386
+ function: {
387
+ name: 'browser_click',
388
+ description: 'Click an element on the current page by CSS selector or visible text.',
389
+ parameters: {
390
+ type: 'object',
391
+ properties: {
392
+ selector: { type: 'string', description: 'CSS selector or visible text of the element to click.' },
393
+ },
394
+ required: ['selector'],
395
+ },
396
+ },
397
+ },
398
+ {
399
+ type: 'function',
400
+ function: {
401
+ name: 'browser_type',
402
+ description: 'Type text into an input field on the current page.',
403
+ parameters: {
404
+ type: 'object',
405
+ properties: {
406
+ selector: { type: 'string', description: 'CSS selector or label of the input field.' },
407
+ text: { type: 'string', description: 'Text to type.' },
408
+ },
409
+ required: ['selector', 'text'],
410
+ },
411
+ },
412
+ },
413
+ {
414
+ type: 'function',
415
+ function: {
416
+ name: 'browser_get_content',
417
+ description: 'Get the full text content of the current browser page as markdown.',
418
+ parameters: { type: 'object', properties: {} },
419
+ },
420
+ },
421
+ {
422
+ type: 'function',
423
+ function: {
424
+ name: 'browser_screenshot',
425
+ description: 'Take a screenshot of the current browser page.',
426
+ parameters: { type: 'object', properties: {} },
427
+ },
428
+ },
429
+ {
430
+ type: 'function',
431
+ function: {
432
+ name: 'browser_eval',
433
+ description: 'Run JavaScript in the current browser page and return the result. Use for extracting specific data or interacting with page APIs.',
434
+ parameters: {
435
+ type: 'object',
436
+ properties: {
437
+ js: { type: 'string', description: 'JavaScript expression to evaluate in the page context.' },
438
+ },
439
+ required: ['js'],
440
+ },
441
+ },
442
+ },
443
+ {
444
+ type: 'function',
445
+ function: {
446
+ name: 'execute_code',
447
+ description: 'Execute Node.js code in a secure sandbox. Use for API calls, data processing, calculations.',
448
+ parameters: {
449
+ type: 'object',
450
+ properties: {
451
+ code: { type: 'string', description: 'Node.js code to execute. Do not wrap in markdown.' },
452
+ },
453
+ required: ['code'],
454
+ },
455
+ },
456
+ },
457
+ {
458
+ type: 'function',
459
+ function: {
460
+ name: 'update_design',
461
+ description: 'Write markdown to the Sandbox DESIGN.md. Use when asked to create or update a UI or document.',
462
+ parameters: {
463
+ type: 'object',
464
+ properties: {
465
+ markdown: { type: 'string', description: 'Complete markdown content for DESIGN.md.' },
466
+ },
467
+ required: ['markdown'],
468
+ },
469
+ },
470
+ },
471
+ // ── Computer Use ──────────────────────────────────────────────────────────
472
+ {
473
+ type: 'function',
474
+ function: {
475
+ name: 'computer_screenshot',
476
+ description: 'Take a full screenshot of the computer desktop. Use this to see what is on screen before clicking.',
477
+ parameters: { type: 'object', properties: {} },
478
+ },
479
+ },
480
+ {
481
+ type: 'function',
482
+ function: {
483
+ name: 'computer_left_click',
484
+ description: 'Move the mouse to absolute screen coordinates and left-click. Always take a screenshot first to find the right coordinates.',
485
+ parameters: {
486
+ type: 'object',
487
+ properties: {
488
+ x: { type: 'integer', description: 'X pixel coordinate on screen' },
489
+ y: { type: 'integer', description: 'Y pixel coordinate on screen' },
490
+ },
491
+ required: ['x', 'y'],
492
+ },
493
+ },
494
+ },
495
+ {
496
+ type: 'function',
497
+ function: {
498
+ name: 'computer_double_click',
499
+ description: 'Move to screen coordinates and double-click (e.g. to open files or select words).',
500
+ parameters: {
501
+ type: 'object',
502
+ properties: {
503
+ x: { type: 'integer', description: 'X pixel coordinate' },
504
+ y: { type: 'integer', description: 'Y pixel coordinate' },
505
+ },
506
+ required: ['x', 'y'],
507
+ },
508
+ },
509
+ },
510
+ {
511
+ type: 'function',
512
+ function: {
513
+ name: 'computer_right_click',
514
+ description: 'Move to screen coordinates and right-click (opens context menus).',
515
+ parameters: {
516
+ type: 'object',
517
+ properties: {
518
+ x: { type: 'integer', description: 'X pixel coordinate' },
519
+ y: { type: 'integer', description: 'Y pixel coordinate' },
520
+ },
521
+ required: ['x', 'y'],
522
+ },
523
+ },
524
+ },
525
+ {
526
+ type: 'function',
527
+ function: {
528
+ name: 'computer_mouse_move',
529
+ description: 'Move the mouse cursor to screen coordinates without clicking.',
530
+ parameters: {
531
+ type: 'object',
532
+ properties: {
533
+ x: { type: 'integer', description: 'X pixel coordinate' },
534
+ y: { type: 'integer', description: 'Y pixel coordinate' },
535
+ },
536
+ required: ['x', 'y'],
537
+ },
538
+ },
539
+ },
540
+ {
541
+ type: 'function',
542
+ function: {
543
+ name: 'computer_type',
544
+ description: 'Type text at the current cursor position using keyboard simulation. Click an input field first.',
545
+ parameters: {
546
+ type: 'object',
547
+ properties: {
548
+ text: { type: 'string', description: 'Text to type' },
549
+ },
550
+ required: ['text'],
551
+ },
552
+ },
553
+ },
554
+ {
555
+ type: 'function',
556
+ function: {
557
+ name: 'computer_key',
558
+ description: 'Press a keyboard key or combo (e.g. "Return", "ctrl+c", "ctrl+v", "alt+Tab", "super", "Escape").',
559
+ parameters: {
560
+ type: 'object',
561
+ properties: {
562
+ key: { type: 'string', description: 'Key name in xdotool format' },
563
+ },
564
+ required: ['key'],
565
+ },
566
+ },
567
+ },
568
+ {
569
+ type: 'function',
570
+ function: {
571
+ name: 'computer_scroll',
572
+ description: 'Scroll the mouse wheel at a screen position.',
573
+ parameters: {
574
+ type: 'object',
575
+ properties: {
576
+ x: { type: 'integer', description: 'X coordinate' },
577
+ y: { type: 'integer', description: 'Y coordinate' },
578
+ direction: { type: 'string', enum: ['up', 'down'], description: 'Scroll direction' },
579
+ amount: { type: 'integer', description: 'Number of scroll clicks (default 3)' },
580
+ },
581
+ required: ['x', 'y', 'direction'],
582
+ },
583
+ },
584
+ },
585
+ ];
586
+
587
+ // ─── Tool Dispatcher ──────────────────────────────────────────────────────────
588
+
589
+ export async function handleToolCall(toolCall) {
590
+ const { name, arguments: argsString } = toolCall.function;
591
+ try {
592
+ const args = JSON.parse(argsString);
593
+ switch (name) {
594
+ case 'search_web': return await executeWebSearch(args.query);
595
+ case 'browser_navigate': return await browserNavigate(args.url);
596
+ case 'browser_click': return await browserClick(args.selector);
597
+ case 'browser_type': return await browserType(args.selector, args.text);
598
+ case 'browser_get_content': return await browserGetContent();
599
+ case 'browser_screenshot': return await browserScreenshot();
600
+ case 'browser_eval': return await browserEval(args.js);
601
+ case 'execute_code': return await executeCode(args.code);
602
+ case 'update_design': return await updateDesign(args.markdown);
603
+ // Computer use
604
+ case 'computer_screenshot': return await computerScreenshot();
605
+ case 'computer_left_click': return await computerLeftClick(args.x, args.y);
606
+ case 'computer_double_click': return await computerDoubleClick(args.x, args.y);
607
+ case 'computer_right_click': return await computerRightClick(args.x, args.y);
608
+ case 'computer_mouse_move': return await computerMouseMove(args.x, args.y);
609
+ case 'computer_type': return await computerType(args.text);
610
+ case 'computer_key': return await computerKey(args.key);
611
+ case 'computer_scroll': return await computerScroll(args.x, args.y, args.direction, args.amount ?? 3);
612
+ default: return `Unknown tool: ${name}`;
613
+ }
614
+ } catch (err) {
615
+ logger.error('tools', `Error in ${name}`, { err: err.message });
616
+ return `Tool execution failed: ${err.message}`;
617
+ }
618
+ }
@@ -0,0 +1,37 @@
1
+ # Total Recall — Caddy Reverse Proxy Configuration
2
+ #
3
+ # Deployed to /etc/caddy/Caddyfile during `npx total-recall deploy --domain <your-domain>`
4
+ #
5
+ # Recommended: Use Cloudflare Tunnel instead of Caddy for HTTPS — zero port-forwarding required.
6
+ # Run: npx total-recall deploy --cloudflare-quick (free temp URL, no account)
7
+ # Or: npx total-recall deploy --cloudflare-token <token> (permanent custom domain)
8
+ #
9
+ # If you prefer Caddy + Let's Encrypt (VPS with open port 80):
10
+ # 1. Point a domain at your server IP (any registrar — duckdns.org is free)
11
+ # 2. Deploy with: npx total-recall deploy --domain yourdomain.com
12
+ # 3. Caddy auto-provisions a TLS cert via HTTP-01 challenge (port 80 must be open)
13
+ # 4. Your dashboard will be at https://yourdomain.com
14
+ #
15
+ # Caddy replaces YOUR_DOMAIN below with your --domain value at deploy time.
16
+
17
+ YOUR_DOMAIN {
18
+ # Reverse proxy to Express server
19
+ reverse_proxy 127.0.0.1:3000
20
+
21
+ # Security headers
22
+ header {
23
+ -Server
24
+ X-Content-Type-Options nosniff
25
+ X-Frame-Options SAMEORIGIN
26
+ Referrer-Policy strict-origin-when-cross-origin
27
+ Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
28
+ Permissions-Policy "camera=(), microphone=(), geolocation=()"
29
+ Content-Security-Policy "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; script-src 'self'; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'"
30
+ }
31
+
32
+ # Logging
33
+ log {
34
+ output file /var/log/caddy/access.log
35
+ format json
36
+ }
37
+ }
@@ -0,0 +1,42 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>Label</key>
6
+ <string>com.totalrecall.backup</string>
7
+
8
+ <key>ProgramArguments</key>
9
+ <array>
10
+ <string>__NODE__</string>
11
+ <string>__ROOT__/bin/total-recall.mjs</string>
12
+ <string>backup</string>
13
+ <string>--push-git</string>
14
+ <string>__BACKUP_REPO__</string>
15
+ </array>
16
+
17
+ <!-- Run daily at 2:00 AM -->
18
+ <key>StartCalendarInterval</key>
19
+ <dict>
20
+ <key>Hour</key>
21
+ <integer>2</integer>
22
+ <key>Minute</key>
23
+ <integer>0</integer>
24
+ </dict>
25
+
26
+ <key>EnvironmentVariables</key>
27
+ <dict>
28
+ <key>HOME</key>
29
+ <string>__HOME__</string>
30
+ <key>PATH</key>
31
+ <string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
32
+ </dict>
33
+
34
+ <key>StandardOutPath</key>
35
+ <string>__HOME__/.agent/logs/backup.log</string>
36
+ <key>StandardErrorPath</key>
37
+ <string>__HOME__/.agent/logs/backup.log</string>
38
+
39
+ <key>RunAtLoad</key>
40
+ <false/>
41
+ </dict>
42
+ </plist>