@usejarvis/brain 0.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 (266) hide show
  1. package/LICENSE +153 -0
  2. package/README.md +278 -0
  3. package/bin/jarvis.ts +413 -0
  4. package/package.json +74 -0
  5. package/scripts/ensure-bun.cjs +8 -0
  6. package/src/actions/README.md +421 -0
  7. package/src/actions/app-control/desktop-controller.test.ts +26 -0
  8. package/src/actions/app-control/desktop-controller.ts +438 -0
  9. package/src/actions/app-control/interface.ts +64 -0
  10. package/src/actions/app-control/linux.ts +273 -0
  11. package/src/actions/app-control/macos.ts +54 -0
  12. package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
  13. package/src/actions/app-control/sidecar-launcher.ts +286 -0
  14. package/src/actions/app-control/windows.ts +44 -0
  15. package/src/actions/browser/cdp.ts +138 -0
  16. package/src/actions/browser/chrome-launcher.ts +252 -0
  17. package/src/actions/browser/session.ts +437 -0
  18. package/src/actions/browser/stealth.ts +49 -0
  19. package/src/actions/index.ts +20 -0
  20. package/src/actions/terminal/executor.ts +157 -0
  21. package/src/actions/terminal/wsl-bridge.ts +126 -0
  22. package/src/actions/test.ts +93 -0
  23. package/src/actions/tools/agents.ts +321 -0
  24. package/src/actions/tools/builtin.ts +846 -0
  25. package/src/actions/tools/commitments.ts +192 -0
  26. package/src/actions/tools/content.ts +217 -0
  27. package/src/actions/tools/delegate.ts +147 -0
  28. package/src/actions/tools/desktop.test.ts +55 -0
  29. package/src/actions/tools/desktop.ts +305 -0
  30. package/src/actions/tools/goals.ts +376 -0
  31. package/src/actions/tools/local-tools-guard.ts +20 -0
  32. package/src/actions/tools/registry.ts +171 -0
  33. package/src/actions/tools/research.ts +111 -0
  34. package/src/actions/tools/sidecar-list.ts +57 -0
  35. package/src/actions/tools/sidecar-route.ts +105 -0
  36. package/src/actions/tools/workflows.ts +216 -0
  37. package/src/agents/agent.ts +132 -0
  38. package/src/agents/delegation.ts +107 -0
  39. package/src/agents/hierarchy.ts +113 -0
  40. package/src/agents/index.ts +19 -0
  41. package/src/agents/messaging.ts +125 -0
  42. package/src/agents/orchestrator.ts +576 -0
  43. package/src/agents/role-discovery.ts +61 -0
  44. package/src/agents/sub-agent-runner.ts +307 -0
  45. package/src/agents/task-manager.ts +151 -0
  46. package/src/authority/approval-delivery.ts +59 -0
  47. package/src/authority/approval.ts +196 -0
  48. package/src/authority/audit.ts +158 -0
  49. package/src/authority/authority.test.ts +519 -0
  50. package/src/authority/deferred-executor.ts +103 -0
  51. package/src/authority/emergency.ts +66 -0
  52. package/src/authority/engine.ts +297 -0
  53. package/src/authority/index.ts +12 -0
  54. package/src/authority/learning.ts +111 -0
  55. package/src/authority/tool-action-map.ts +74 -0
  56. package/src/awareness/analytics.ts +466 -0
  57. package/src/awareness/awareness.test.ts +332 -0
  58. package/src/awareness/capture-engine.ts +305 -0
  59. package/src/awareness/context-graph.ts +130 -0
  60. package/src/awareness/context-tracker.ts +349 -0
  61. package/src/awareness/index.ts +25 -0
  62. package/src/awareness/intelligence.ts +321 -0
  63. package/src/awareness/ocr-engine.ts +88 -0
  64. package/src/awareness/service.ts +528 -0
  65. package/src/awareness/struggle-detector.ts +342 -0
  66. package/src/awareness/suggestion-engine.ts +476 -0
  67. package/src/awareness/types.ts +201 -0
  68. package/src/cli/autostart.ts +241 -0
  69. package/src/cli/deps.ts +449 -0
  70. package/src/cli/doctor.ts +230 -0
  71. package/src/cli/helpers.ts +401 -0
  72. package/src/cli/onboard.ts +580 -0
  73. package/src/comms/README.md +329 -0
  74. package/src/comms/auth-error.html +48 -0
  75. package/src/comms/channels/discord.ts +228 -0
  76. package/src/comms/channels/signal.ts +56 -0
  77. package/src/comms/channels/telegram.ts +316 -0
  78. package/src/comms/channels/whatsapp.ts +60 -0
  79. package/src/comms/channels.test.ts +173 -0
  80. package/src/comms/desktop-notify.ts +114 -0
  81. package/src/comms/example.ts +129 -0
  82. package/src/comms/index.ts +129 -0
  83. package/src/comms/streaming.ts +142 -0
  84. package/src/comms/voice.test.ts +152 -0
  85. package/src/comms/voice.ts +291 -0
  86. package/src/comms/websocket.test.ts +409 -0
  87. package/src/comms/websocket.ts +473 -0
  88. package/src/config/README.md +387 -0
  89. package/src/config/index.ts +6 -0
  90. package/src/config/loader.test.ts +137 -0
  91. package/src/config/loader.ts +142 -0
  92. package/src/config/types.ts +260 -0
  93. package/src/daemon/README.md +232 -0
  94. package/src/daemon/agent-service-interface.ts +9 -0
  95. package/src/daemon/agent-service.ts +600 -0
  96. package/src/daemon/api-routes.ts +2119 -0
  97. package/src/daemon/background-agent-service.ts +396 -0
  98. package/src/daemon/background-agent.test.ts +78 -0
  99. package/src/daemon/channel-service.ts +201 -0
  100. package/src/daemon/commitment-executor.ts +297 -0
  101. package/src/daemon/event-classifier.ts +239 -0
  102. package/src/daemon/event-coalescer.ts +123 -0
  103. package/src/daemon/event-reactor.ts +214 -0
  104. package/src/daemon/health.ts +220 -0
  105. package/src/daemon/index.ts +1004 -0
  106. package/src/daemon/llm-settings.ts +316 -0
  107. package/src/daemon/observer-service.ts +150 -0
  108. package/src/daemon/pid.ts +98 -0
  109. package/src/daemon/research-queue.ts +155 -0
  110. package/src/daemon/services.ts +175 -0
  111. package/src/daemon/ws-service.ts +788 -0
  112. package/src/goals/accountability.ts +240 -0
  113. package/src/goals/awareness-bridge.ts +185 -0
  114. package/src/goals/estimator.ts +185 -0
  115. package/src/goals/events.ts +28 -0
  116. package/src/goals/goals.test.ts +400 -0
  117. package/src/goals/integration.test.ts +329 -0
  118. package/src/goals/nl-builder.test.ts +220 -0
  119. package/src/goals/nl-builder.ts +256 -0
  120. package/src/goals/rhythm.test.ts +177 -0
  121. package/src/goals/rhythm.ts +275 -0
  122. package/src/goals/service.test.ts +135 -0
  123. package/src/goals/service.ts +348 -0
  124. package/src/goals/types.ts +106 -0
  125. package/src/goals/workflow-bridge.ts +96 -0
  126. package/src/integrations/google-api.ts +134 -0
  127. package/src/integrations/google-auth.ts +175 -0
  128. package/src/llm/README.md +291 -0
  129. package/src/llm/anthropic.ts +386 -0
  130. package/src/llm/gemini.ts +371 -0
  131. package/src/llm/index.ts +19 -0
  132. package/src/llm/manager.ts +153 -0
  133. package/src/llm/ollama.ts +307 -0
  134. package/src/llm/openai.ts +350 -0
  135. package/src/llm/provider.test.ts +231 -0
  136. package/src/llm/provider.ts +60 -0
  137. package/src/llm/test.ts +87 -0
  138. package/src/observers/README.md +278 -0
  139. package/src/observers/calendar.ts +113 -0
  140. package/src/observers/clipboard.ts +136 -0
  141. package/src/observers/email.ts +109 -0
  142. package/src/observers/example.ts +58 -0
  143. package/src/observers/file-watcher.ts +124 -0
  144. package/src/observers/index.ts +159 -0
  145. package/src/observers/notifications.ts +197 -0
  146. package/src/observers/observers.test.ts +203 -0
  147. package/src/observers/processes.ts +225 -0
  148. package/src/personality/README.md +61 -0
  149. package/src/personality/adapter.ts +196 -0
  150. package/src/personality/index.ts +20 -0
  151. package/src/personality/learner.ts +209 -0
  152. package/src/personality/model.ts +132 -0
  153. package/src/personality/personality.test.ts +236 -0
  154. package/src/roles/README.md +252 -0
  155. package/src/roles/authority.ts +119 -0
  156. package/src/roles/example-usage.ts +198 -0
  157. package/src/roles/index.ts +42 -0
  158. package/src/roles/loader.ts +143 -0
  159. package/src/roles/prompt-builder.ts +194 -0
  160. package/src/roles/test-multi.ts +102 -0
  161. package/src/roles/test-role.yaml +77 -0
  162. package/src/roles/test-utils.ts +93 -0
  163. package/src/roles/test.ts +106 -0
  164. package/src/roles/tool-guide.ts +190 -0
  165. package/src/roles/types.ts +36 -0
  166. package/src/roles/utils.ts +200 -0
  167. package/src/scripts/google-setup.ts +168 -0
  168. package/src/sidecar/connection.ts +179 -0
  169. package/src/sidecar/index.ts +6 -0
  170. package/src/sidecar/manager.ts +542 -0
  171. package/src/sidecar/protocol.ts +85 -0
  172. package/src/sidecar/rpc.ts +161 -0
  173. package/src/sidecar/scheduler.ts +136 -0
  174. package/src/sidecar/types.ts +112 -0
  175. package/src/sidecar/validator.ts +144 -0
  176. package/src/vault/README.md +110 -0
  177. package/src/vault/awareness.ts +341 -0
  178. package/src/vault/commitments.ts +299 -0
  179. package/src/vault/content-pipeline.ts +260 -0
  180. package/src/vault/conversations.ts +173 -0
  181. package/src/vault/entities.ts +180 -0
  182. package/src/vault/extractor.test.ts +356 -0
  183. package/src/vault/extractor.ts +345 -0
  184. package/src/vault/facts.ts +190 -0
  185. package/src/vault/goals.ts +477 -0
  186. package/src/vault/index.ts +87 -0
  187. package/src/vault/keychain.ts +99 -0
  188. package/src/vault/observations.ts +115 -0
  189. package/src/vault/relationships.ts +178 -0
  190. package/src/vault/retrieval.test.ts +126 -0
  191. package/src/vault/retrieval.ts +227 -0
  192. package/src/vault/schema.ts +658 -0
  193. package/src/vault/settings.ts +38 -0
  194. package/src/vault/vectors.ts +92 -0
  195. package/src/vault/workflows.ts +403 -0
  196. package/src/workflows/auto-suggest.ts +290 -0
  197. package/src/workflows/engine.ts +366 -0
  198. package/src/workflows/events.ts +24 -0
  199. package/src/workflows/executor.ts +207 -0
  200. package/src/workflows/nl-builder.ts +198 -0
  201. package/src/workflows/nodes/actions/agent-task.ts +73 -0
  202. package/src/workflows/nodes/actions/calendar-action.ts +85 -0
  203. package/src/workflows/nodes/actions/code-execution.ts +73 -0
  204. package/src/workflows/nodes/actions/discord.ts +77 -0
  205. package/src/workflows/nodes/actions/file-write.ts +73 -0
  206. package/src/workflows/nodes/actions/gmail.ts +69 -0
  207. package/src/workflows/nodes/actions/http-request.ts +117 -0
  208. package/src/workflows/nodes/actions/notification.ts +85 -0
  209. package/src/workflows/nodes/actions/run-tool.ts +55 -0
  210. package/src/workflows/nodes/actions/send-message.ts +82 -0
  211. package/src/workflows/nodes/actions/shell-command.ts +76 -0
  212. package/src/workflows/nodes/actions/telegram.ts +60 -0
  213. package/src/workflows/nodes/builtin.ts +119 -0
  214. package/src/workflows/nodes/error/error-handler.ts +37 -0
  215. package/src/workflows/nodes/error/fallback.ts +47 -0
  216. package/src/workflows/nodes/error/retry.ts +82 -0
  217. package/src/workflows/nodes/logic/delay.ts +42 -0
  218. package/src/workflows/nodes/logic/if-else.ts +41 -0
  219. package/src/workflows/nodes/logic/loop.ts +90 -0
  220. package/src/workflows/nodes/logic/merge.ts +38 -0
  221. package/src/workflows/nodes/logic/race.ts +40 -0
  222. package/src/workflows/nodes/logic/switch.ts +59 -0
  223. package/src/workflows/nodes/logic/template-render.ts +53 -0
  224. package/src/workflows/nodes/logic/variable-get.ts +37 -0
  225. package/src/workflows/nodes/logic/variable-set.ts +59 -0
  226. package/src/workflows/nodes/registry.ts +99 -0
  227. package/src/workflows/nodes/transform/aggregate.ts +99 -0
  228. package/src/workflows/nodes/transform/csv-parse.ts +70 -0
  229. package/src/workflows/nodes/transform/json-parse.ts +63 -0
  230. package/src/workflows/nodes/transform/map-filter.ts +84 -0
  231. package/src/workflows/nodes/transform/regex-match.ts +89 -0
  232. package/src/workflows/nodes/triggers/calendar.ts +33 -0
  233. package/src/workflows/nodes/triggers/clipboard.ts +32 -0
  234. package/src/workflows/nodes/triggers/cron.ts +40 -0
  235. package/src/workflows/nodes/triggers/email.ts +40 -0
  236. package/src/workflows/nodes/triggers/file-change.ts +45 -0
  237. package/src/workflows/nodes/triggers/git.ts +46 -0
  238. package/src/workflows/nodes/triggers/manual.ts +23 -0
  239. package/src/workflows/nodes/triggers/poll.ts +81 -0
  240. package/src/workflows/nodes/triggers/process.ts +44 -0
  241. package/src/workflows/nodes/triggers/screen-event.ts +37 -0
  242. package/src/workflows/nodes/triggers/webhook.ts +39 -0
  243. package/src/workflows/safe-eval.ts +139 -0
  244. package/src/workflows/template.ts +118 -0
  245. package/src/workflows/triggers/cron.ts +311 -0
  246. package/src/workflows/triggers/manager.ts +285 -0
  247. package/src/workflows/triggers/observer-bridge.ts +172 -0
  248. package/src/workflows/triggers/poller.ts +201 -0
  249. package/src/workflows/triggers/screen-condition.ts +218 -0
  250. package/src/workflows/triggers/triggers.test.ts +740 -0
  251. package/src/workflows/triggers/webhook.ts +191 -0
  252. package/src/workflows/types.ts +133 -0
  253. package/src/workflows/variables.ts +72 -0
  254. package/src/workflows/workflows.test.ts +383 -0
  255. package/src/workflows/yaml.ts +104 -0
  256. package/ui/dist/index-j75njzc1.css +1199 -0
  257. package/ui/dist/index-p2zh407q.js +80603 -0
  258. package/ui/dist/index.html +13 -0
  259. package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
  260. package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
  261. package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
  262. package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
  263. package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
  264. package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
  265. package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
  266. package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Autostart Setup for J.A.R.V.I.S.
3
+ *
4
+ * Installs/uninstalls daemon autostart on system boot:
5
+ * - Linux: systemd user service
6
+ * - macOS: launchd plist
7
+ */
8
+
9
+ import { join } from 'node:path';
10
+ import { homedir } from 'node:os';
11
+ import { existsSync, mkdirSync, writeFileSync, unlinkSync } from 'node:fs';
12
+ import { c, printOk, printErr, printWarn } from './helpers.ts';
13
+
14
+ function getBunPath(): string {
15
+ try {
16
+ return Bun.which('bun') ?? 'bun';
17
+ } catch {
18
+ return 'bun';
19
+ }
20
+ }
21
+
22
+ function getJarvisPath(): string {
23
+ // When installed globally, import.meta.dir points to the package
24
+ return join(import.meta.dir, '../../bin/jarvis.ts');
25
+ }
26
+
27
+ // ── systemd (Linux) ──────────────────────────────────────────────────
28
+
29
+ const SYSTEMD_DIR = join(homedir(), '.config', 'systemd', 'user');
30
+ const SYSTEMD_SERVICE = join(SYSTEMD_DIR, 'jarvis.service');
31
+
32
+ function generateSystemdUnit(): string {
33
+ const bunPath = getBunPath();
34
+ const jarvisPath = getJarvisPath();
35
+
36
+ return `[Unit]
37
+ Description=J.A.R.V.I.S. Daemon
38
+ After=network.target
39
+
40
+ [Service]
41
+ Type=simple
42
+ ExecStart=${bunPath} ${jarvisPath} start --foreground
43
+ Restart=on-failure
44
+ RestartSec=5
45
+ Environment=HOME=${homedir()}
46
+
47
+ [Install]
48
+ WantedBy=default.target
49
+ `;
50
+ }
51
+
52
+ async function installSystemd(): Promise<boolean> {
53
+ try {
54
+ if (!existsSync(SYSTEMD_DIR)) {
55
+ mkdirSync(SYSTEMD_DIR, { recursive: true });
56
+ }
57
+
58
+ writeFileSync(SYSTEMD_SERVICE, generateSystemdUnit(), 'utf-8');
59
+
60
+ // Reload systemd and enable
61
+ const reload = Bun.spawnSync(['systemctl', '--user', 'daemon-reload']);
62
+ if (reload.exitCode !== 0) {
63
+ printErr('Failed to reload systemd. You may need to run: systemctl --user daemon-reload');
64
+ return false;
65
+ }
66
+
67
+ const enable = Bun.spawnSync(['systemctl', '--user', 'enable', 'jarvis.service']);
68
+ if (enable.exitCode !== 0) {
69
+ printErr('Failed to enable service. You may need to run: systemctl --user enable jarvis.service');
70
+ return false;
71
+ }
72
+
73
+ // Enable lingering so the service runs even when not logged in
74
+ const lingering = Bun.spawnSync(['loginctl', 'enable-linger', process.env.USER ?? '']);
75
+ if (lingering.exitCode !== 0) {
76
+ printWarn('Could not enable lingering. Service may stop when you log out.');
77
+ }
78
+
79
+ printOk(`Installed systemd service: ${SYSTEMD_SERVICE}`);
80
+ printOk('Service will start on boot. To start now: systemctl --user start jarvis');
81
+ return true;
82
+ } catch (err) {
83
+ printErr(`Failed to install systemd service: ${err}`);
84
+ return false;
85
+ }
86
+ }
87
+
88
+ async function uninstallSystemd(): Promise<boolean> {
89
+ try {
90
+ Bun.spawnSync(['systemctl', '--user', 'stop', 'jarvis.service']);
91
+ Bun.spawnSync(['systemctl', '--user', 'disable', 'jarvis.service']);
92
+
93
+ if (existsSync(SYSTEMD_SERVICE)) {
94
+ unlinkSync(SYSTEMD_SERVICE);
95
+ }
96
+
97
+ Bun.spawnSync(['systemctl', '--user', 'daemon-reload']);
98
+ printOk('Uninstalled systemd service.');
99
+ return true;
100
+ } catch (err) {
101
+ printErr(`Failed to uninstall systemd service: ${err}`);
102
+ return false;
103
+ }
104
+ }
105
+
106
+ function isSystemdInstalled(): boolean {
107
+ return existsSync(SYSTEMD_SERVICE);
108
+ }
109
+
110
+ // ── launchd (macOS) ──────────────────────────────────────────────────
111
+
112
+ const LAUNCHD_DIR = join(homedir(), 'Library', 'LaunchAgents');
113
+ const LAUNCHD_PLIST = join(LAUNCHD_DIR, 'ai.jarvis.daemon.plist');
114
+
115
+ function generateLaunchdPlist(): string {
116
+ const bunPath = getBunPath();
117
+ const jarvisPath = getJarvisPath();
118
+ const logDir = join(homedir(), '.jarvis', 'logs');
119
+
120
+ return `<?xml version="1.0" encoding="UTF-8"?>
121
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
122
+ <plist version="1.0">
123
+ <dict>
124
+ <key>Label</key>
125
+ <string>ai.jarvis.daemon</string>
126
+ <key>ProgramArguments</key>
127
+ <array>
128
+ <string>${bunPath}</string>
129
+ <string>${jarvisPath}</string>
130
+ <string>start</string>
131
+ <string>--foreground</string>
132
+ </array>
133
+ <key>RunAtLoad</key>
134
+ <true/>
135
+ <key>KeepAlive</key>
136
+ <true/>
137
+ <key>StandardOutPath</key>
138
+ <string>${logDir}/jarvis.log</string>
139
+ <key>StandardErrorPath</key>
140
+ <string>${logDir}/jarvis-error.log</string>
141
+ <key>EnvironmentVariables</key>
142
+ <dict>
143
+ <key>HOME</key>
144
+ <string>${homedir()}</string>
145
+ <key>PATH</key>
146
+ <string>/usr/local/bin:/usr/bin:/bin:${join(homedir(), '.bun', 'bin')}</string>
147
+ </dict>
148
+ </dict>
149
+ </plist>
150
+ `;
151
+ }
152
+
153
+ async function installLaunchd(): Promise<boolean> {
154
+ try {
155
+ if (!existsSync(LAUNCHD_DIR)) {
156
+ mkdirSync(LAUNCHD_DIR, { recursive: true });
157
+ }
158
+
159
+ // Ensure log directory exists
160
+ const logDir = join(homedir(), '.jarvis', 'logs');
161
+ if (!existsSync(logDir)) {
162
+ mkdirSync(logDir, { recursive: true });
163
+ }
164
+
165
+ writeFileSync(LAUNCHD_PLIST, generateLaunchdPlist(), 'utf-8');
166
+
167
+ // Load the plist
168
+ const load = Bun.spawnSync(['launchctl', 'load', LAUNCHD_PLIST]);
169
+ if (load.exitCode !== 0) {
170
+ printWarn('Could not load plist immediately. It will start on next login.');
171
+ }
172
+
173
+ printOk(`Installed launchd plist: ${LAUNCHD_PLIST}`);
174
+ printOk('Service will start on login.');
175
+ return true;
176
+ } catch (err) {
177
+ printErr(`Failed to install launchd plist: ${err}`);
178
+ return false;
179
+ }
180
+ }
181
+
182
+ async function uninstallLaunchd(): Promise<boolean> {
183
+ try {
184
+ if (existsSync(LAUNCHD_PLIST)) {
185
+ Bun.spawnSync(['launchctl', 'unload', LAUNCHD_PLIST]);
186
+ unlinkSync(LAUNCHD_PLIST);
187
+ }
188
+
189
+ printOk('Uninstalled launchd plist.');
190
+ return true;
191
+ } catch (err) {
192
+ printErr(`Failed to uninstall launchd plist: ${err}`);
193
+ return false;
194
+ }
195
+ }
196
+
197
+ function isLaunchdInstalled(): boolean {
198
+ return existsSync(LAUNCHD_PLIST);
199
+ }
200
+
201
+ // ── Public API ───────────────────────────────────────────────────────
202
+
203
+ /**
204
+ * Install autostart for the current platform.
205
+ */
206
+ export async function installAutostart(): Promise<boolean> {
207
+ if (process.platform === 'darwin') {
208
+ return installLaunchd();
209
+ }
210
+ return installSystemd();
211
+ }
212
+
213
+ /**
214
+ * Uninstall autostart for the current platform.
215
+ */
216
+ export async function uninstallAutostart(): Promise<boolean> {
217
+ if (process.platform === 'darwin') {
218
+ return uninstallLaunchd();
219
+ }
220
+ return uninstallSystemd();
221
+ }
222
+
223
+ /**
224
+ * Check if autostart is installed for the current platform.
225
+ */
226
+ export function isAutostartInstalled(): boolean {
227
+ if (process.platform === 'darwin') {
228
+ return isLaunchdInstalled();
229
+ }
230
+ return isSystemdInstalled();
231
+ }
232
+
233
+ /**
234
+ * Get the name of the autostart mechanism for the current platform.
235
+ */
236
+ export function getAutostartName(): string {
237
+ if (process.platform === 'darwin') {
238
+ return 'launchd (Login Item)';
239
+ }
240
+ return 'systemd (User Service)';
241
+ }
@@ -0,0 +1,449 @@
1
+ /**
2
+ * System Dependency Checker & Installer
3
+ *
4
+ * Detects and offers to install system dependencies during onboard:
5
+ * - Chromium/Chrome browser (all platforms)
6
+ * - Linux X11 tools: xdotool, xprop, imagemagick (Linux/WSL)
7
+ * - Google OAuth tokens (optional, all platforms)
8
+ */
9
+
10
+ import { existsSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import { homedir } from 'node:os';
13
+ import { spawnSync } from 'bun';
14
+ import { c, printOk, printWarn, printErr, printInfo, askYesNo, ask, askSecret, startSpinner, detectPlatform } from './helpers.ts';
15
+ import { LINUX_BROWSER_PATHS, MACOS_BROWSER_PATHS, type BrowserExecutable } from '../actions/browser/chrome-launcher.ts';
16
+
17
+ export type DepStatus = {
18
+ name: string;
19
+ found: boolean;
20
+ path?: string;
21
+ message: string;
22
+ installable: boolean;
23
+ };
24
+
25
+ // ── Detection ─────────────────────────────────────────────────────────
26
+
27
+ /**
28
+ * Check if a Chromium-based browser is installed.
29
+ */
30
+ export function checkBrowser(): DepStatus {
31
+ const platform = detectPlatform();
32
+
33
+ const candidates: BrowserExecutable[] =
34
+ platform === 'macos' ? MACOS_BROWSER_PATHS : LINUX_BROWSER_PATHS;
35
+
36
+ for (const c of candidates) {
37
+ if (existsSync(c.path)) {
38
+ return {
39
+ name: 'Browser (Chrome/Chromium)',
40
+ found: true,
41
+ path: c.path,
42
+ message: `${c.kind} at ${c.path}`,
43
+ installable: false,
44
+ };
45
+ }
46
+ }
47
+
48
+ // On WSL, also check Windows-side browsers
49
+ if (platform === 'wsl') {
50
+ const wslPaths = [
51
+ '/mnt/c/Program Files/Google/Chrome/Application/chrome.exe',
52
+ '/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe',
53
+ '/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe',
54
+ '/mnt/c/Program Files/Microsoft/Edge/Application/msedge.exe',
55
+ ];
56
+ for (const p of wslPaths) {
57
+ if (existsSync(p)) {
58
+ return {
59
+ name: 'Browser (Chrome/Chromium)',
60
+ found: true,
61
+ path: p,
62
+ message: `Windows browser at ${p}`,
63
+ installable: false,
64
+ };
65
+ }
66
+ }
67
+ }
68
+
69
+ return {
70
+ name: 'Browser (Chrome/Chromium)',
71
+ found: false,
72
+ message: 'Not found',
73
+ installable: true,
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Check for Linux X11 tools needed for app control.
79
+ */
80
+ export function checkLinuxTools(): DepStatus[] {
81
+ const platform = detectPlatform();
82
+ if (platform === 'macos') return []; // Not needed on macOS
83
+
84
+ const tools = [
85
+ { name: 'xdotool', pkg: 'xdotool', desc: 'keyboard/mouse automation' },
86
+ { name: 'xprop', pkg: 'x11-utils', desc: 'window property inspection' },
87
+ { name: 'import (ImageMagick)', cmd: 'import', pkg: 'imagemagick', desc: 'screenshot capture' },
88
+ ];
89
+
90
+ return tools.map(tool => {
91
+ const cmd = tool.cmd ?? tool.name;
92
+ const result = spawnSync(['which', cmd], { stdout: 'pipe', stderr: 'pipe' });
93
+ const found = result.exitCode === 0;
94
+ const path = found ? result.stdout.toString().trim() : undefined;
95
+
96
+ return {
97
+ name: tool.name,
98
+ found,
99
+ path,
100
+ message: found ? path! : `Not installed (${tool.desc})`,
101
+ installable: true,
102
+ };
103
+ });
104
+ }
105
+
106
+ /**
107
+ * Check if Google OAuth tokens exist.
108
+ */
109
+ export function checkGoogleAuth(): DepStatus {
110
+ const tokensPath = join(homedir(), '.jarvis', 'google-tokens.json');
111
+ const found = existsSync(tokensPath);
112
+
113
+ return {
114
+ name: 'Google OAuth',
115
+ found,
116
+ path: found ? tokensPath : undefined,
117
+ message: found ? 'Tokens exist' : 'Not configured (optional, for Gmail/Calendar)',
118
+ installable: true,
119
+ };
120
+ }
121
+
122
+ // ── Installation ──────────────────────────────────────────────────────
123
+
124
+ /**
125
+ * Detect the system package manager.
126
+ */
127
+ function detectPackageManager(): 'apt' | 'dnf' | 'pacman' | 'brew' | null {
128
+ if (spawnSync(['which', 'apt'], { stdout: 'pipe' }).exitCode === 0) return 'apt';
129
+ if (spawnSync(['which', 'dnf'], { stdout: 'pipe' }).exitCode === 0) return 'dnf';
130
+ if (spawnSync(['which', 'pacman'], { stdout: 'pipe' }).exitCode === 0) return 'pacman';
131
+ if (spawnSync(['which', 'brew'], { stdout: 'pipe' }).exitCode === 0) return 'brew';
132
+ return null;
133
+ }
134
+
135
+ /**
136
+ * Install a Chromium-based browser.
137
+ */
138
+ export async function installBrowser(): Promise<boolean> {
139
+ const platform = detectPlatform();
140
+ const pm = detectPackageManager();
141
+
142
+ if (platform === 'macos') {
143
+ if (pm === 'brew') {
144
+ console.log(c.dim(' Running: brew install --cask google-chrome'));
145
+ const result = spawnSync(['brew', 'install', '--cask', 'google-chrome'], {
146
+ stdout: 'inherit', stderr: 'inherit',
147
+ });
148
+ return result.exitCode === 0;
149
+ }
150
+ printInfo('Install Chrome from: https://www.google.com/chrome/');
151
+ return false;
152
+ }
153
+
154
+ // Linux / WSL — install a Linux browser (preferred for CDP)
155
+ if (pm === 'apt') {
156
+ // Try chromium-browser first (most common on Ubuntu/Debian), then chromium
157
+ console.log(c.dim(' Running: sudo apt install -y chromium-browser'));
158
+ let result = spawnSync(['sudo', 'apt', 'install', '-y', 'chromium-browser'], {
159
+ stdout: 'inherit', stderr: 'inherit',
160
+ });
161
+ if (result.exitCode === 0) return true;
162
+
163
+ console.log(c.dim(' Trying: sudo apt install -y chromium'));
164
+ result = spawnSync(['sudo', 'apt', 'install', '-y', 'chromium'], {
165
+ stdout: 'inherit', stderr: 'inherit',
166
+ });
167
+ return result.exitCode === 0;
168
+ }
169
+
170
+ if (pm === 'dnf') {
171
+ console.log(c.dim(' Running: sudo dnf install -y chromium'));
172
+ const result = spawnSync(['sudo', 'dnf', 'install', '-y', 'chromium'], {
173
+ stdout: 'inherit', stderr: 'inherit',
174
+ });
175
+ return result.exitCode === 0;
176
+ }
177
+
178
+ if (pm === 'pacman') {
179
+ console.log(c.dim(' Running: sudo pacman -S --noconfirm chromium'));
180
+ const result = spawnSync(['sudo', 'pacman', '-S', '--noconfirm', 'chromium'], {
181
+ stdout: 'inherit', stderr: 'inherit',
182
+ });
183
+ return result.exitCode === 0;
184
+ }
185
+
186
+ printInfo('Install Chromium manually for your distribution.');
187
+ return false;
188
+ }
189
+
190
+ /**
191
+ * Install missing Linux X11 tools.
192
+ */
193
+ export async function installLinuxTools(missing: string[]): Promise<boolean> {
194
+ const pm = detectPackageManager();
195
+
196
+ // Map tool names to package names
197
+ const pkgMap: Record<string, Record<string, string>> = {
198
+ apt: { xdotool: 'xdotool', xprop: 'x11-utils', 'import (ImageMagick)': 'imagemagick' },
199
+ dnf: { xdotool: 'xdotool', xprop: 'xprop', 'import (ImageMagick)': 'ImageMagick' },
200
+ pacman: { xdotool: 'xdotool', xprop: 'xorg-xprop', 'import (ImageMagick)': 'imagemagick' },
201
+ };
202
+
203
+ if (!pm || pm === 'brew') {
204
+ printInfo('Install manually: ' + missing.join(', '));
205
+ return false;
206
+ }
207
+
208
+ const packages = missing
209
+ .map(name => pkgMap[pm]?.[name])
210
+ .filter(Boolean) as string[];
211
+
212
+ if (packages.length === 0) return true;
213
+
214
+ const unique = [...new Set(packages)];
215
+
216
+ if (pm === 'apt') {
217
+ console.log(c.dim(` Running: sudo apt install -y ${unique.join(' ')}`));
218
+ const result = spawnSync(['sudo', 'apt', 'install', '-y', ...unique], {
219
+ stdout: 'inherit', stderr: 'inherit',
220
+ });
221
+ return result.exitCode === 0;
222
+ }
223
+
224
+ if (pm === 'dnf') {
225
+ console.log(c.dim(` Running: sudo dnf install -y ${unique.join(' ')}`));
226
+ const result = spawnSync(['sudo', 'dnf', 'install', '-y', ...unique], {
227
+ stdout: 'inherit', stderr: 'inherit',
228
+ });
229
+ return result.exitCode === 0;
230
+ }
231
+
232
+ if (pm === 'pacman') {
233
+ console.log(c.dim(` Running: sudo pacman -S --noconfirm ${unique.join(' ')}`));
234
+ const result = spawnSync(['sudo', 'pacman', '-S', '--noconfirm', ...unique], {
235
+ stdout: 'inherit', stderr: 'inherit',
236
+ });
237
+ return result.exitCode === 0;
238
+ }
239
+
240
+ return false;
241
+ }
242
+
243
+ /**
244
+ * Run inline Google OAuth setup flow.
245
+ */
246
+ export async function setupGoogleOAuth(config: any): Promise<boolean> {
247
+ let clientId = config.google?.client_id ?? '';
248
+ let clientSecret = config.google?.client_secret ?? '';
249
+
250
+ if (!clientId || !clientSecret) {
251
+ printInfo('Google OAuth requires OAuth2 credentials from Google Cloud Console.');
252
+ printInfo('1. Go to https://console.cloud.google.com/apis/credentials');
253
+ printInfo('2. Create an OAuth2 client ID (Web application)');
254
+ printInfo('3. Add redirect URI: http://localhost:3142/api/auth/google/callback');
255
+ console.log('');
256
+
257
+ clientId = await ask('Google OAuth Client ID (or press Enter to skip)');
258
+ if (!clientId) return false;
259
+
260
+ clientSecret = await askSecret('Google OAuth Client Secret');
261
+ if (!clientSecret) return false;
262
+
263
+ // Save credentials to config
264
+ config.google = { ...config.google, client_id: clientId, client_secret: clientSecret };
265
+ }
266
+
267
+ // Import GoogleAuth and start inline flow
268
+ const { GoogleAuth } = await import('../integrations/google-auth.ts');
269
+ const auth = new GoogleAuth(clientId, clientSecret);
270
+
271
+ if (auth.isAuthenticated()) {
272
+ printOk('Already authenticated with Google!');
273
+ return true;
274
+ }
275
+
276
+ const SCOPES = [
277
+ 'https://www.googleapis.com/auth/gmail.readonly',
278
+ 'https://www.googleapis.com/auth/calendar.readonly',
279
+ ];
280
+
281
+ const authUrl = auth.getAuthUrl(SCOPES);
282
+
283
+ console.log('');
284
+ printInfo('Opening browser for Google authorization...');
285
+ console.log(c.dim(` ${authUrl}`));
286
+ console.log('');
287
+
288
+ // Try to open browser
289
+ try {
290
+ const platform = detectPlatform();
291
+ if (platform === 'macos') {
292
+ spawnSync(['open', authUrl], { stdout: 'ignore', stderr: 'ignore' });
293
+ } else if (platform === 'wsl') {
294
+ spawnSync(['wslview', authUrl], { stdout: 'ignore', stderr: 'ignore' });
295
+ } else {
296
+ spawnSync(['xdg-open', authUrl], { stdout: 'ignore', stderr: 'ignore' });
297
+ }
298
+ } catch {
299
+ // User can open manually
300
+ }
301
+
302
+ // Start temporary callback server for OAuth redirect
303
+ printInfo('Waiting for authorization callback on port 3142...');
304
+
305
+ return new Promise<boolean>((resolve) => {
306
+ let server: ReturnType<typeof Bun.serve>;
307
+ try {
308
+ server = Bun.serve({
309
+ port: 3142,
310
+ async fetch(req) {
311
+ const url = new URL(req.url);
312
+
313
+ if (url.pathname === '/api/auth/google/callback') {
314
+ const code = url.searchParams.get('code');
315
+ const error = url.searchParams.get('error');
316
+
317
+ if (error) {
318
+ clearTimeout(timeout);
319
+ printErr(`Authorization denied: ${error}`);
320
+ setTimeout(() => { server.stop(); resolve(false); }, 300);
321
+ return new Response(
322
+ '<html><body><h1>Authorization Denied</h1><p>You can close this tab.</p></body></html>',
323
+ { headers: { 'Content-Type': 'text/html' } }
324
+ );
325
+ }
326
+
327
+ if (!code) {
328
+ return new Response('Missing code', { status: 400 });
329
+ }
330
+
331
+ try {
332
+ await auth.exchangeCode(code);
333
+ clearTimeout(timeout);
334
+ printOk('Google OAuth configured! Tokens saved.');
335
+ setTimeout(() => { server.stop(); resolve(true); }, 300);
336
+ return new Response(
337
+ '<html><body><h1>JARVIS Google Authorization Complete!</h1><p>You can close this tab.</p></body></html>',
338
+ { headers: { 'Content-Type': 'text/html' } }
339
+ );
340
+ } catch (err) {
341
+ clearTimeout(timeout);
342
+ printErr(`Token exchange failed: ${err}`);
343
+ setTimeout(() => { server.stop(); resolve(false); }, 300);
344
+ return new Response(
345
+ `<html><body><h1>Token Exchange Failed</h1><pre>${err}</pre></body></html>`,
346
+ { headers: { 'Content-Type': 'text/html' }, status: 500 }
347
+ );
348
+ }
349
+ }
350
+
351
+ return new Response('Not found', { status: 404 });
352
+ },
353
+ });
354
+ } catch (err) {
355
+ printErr(`Could not start OAuth callback server on port 3142 (port in use?)`);
356
+ printInfo('Stop the JARVIS daemon first, or run later with: bun run setup:google');
357
+ resolve(false);
358
+ return;
359
+ }
360
+
361
+ const timeout = setTimeout(() => {
362
+ server.stop();
363
+ printWarn('Timeout waiting for Google authorization (60s).');
364
+ printInfo('Run later with: bun run setup:google');
365
+ resolve(false);
366
+ }, 60_000);
367
+ });
368
+ }
369
+
370
+ // ── Main Step Runner ──────────────────────────────────────────────────
371
+
372
+ /**
373
+ * Run the full dependency check + install flow.
374
+ * Called as Step 3 of the onboard wizard.
375
+ */
376
+ export async function runDependencyCheck(config: any): Promise<void> {
377
+ // Collect all dependency statuses
378
+ const deps: DepStatus[] = [];
379
+
380
+ deps.push(checkBrowser());
381
+
382
+ const linuxTools = checkLinuxTools();
383
+ deps.push(...linuxTools);
384
+
385
+ deps.push(checkGoogleAuth());
386
+
387
+ // Display status table
388
+ console.log('');
389
+ for (const dep of deps) {
390
+ const icon = dep.found ? c.green('\u2713') : c.red('\u2717');
391
+ const name = dep.name.padEnd(26);
392
+ const detail = dep.found
393
+ ? c.dim(dep.path ?? dep.message)
394
+ : c.yellow(dep.message);
395
+ console.log(` ${icon} ${name} ${detail}`);
396
+ }
397
+ console.log('');
398
+
399
+ const missing = deps.filter(d => !d.found && d.installable);
400
+
401
+ if (missing.length === 0) {
402
+ printOk('All system dependencies are satisfied!');
403
+ return;
404
+ }
405
+
406
+ printInfo(`${missing.length} optional ${missing.length === 1 ? 'dependency' : 'dependencies'} not found.`);
407
+ console.log('');
408
+
409
+ // Offer to install each missing dependency
410
+ // Group: browser
411
+ const missingBrowser = missing.find(d => d.name.includes('Browser'));
412
+ if (missingBrowser) {
413
+ const install = await askYesNo('Install a Chromium-based browser?', true);
414
+ if (install) {
415
+ const ok = await installBrowser();
416
+ if (ok) printOk('Browser installed!');
417
+ else printWarn('Browser install incomplete. Install manually later.');
418
+ } else {
419
+ printInfo('Skip. Install later: sudo apt install chromium-browser');
420
+ }
421
+ }
422
+
423
+ // Group: Linux tools (batch install)
424
+ const missingLinux = missing.filter(d =>
425
+ d.name === 'xdotool' || d.name === 'xprop' || d.name === 'import (ImageMagick)'
426
+ );
427
+ if (missingLinux.length > 0) {
428
+ const names = missingLinux.map(d => d.name).join(', ');
429
+ const install = await askYesNo(`Install Linux tools (${names})?`, true);
430
+ if (install) {
431
+ const ok = await installLinuxTools(missingLinux.map(d => d.name));
432
+ if (ok) printOk('Linux tools installed!');
433
+ else printWarn('Some tools may not have installed. Check manually.');
434
+ } else {
435
+ printInfo('Skip. Install later: sudo apt install xdotool x11-utils imagemagick');
436
+ }
437
+ }
438
+
439
+ // Group: Google OAuth (special — only if user has google config or wants to set up)
440
+ const missingGoogle = missing.find(d => d.name === 'Google OAuth');
441
+ if (missingGoogle) {
442
+ const install = await askYesNo('Set up Google OAuth for Gmail/Calendar? (optional)', false);
443
+ if (install) {
444
+ await setupGoogleOAuth(config);
445
+ } else {
446
+ printInfo('Skip. Set up later with: bun run setup:google');
447
+ }
448
+ }
449
+ }