@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,273 @@
1
+ import type { AppController, WindowInfo, UIElement } from './interface.ts';
2
+ import { $ } from 'bun';
3
+
4
+ export class LinuxAppController implements AppController {
5
+ private async checkTool(tool: string): Promise<boolean> {
6
+ try {
7
+ await $`which ${tool}`.quiet();
8
+ return true;
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+
14
+ private async ensureTool(tool: string): Promise<void> {
15
+ if (!(await this.checkTool(tool))) {
16
+ throw new Error(
17
+ `Required tool '${tool}' not found. Please install it:\n` +
18
+ ` Ubuntu/Debian: sudo apt install ${tool}\n` +
19
+ ` Fedora: sudo dnf install ${tool}\n` +
20
+ ` Arch: sudo pacman -S ${tool}`
21
+ );
22
+ }
23
+ }
24
+
25
+ async getActiveWindow(): Promise<WindowInfo> {
26
+ await this.ensureTool('xdotool');
27
+ await this.ensureTool('xprop');
28
+
29
+ try {
30
+ const windowId = (await $`xdotool getactivewindow`.text()).trim();
31
+
32
+ const xpropOutput = await $`xprop -id ${windowId}`.text();
33
+
34
+ const title = this.extractXpropValue(xpropOutput, 'WM_NAME') || 'Unknown';
35
+ const className = this.extractXpropValue(xpropOutput, 'WM_CLASS') || 'Unknown';
36
+
37
+ const geometryOutput = await $`xdotool getwindowgeometry ${windowId}`.text();
38
+ const bounds = this.parseGeometry(geometryOutput);
39
+
40
+ const pid = parseInt(this.extractXpropValue(xpropOutput, '_NET_WM_PID') || '0', 10);
41
+
42
+ return {
43
+ pid,
44
+ title,
45
+ className,
46
+ bounds,
47
+ focused: true,
48
+ };
49
+ } catch (error) {
50
+ throw new Error(`Failed to get active window: ${error instanceof Error ? error.message : String(error)}`);
51
+ }
52
+ }
53
+
54
+ async getWindowTree(pid: number): Promise<UIElement[]> {
55
+ // TODO: Implement using AT-SPI2 (Assistive Technology Service Provider Interface)
56
+ // This requires complex bindings to the AT-SPI D-Bus interface
57
+ // For now, return empty array with informative error
58
+ console.warn(
59
+ `getWindowTree not yet implemented for Linux.\n` +
60
+ `Requires AT-SPI2 integration via D-Bus. Consider using:\n` +
61
+ ` - python-atspi library\n` +
62
+ ` - Accerciser tool for exploration\n` +
63
+ ` - Direct D-Bus bindings`
64
+ );
65
+ return [];
66
+ }
67
+
68
+ async listWindows(): Promise<WindowInfo[]> {
69
+ await this.ensureTool('xdotool');
70
+ await this.ensureTool('xprop');
71
+
72
+ try {
73
+ const hasWmctrl = await this.checkTool('wmctrl');
74
+
75
+ let windowIds: string[];
76
+
77
+ if (hasWmctrl) {
78
+ const wmctrlOutput = await $`wmctrl -l -p`.text();
79
+ windowIds = wmctrlOutput
80
+ .split('\n')
81
+ .filter(line => line.trim())
82
+ .map(line => line.split(/\s+/)[0] || '');
83
+ } else {
84
+ const xdotoolOutput = await $`xdotool search --name "."`.text();
85
+ windowIds = xdotoolOutput.split('\n').filter(id => id.trim());
86
+ }
87
+
88
+ const windows: WindowInfo[] = [];
89
+ const activeWindowId = (await $`xdotool getactivewindow`.text()).trim();
90
+
91
+ for (const windowId of windowIds) {
92
+ if (!windowId) continue;
93
+
94
+ try {
95
+ const xpropOutput = await $`xprop -id ${windowId}`.text();
96
+ const title = this.extractXpropValue(xpropOutput, 'WM_NAME') || 'Unknown';
97
+ const className = this.extractXpropValue(xpropOutput, 'WM_CLASS') || 'Unknown';
98
+ const pid = parseInt(this.extractXpropValue(xpropOutput, '_NET_WM_PID') || '0', 10);
99
+
100
+ const geometryOutput = await $`xdotool getwindowgeometry ${windowId}`.text();
101
+ const bounds = this.parseGeometry(geometryOutput);
102
+
103
+ windows.push({
104
+ pid,
105
+ title,
106
+ className,
107
+ bounds,
108
+ focused: windowId === activeWindowId,
109
+ });
110
+ } catch {
111
+ // Skip windows that can't be queried
112
+ continue;
113
+ }
114
+ }
115
+
116
+ return windows;
117
+ } catch (error) {
118
+ throw new Error(`Failed to list windows: ${error instanceof Error ? error.message : String(error)}`);
119
+ }
120
+ }
121
+
122
+ async clickElement(element: UIElement): Promise<void> {
123
+ await this.ensureTool('xdotool');
124
+
125
+ try {
126
+ const centerX = element.bounds.x + element.bounds.width / 2;
127
+ const centerY = element.bounds.y + element.bounds.height / 2;
128
+
129
+ await $`xdotool mousemove ${Math.round(centerX)} ${Math.round(centerY)}`;
130
+ await $`xdotool click 1`;
131
+ } catch (error) {
132
+ throw new Error(`Failed to click element: ${error instanceof Error ? error.message : String(error)}`);
133
+ }
134
+ }
135
+
136
+ async typeText(text: string): Promise<void> {
137
+ await this.ensureTool('xdotool');
138
+
139
+ try {
140
+ await $`xdotool type --clearmodifiers ${text}`;
141
+ } catch (error) {
142
+ throw new Error(`Failed to type text: ${error instanceof Error ? error.message : String(error)}`);
143
+ }
144
+ }
145
+
146
+ async pressKeys(keys: string[]): Promise<void> {
147
+ await this.ensureTool('xdotool');
148
+
149
+ try {
150
+ const keyString = keys.join('+');
151
+ await $`xdotool key --clearmodifiers ${keyString}`;
152
+ } catch (error) {
153
+ throw new Error(`Failed to press keys: ${error instanceof Error ? error.message : String(error)}`);
154
+ }
155
+ }
156
+
157
+ async captureScreen(): Promise<Buffer> {
158
+ const hasImport = await this.checkTool('import');
159
+ const hasScrot = await this.checkTool('scrot');
160
+
161
+ if (!hasImport && !hasScrot) {
162
+ throw new Error(
163
+ `No screenshot tool found. Please install one:\n` +
164
+ ` ImageMagick: sudo apt install imagemagick\n` +
165
+ ` Scrot: sudo apt install scrot`
166
+ );
167
+ }
168
+
169
+ try {
170
+ const tmpFile = `/tmp/jarvis-screen-${Date.now()}.png`;
171
+
172
+ if (hasImport) {
173
+ await $`import -window root ${tmpFile}`;
174
+ } else {
175
+ await $`scrot ${tmpFile}`;
176
+ }
177
+
178
+ const buffer = await Bun.file(tmpFile).arrayBuffer();
179
+ await $`rm ${tmpFile}`;
180
+
181
+ return Buffer.from(buffer);
182
+ } catch (error) {
183
+ throw new Error(`Failed to capture screen: ${error instanceof Error ? error.message : String(error)}`);
184
+ }
185
+ }
186
+
187
+ async captureWindow(pid: number): Promise<Buffer> {
188
+ await this.ensureTool('xdotool');
189
+ const hasImport = await this.checkTool('import');
190
+
191
+ if (!hasImport) {
192
+ throw new Error(
193
+ `ImageMagick not found. Please install:\n` +
194
+ ` sudo apt install imagemagick`
195
+ );
196
+ }
197
+
198
+ try {
199
+ const windowId = await this.findWindowByPid(pid);
200
+
201
+ const tmpFile = `/tmp/jarvis-window-${pid}-${Date.now()}.png`;
202
+ await $`import -window ${windowId} ${tmpFile}`;
203
+
204
+ const buffer = await Bun.file(tmpFile).arrayBuffer();
205
+ await $`rm ${tmpFile}`;
206
+
207
+ return Buffer.from(buffer);
208
+ } catch (error) {
209
+ throw new Error(`Failed to capture window: ${error instanceof Error ? error.message : String(error)}`);
210
+ }
211
+ }
212
+
213
+ async focusWindow(pid: number): Promise<void> {
214
+ await this.ensureTool('xdotool');
215
+
216
+ try {
217
+ const windowId = await this.findWindowByPid(pid);
218
+ await $`xdotool windowactivate ${windowId}`;
219
+ } catch (error) {
220
+ throw new Error(`Failed to focus window: ${error instanceof Error ? error.message : String(error)}`);
221
+ }
222
+ }
223
+
224
+ private async findWindowByPid(pid: number): Promise<string> {
225
+ const windowIds = (await $`xdotool search --name "."`.text())
226
+ .split('\n')
227
+ .filter(id => id.trim());
228
+
229
+ for (const windowId of windowIds) {
230
+ try {
231
+ const xpropOutput = await $`xprop -id ${windowId}`.text();
232
+ const windowPid = parseInt(this.extractXpropValue(xpropOutput, '_NET_WM_PID') || '0', 10);
233
+
234
+ if (windowPid === pid) {
235
+ return windowId;
236
+ }
237
+ } catch {
238
+ continue;
239
+ }
240
+ }
241
+
242
+ throw new Error(`No window found for PID ${pid}`);
243
+ }
244
+
245
+ private extractXpropValue(output: string, property: string): string | null {
246
+ const regex = new RegExp(`^${property}\\(.*?\\)\\s*=\\s*(.+)$`, 'm');
247
+ const match = output.match(regex);
248
+
249
+ if (!match || !match[1]) {
250
+ return null;
251
+ }
252
+
253
+ let value = match[1].trim();
254
+
255
+ value = value.replace(/^"(.*)"$/, '$1');
256
+ value = value.replace(/^{([^}]*)}.*$/, '$1');
257
+ value = value.replace(/^"([^"]*)".*$/, '$1');
258
+
259
+ return value;
260
+ }
261
+
262
+ private parseGeometry(geometryOutput: string): { x: number; y: number; width: number; height: number } {
263
+ const positionMatch = geometryOutput.match(/Position:\s*(\d+),(\d+)/);
264
+ const geometryMatch = geometryOutput.match(/Geometry:\s*(\d+)x(\d+)/);
265
+
266
+ const x = positionMatch?.[1] ? parseInt(positionMatch[1], 10) : 0;
267
+ const y = positionMatch?.[2] ? parseInt(positionMatch[2], 10) : 0;
268
+ const width = geometryMatch?.[1] ? parseInt(geometryMatch[1], 10) : 0;
269
+ const height = geometryMatch?.[2] ? parseInt(geometryMatch[2], 10) : 0;
270
+
271
+ return { x, y, width, height };
272
+ }
273
+ }
@@ -0,0 +1,54 @@
1
+ import type { AppController, WindowInfo, UIElement } from './interface.ts';
2
+
3
+ export class MacAppController implements AppController {
4
+ private notImplemented(method: string): never {
5
+ throw new Error(
6
+ `${method} not yet implemented for macOS.\n\n` +
7
+ `TODO: Implement using one of:\n` +
8
+ ` - AXUIElement API via N-API native bindings\n` +
9
+ ` - AppleScript automation\n` +
10
+ ` - node-mac-automation package\n` +
11
+ ` - Swift/Objective-C bridge via FFI\n\n` +
12
+ `Reference:\n` +
13
+ ` - https://developer.apple.com/documentation/applicationservices/axuielement\n` +
14
+ ` - https://developer.apple.com/library/archive/documentation/Accessibility/Conceptual/AccessibilityMacOSX/\n` +
15
+ ` - https://github.com/sveinbjornt/AXElementsTester`
16
+ );
17
+ }
18
+
19
+ async getActiveWindow(): Promise<WindowInfo> {
20
+ this.notImplemented('getActiveWindow');
21
+ }
22
+
23
+ async getWindowTree(pid: number): Promise<UIElement[]> {
24
+ this.notImplemented('getWindowTree');
25
+ }
26
+
27
+ async listWindows(): Promise<WindowInfo[]> {
28
+ this.notImplemented('listWindows');
29
+ }
30
+
31
+ async clickElement(element: UIElement): Promise<void> {
32
+ this.notImplemented('clickElement');
33
+ }
34
+
35
+ async typeText(text: string): Promise<void> {
36
+ this.notImplemented('typeText');
37
+ }
38
+
39
+ async pressKeys(keys: string[]): Promise<void> {
40
+ this.notImplemented('pressKeys');
41
+ }
42
+
43
+ async captureScreen(): Promise<Buffer> {
44
+ this.notImplemented('captureScreen');
45
+ }
46
+
47
+ async captureWindow(pid: number): Promise<Buffer> {
48
+ this.notImplemented('captureWindow');
49
+ }
50
+
51
+ async focusWindow(pid: number): Promise<void> {
52
+ this.notImplemented('focusWindow');
53
+ }
54
+ }
@@ -0,0 +1,23 @@
1
+ import { test, expect, describe } from 'bun:test';
2
+ import { findSidecarExecutable, getWSLHostIP } from './sidecar-launcher.ts';
3
+
4
+ describe('sidecar-launcher', () => {
5
+ test('findSidecarExecutable returns string or null', () => {
6
+ const result = findSidecarExecutable();
7
+ expect(result === null || typeof result === 'string').toBe(true);
8
+ });
9
+
10
+ test('getWSLHostIP returns a string', () => {
11
+ const ip = getWSLHostIP();
12
+ expect(typeof ip).toBe('string');
13
+ expect(ip.length).toBeGreaterThan(0);
14
+ });
15
+
16
+ test('getWSLHostIP returns localhost or valid IP', () => {
17
+ const ip = getWSLHostIP();
18
+ // Either "localhost" or an IPv4 address
19
+ const isLocalhost = ip === 'localhost';
20
+ const isIP = /^\d+\.\d+\.\d+\.\d+$/.test(ip);
21
+ expect(isLocalhost || isIP).toBe(true);
22
+ });
23
+ });
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Sidecar Launcher — Find, Launch, and Manage the Desktop Bridge
3
+ *
4
+ * Auto-detects the desktop-bridge.exe sidecar, launches it from WSL,
5
+ * and polls TCP to confirm it's ready. Mirrors the pattern in
6
+ * src/actions/browser/chrome-launcher.ts.
7
+ */
8
+
9
+ import { spawn, type Subprocess } from 'bun';
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+ import { homedir } from 'node:os';
12
+ import { join } from 'node:path';
13
+ import { createConnection } from 'node:net';
14
+ import { WSLBridge } from '../terminal/wsl-bridge.ts';
15
+
16
+ export type RunningSidecar = {
17
+ proc: Subprocess | null; // null if externally managed
18
+ port: number;
19
+ host: string;
20
+ startedAt: number;
21
+ exePath: string;
22
+ };
23
+
24
+ const DEFAULT_PORT = 9224;
25
+
26
+ /**
27
+ * Find the desktop-bridge.exe sidecar on disk.
28
+ * Checks: ~/.jarvis/sidecar/ then repo build output.
29
+ */
30
+ export function findSidecarExecutable(): string | null {
31
+ const candidates: string[] = [];
32
+
33
+ if (WSLBridge.isWSL()) {
34
+ // WSL: check the Windows-side paths via /mnt/c/
35
+ // First try USERPROFILE-based path (most common)
36
+ try {
37
+ const userProfileResult = Bun.spawnSync(['cmd.exe', '/C', 'echo', '%USERPROFILE%']);
38
+ const userProfile = userProfileResult.stdout.toString().trim();
39
+ if (userProfile && !userProfile.includes('%')) {
40
+ const drive = userProfile.charAt(0).toLowerCase();
41
+ const rest = userProfile.slice(2).replace(/\\/g, '/');
42
+ const wslPath = `/mnt/${drive}${rest}/.jarvis/sidecar/desktop-bridge.exe`;
43
+ candidates.push(wslPath);
44
+ }
45
+ } catch {
46
+ // Fall back to common paths
47
+ }
48
+
49
+ candidates.push('/mnt/c/Users/' + (process.env.USER ?? 'user') + '/.jarvis/sidecar/desktop-bridge.exe');
50
+ } else {
51
+ // Native Windows
52
+ candidates.push(join(homedir(), '.jarvis', 'sidecar', 'desktop-bridge.exe'));
53
+ }
54
+
55
+ // Also check repo build output (both custom -o path and default dotnet publish path)
56
+ const repoBase = join(import.meta.dir, '../../../sidecar/desktop-bridge');
57
+ candidates.push(join(repoBase, 'bin', 'publish', 'desktop-bridge.exe'));
58
+ candidates.push(join(repoBase, 'bin', 'Release', 'net10.0-windows', 'win-x64', 'publish', 'desktop-bridge.exe'));
59
+ candidates.push(join(repoBase, 'bin', 'Release', 'net9.0-windows', 'win-x64', 'publish', 'desktop-bridge.exe'));
60
+ candidates.push(join(repoBase, 'bin', 'Release', 'net8.0-windows', 'win-x64', 'publish', 'desktop-bridge.exe'));
61
+
62
+ for (const path of candidates) {
63
+ if (existsSync(path)) {
64
+ return path;
65
+ }
66
+ }
67
+
68
+ return null;
69
+ }
70
+
71
+ /**
72
+ * Get the Windows host IP when WSL2 doesn't have mirrored networking.
73
+ * Parses /etc/resolv.conf for the nameserver entry.
74
+ */
75
+ export function getWSLHostIP(): string {
76
+ try {
77
+ const resolv = readFileSync('/etc/resolv.conf', 'utf-8');
78
+ const match = resolv.match(/nameserver\s+(\d+\.\d+\.\d+\.\d+)/);
79
+ if (match) {
80
+ return match[1]!;
81
+ }
82
+ } catch {
83
+ // ignore
84
+ }
85
+ return 'localhost';
86
+ }
87
+
88
+ /**
89
+ * Determine the host to connect to the sidecar.
90
+ * With WSL2 mirrored networking, localhost works.
91
+ * Without it, we use the Windows host IP.
92
+ */
93
+ function getSidecarHost(): string {
94
+ if (!WSLBridge.isWSL()) {
95
+ return 'localhost';
96
+ }
97
+
98
+ // Check if mirrored networking is active (localhost works)
99
+ // WSL2 mirrored mode sets networkingMode in .wslconfig
100
+ // Simplest check: if localhost resolves to Windows, it's mirrored
101
+ // For now, try localhost first; the connect logic handles fallback
102
+ return 'localhost';
103
+ }
104
+
105
+ /**
106
+ * Check if the sidecar is already running on the given port.
107
+ */
108
+ export async function isSidecarRunning(port: number = DEFAULT_PORT): Promise<boolean> {
109
+ const host = getSidecarHost();
110
+ const hosts = [host];
111
+
112
+ // If on WSL and host is localhost, also try the WSL host IP as fallback
113
+ if (WSLBridge.isWSL() && host === 'localhost') {
114
+ const hostIP = getWSLHostIP();
115
+ if (hostIP !== 'localhost') {
116
+ hosts.push(hostIP);
117
+ }
118
+ }
119
+
120
+ for (const h of hosts) {
121
+ try {
122
+ const alive = await pingTcp(h, port, 2000);
123
+ if (alive) return true;
124
+ } catch {
125
+ // Try next host
126
+ }
127
+ }
128
+
129
+ return false;
130
+ }
131
+
132
+ /**
133
+ * Launch the desktop-bridge sidecar.
134
+ * Auto-detects the executable and spawns it.
135
+ */
136
+ export async function launchSidecar(port: number = DEFAULT_PORT): Promise<RunningSidecar> {
137
+ const exePath = findSidecarExecutable();
138
+ if (!exePath) {
139
+ throw new Error(
140
+ 'Desktop bridge sidecar not found.\n' +
141
+ 'Build it with: bun run scripts/build-sidecar.ts\n' +
142
+ 'Requires .NET 8 SDK on Windows.'
143
+ );
144
+ }
145
+
146
+ console.log(`[SidecarLauncher] Launching: ${exePath}`);
147
+
148
+ // Spawn the sidecar
149
+ const proc = spawn([exePath, '--port', String(port)], {
150
+ stdout: 'ignore',
151
+ stderr: 'ignore',
152
+ });
153
+
154
+ const startedAt = Date.now();
155
+ const host = getSidecarHost();
156
+
157
+ // Poll TCP for up to 10s
158
+ const deadline = Date.now() + 10_000;
159
+ let reachable = false;
160
+ let connectedHost = host;
161
+
162
+ while (Date.now() < deadline) {
163
+ try {
164
+ if (await pingTcp(host, port, 1000)) {
165
+ reachable = true;
166
+ connectedHost = host;
167
+ break;
168
+ }
169
+ } catch {
170
+ // Not ready yet
171
+ }
172
+
173
+ // Also try WSL host IP if localhost fails
174
+ if (WSLBridge.isWSL() && host === 'localhost') {
175
+ const hostIP = getWSLHostIP();
176
+ if (hostIP !== 'localhost') {
177
+ try {
178
+ if (await pingTcp(hostIP, port, 1000)) {
179
+ reachable = true;
180
+ connectedHost = hostIP;
181
+ break;
182
+ }
183
+ } catch {
184
+ // Not ready
185
+ }
186
+ }
187
+ }
188
+
189
+ await Bun.sleep(300);
190
+ }
191
+
192
+ if (!reachable) {
193
+ try { proc.kill(); } catch {}
194
+ throw new Error(
195
+ `Sidecar started but not reachable on port ${port} after 10s.\n` +
196
+ `Binary: ${exePath}`
197
+ );
198
+ }
199
+
200
+ console.log(`[SidecarLauncher] Sidecar ready on ${connectedHost}:${port} (pid ${proc.pid})`);
201
+
202
+ return { proc, port, host: connectedHost, startedAt, exePath };
203
+ }
204
+
205
+ /**
206
+ * Stop a running sidecar. Sends shutdown command, then kills process.
207
+ */
208
+ export async function stopSidecar(running: RunningSidecar): Promise<void> {
209
+ // Try graceful shutdown via JSON-RPC
210
+ try {
211
+ const shutdown = JSON.stringify({ jsonrpc: '2.0', method: 'shutdown', params: {}, id: 0 }) + '\n';
212
+ await sendTcpRaw(running.host, running.port, shutdown, 2000);
213
+ } catch {
214
+ // ignore
215
+ }
216
+
217
+ // Kill process if we spawned it
218
+ if (running.proc) {
219
+ try { running.proc.kill(); } catch {}
220
+
221
+ // Wait for exit
222
+ const deadline = Date.now() + 3000;
223
+ while (Date.now() < deadline) {
224
+ if (running.proc.exitCode !== null) break;
225
+ await Bun.sleep(100);
226
+ }
227
+
228
+ try { running.proc.kill(9); } catch {}
229
+ }
230
+
231
+ console.log('[SidecarLauncher] Sidecar stopped');
232
+ }
233
+
234
+ // --- TCP helpers ---
235
+
236
+ function pingTcp(host: string, port: number, timeoutMs: number): Promise<boolean> {
237
+ return new Promise((resolve) => {
238
+ const socket = createConnection({ host, port, timeout: timeoutMs }, () => {
239
+ // Send ping JSON-RPC
240
+ const ping = JSON.stringify({ jsonrpc: '2.0', method: 'ping', params: {}, id: -1 }) + '\n';
241
+ socket.write(ping);
242
+ });
243
+
244
+ let data = '';
245
+
246
+ socket.on('data', (chunk) => {
247
+ data += chunk.toString();
248
+ if (data.includes('pong')) {
249
+ socket.destroy();
250
+ resolve(true);
251
+ }
252
+ });
253
+
254
+ socket.on('error', () => {
255
+ socket.destroy();
256
+ resolve(false);
257
+ });
258
+
259
+ socket.on('timeout', () => {
260
+ socket.destroy();
261
+ resolve(false);
262
+ });
263
+
264
+ // Safety timeout
265
+ setTimeout(() => {
266
+ socket.destroy();
267
+ resolve(false);
268
+ }, timeoutMs + 500);
269
+ });
270
+ }
271
+
272
+ function sendTcpRaw(host: string, port: number, data: string, timeoutMs: number): Promise<void> {
273
+ return new Promise((resolve, reject) => {
274
+ const socket = createConnection({ host, port, timeout: timeoutMs }, () => {
275
+ socket.write(data, () => {
276
+ socket.destroy();
277
+ resolve();
278
+ });
279
+ });
280
+ socket.on('error', reject);
281
+ socket.on('timeout', () => {
282
+ socket.destroy();
283
+ reject(new Error('Timeout'));
284
+ });
285
+ });
286
+ }
@@ -0,0 +1,44 @@
1
+ import type { AppController, WindowInfo, UIElement } from './interface.ts';
2
+
3
+ /**
4
+ * Windows App Controller — stub.
5
+ * Previously delegated to the C# DesktopController sidecar.
6
+ * TODO: Implement local platform-native commands or route via Go sidecar.
7
+ */
8
+ export class WindowsAppController implements AppController {
9
+ async getActiveWindow(): Promise<WindowInfo> {
10
+ throw new Error('WindowsAppController: Not implemented. Use sidecar desktop tools with a target parameter.');
11
+ }
12
+
13
+ async getWindowTree(_pid: number): Promise<UIElement[]> {
14
+ throw new Error('WindowsAppController: Not implemented. Use sidecar desktop tools with a target parameter.');
15
+ }
16
+
17
+ async listWindows(): Promise<WindowInfo[]> {
18
+ throw new Error('WindowsAppController: Not implemented. Use sidecar desktop tools with a target parameter.');
19
+ }
20
+
21
+ async clickElement(_element: UIElement): Promise<void> {
22
+ throw new Error('WindowsAppController: Not implemented. Use sidecar desktop tools with a target parameter.');
23
+ }
24
+
25
+ async typeText(_text: string): Promise<void> {
26
+ throw new Error('WindowsAppController: Not implemented. Use sidecar desktop tools with a target parameter.');
27
+ }
28
+
29
+ async pressKeys(_keys: string[]): Promise<void> {
30
+ throw new Error('WindowsAppController: Not implemented. Use sidecar desktop tools with a target parameter.');
31
+ }
32
+
33
+ async captureScreen(): Promise<Buffer> {
34
+ throw new Error('WindowsAppController: Not implemented. Use sidecar desktop tools with a target parameter.');
35
+ }
36
+
37
+ async captureWindow(_pid: number): Promise<Buffer> {
38
+ throw new Error('WindowsAppController: Not implemented. Use sidecar desktop tools with a target parameter.');
39
+ }
40
+
41
+ async focusWindow(_pid: number): Promise<void> {
42
+ throw new Error('WindowsAppController: Not implemented. Use sidecar desktop tools with a target parameter.');
43
+ }
44
+ }