@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,438 @@
1
+ /**
2
+ * Desktop Controller — Windows Desktop Automation via Sidecar
3
+ *
4
+ * TCP JSON-RPC client that communicates with the desktop-bridge.exe sidecar.
5
+ * Mirrors the BrowserController pattern: lazy connection, auto-launch,
6
+ * snapshot with numbered element IDs, element cache for click/type by ID.
7
+ */
8
+
9
+ import { createConnection, type Socket } from 'node:net';
10
+ import { writeFileSync } from 'node:fs';
11
+ import type { AppController, WindowInfo, UIElement } from './interface.ts';
12
+ import { launchSidecar, stopSidecar, isSidecarRunning, type RunningSidecar } from './sidecar-launcher.ts';
13
+
14
+ export type DesktopSnapshot = {
15
+ window: { pid: number; title: string; className: string };
16
+ elements: FlatElement[];
17
+ totalElements: number;
18
+ };
19
+
20
+ export type FlatElement = {
21
+ id: number;
22
+ role: string;
23
+ name: string;
24
+ value: string | null;
25
+ depth: number;
26
+ isEnabled: boolean;
27
+ };
28
+
29
+ const DEFAULT_PORT = 9224;
30
+ const MAX_SNAPSHOT_ELEMENTS = 60;
31
+
32
+ export class DesktopController implements AppController {
33
+ private port: number;
34
+ private host: string = 'localhost';
35
+ private socket: Socket | null = null;
36
+ private _connected = false;
37
+ private runningSidecar: RunningSidecar | null = null;
38
+ private nextRequestId = 1;
39
+ private pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();
40
+ private buffer = '';
41
+
42
+ // Element cache from last snapshot (like BrowserController.elementCoords)
43
+ private elementCache = new Map<number, UIElement>();
44
+ private lastSnapshotWindow: { pid: number; title: string } | null = null;
45
+
46
+ constructor(port: number = DEFAULT_PORT) {
47
+ this.port = port;
48
+ }
49
+
50
+ // --- Connection lifecycle ---
51
+
52
+ async connect(): Promise<void> {
53
+ if (this._connected) return;
54
+
55
+ // Check if sidecar is already running
56
+ if (!(await isSidecarRunning(this.port))) {
57
+ console.log('[DesktopController] Sidecar not running, launching...');
58
+ this.runningSidecar = await launchSidecar(this.port);
59
+ this.host = this.runningSidecar.host;
60
+ }
61
+
62
+ // Open TCP socket
63
+ await this.openSocket();
64
+ this._connected = true;
65
+ console.log(`[DesktopController] Connected to sidecar on ${this.host}:${this.port}`);
66
+ }
67
+
68
+ async disconnect(): Promise<void> {
69
+ if (this.socket) {
70
+ this.socket.destroy();
71
+ this.socket = null;
72
+ }
73
+ this._connected = false;
74
+ this.elementCache.clear();
75
+ this.pending.clear();
76
+ this.buffer = '';
77
+
78
+ if (this.runningSidecar) {
79
+ await stopSidecar(this.runningSidecar);
80
+ this.runningSidecar = null;
81
+ }
82
+
83
+ console.log('[DesktopController] Disconnected');
84
+ }
85
+
86
+ get connected(): boolean {
87
+ return this._connected;
88
+ }
89
+
90
+ // --- AppController interface ---
91
+
92
+ async getActiveWindow(): Promise<WindowInfo> {
93
+ await this.ensureConnected();
94
+ const result = await this.send('getActiveWindow') as any;
95
+ if (!result) throw new Error('No active window found');
96
+ return this.toWindowInfo(result);
97
+ }
98
+
99
+ async listWindows(): Promise<WindowInfo[]> {
100
+ await this.ensureConnected();
101
+ const result = await this.send('listWindows') as any[];
102
+ return result.map((w) => this.toWindowInfo(w));
103
+ }
104
+
105
+ async getWindowTree(pid: number): Promise<UIElement[]> {
106
+ await this.ensureConnected();
107
+ const result = await this.send('getWindowTree', { pid, depth: 5 }) as any;
108
+ return this.parseElements(result.elements || []);
109
+ }
110
+
111
+ async clickElement(element: UIElement): Promise<void> {
112
+ await this.ensureConnected();
113
+ const id = parseInt(element.id, 10);
114
+ await this.send('clickElement', { elementId: id });
115
+ }
116
+
117
+ async typeText(text: string): Promise<void> {
118
+ await this.ensureConnected();
119
+ await this.send('typeText', { text });
120
+ }
121
+
122
+ async pressKeys(keys: string[]): Promise<void> {
123
+ await this.ensureConnected();
124
+ await this.send('pressKeys', { keys });
125
+ }
126
+
127
+ async captureScreen(): Promise<Buffer> {
128
+ await this.ensureConnected();
129
+ const base64 = await this.send('captureScreen') as string;
130
+ return Buffer.from(base64, 'base64');
131
+ }
132
+
133
+ async captureWindow(pid: number): Promise<Buffer> {
134
+ await this.ensureConnected();
135
+ const base64 = await this.send('captureWindow', { pid }) as string;
136
+ return Buffer.from(base64, 'base64');
137
+ }
138
+
139
+ async focusWindow(pid: number): Promise<void> {
140
+ await this.ensureConnected();
141
+ await this.send('focusWindow', { pid });
142
+ }
143
+
144
+ // --- Extended methods (snapshot-based, like BrowserController) ---
145
+
146
+ /**
147
+ * Get a snapshot of a window's UI elements with sequential [id]s.
148
+ * If no PID given, snapshots the active window.
149
+ */
150
+ async snapshot(pid?: number): Promise<DesktopSnapshot> {
151
+ await this.ensureConnected();
152
+
153
+ // Get target window
154
+ let targetPid = pid;
155
+ if (!targetPid) {
156
+ const active = await this.send('getActiveWindow') as any;
157
+ if (!active) throw new Error('No active window');
158
+ targetPid = active.pid;
159
+ }
160
+
161
+ // Get UI tree
162
+ const result = await this.send('getWindowTree', { pid: targetPid, depth: 5 }) as any;
163
+
164
+ // Flatten tree into sequential IDs
165
+ this.elementCache.clear();
166
+ const flatElements: FlatElement[] = [];
167
+ this.flattenTree(result.elements || [], 0, flatElements);
168
+
169
+ this.lastSnapshotWindow = {
170
+ pid: targetPid!,
171
+ title: result.window?.title || '',
172
+ };
173
+
174
+ return {
175
+ window: result.window || { pid: targetPid, title: '', className: '' },
176
+ elements: flatElements.slice(0, MAX_SNAPSHOT_ELEMENTS),
177
+ totalElements: flatElements.length,
178
+ };
179
+ }
180
+
181
+ /**
182
+ * Click a UI element by its snapshot [id].
183
+ */
184
+ async clickById(elementId: number): Promise<string> {
185
+ await this.ensureConnected();
186
+
187
+ const element = this.elementCache.get(elementId);
188
+ if (!element) {
189
+ return `Error: Element [${elementId}] not found. Run desktop_snapshot first.`;
190
+ }
191
+
192
+ await this.send('clickElement', { elementId });
193
+
194
+ const label = element.name ? `"${element.name}"` : element.role;
195
+ return `Clicked [${element.role}] ${label} (id: ${elementId})`;
196
+ }
197
+
198
+ /**
199
+ * Type text, optionally clicking an element first.
200
+ */
201
+ async typeById(elementId: number | undefined, text: string): Promise<string> {
202
+ await this.ensureConnected();
203
+
204
+ if (elementId !== undefined) {
205
+ const clickResult = await this.clickById(elementId);
206
+ if (clickResult.startsWith('Error:')) return clickResult;
207
+ // Brief pause after click
208
+ await Bun.sleep(200);
209
+ }
210
+
211
+ await this.send('typeText', { text });
212
+
213
+ const targetStr = elementId !== undefined ? ` into element [${elementId}]` : '';
214
+ return `Typed "${text}"${targetStr}`;
215
+ }
216
+
217
+ /**
218
+ * Launch a Windows application.
219
+ */
220
+ async launchApp(executable: string, args?: string): Promise<object> {
221
+ await this.ensureConnected();
222
+ return await this.send('launchApp', { executable, args: args || '' }) as object;
223
+ }
224
+
225
+ /**
226
+ * Close a window by PID.
227
+ */
228
+ async closeWindow(pid: number): Promise<void> {
229
+ await this.ensureConnected();
230
+ await this.send('closeWindow', { pid });
231
+ }
232
+
233
+ /**
234
+ * Drag one element onto another.
235
+ */
236
+ async dragElement(fromId: UIElement | number, toId: UIElement | number): Promise<void> {
237
+ await this.ensureConnected();
238
+
239
+ const fromNum = typeof fromId === 'number' ? fromId : parseInt(fromId.id, 10);
240
+ const toNum = typeof toId === 'number' ? toId : parseInt(toId.id, 10);
241
+
242
+ await this.send('dragElement', { fromId: fromNum, toId: toNum });
243
+ }
244
+
245
+ /**
246
+ * Take a desktop screenshot and save to file.
247
+ */
248
+ async screenshotToFile(pid?: number, filePath: string = '/tmp/jarvis-desktop-screenshot.png'): Promise<string> {
249
+ const buffer = pid ? await this.captureWindow(pid) : await this.captureScreen();
250
+ writeFileSync(filePath, buffer);
251
+ return filePath;
252
+ }
253
+
254
+ /**
255
+ * Take a desktop screenshot and return raw base64 data (for vision/LLM).
256
+ */
257
+ async screenshotBase64(pid?: number): Promise<{ base64: string; mimeType: string }> {
258
+ await this.ensureConnected();
259
+ const method = pid ? 'captureWindow' : 'captureScreen';
260
+ const base64 = await this.send(method, pid ? { pid } : {}) as string;
261
+ return { base64, mimeType: 'image/png' };
262
+ }
263
+
264
+ // --- Private helpers ---
265
+
266
+ private async ensureConnected(): Promise<void> {
267
+ if (this._connected && this.socket && !this.socket.destroyed) {
268
+ return;
269
+ }
270
+
271
+ // Connection went stale
272
+ if (this._connected) {
273
+ console.warn('[DesktopController] Connection stale, reconnecting...');
274
+ this._connected = false;
275
+ this.elementCache.clear();
276
+ this.pending.clear();
277
+ this.buffer = '';
278
+ }
279
+
280
+ await this.connect();
281
+ }
282
+
283
+ private openSocket(): Promise<void> {
284
+ return new Promise((resolve, reject) => {
285
+ const socket = createConnection({ host: this.host, port: this.port }, () => {
286
+ this.socket = socket;
287
+ resolve();
288
+ });
289
+
290
+ socket.setEncoding('utf-8');
291
+
292
+ socket.on('data', (chunk: string) => {
293
+ this.buffer += chunk;
294
+ this.processBuffer();
295
+ });
296
+
297
+ socket.on('error', (err) => {
298
+ if (!this._connected) {
299
+ reject(err);
300
+ } else {
301
+ console.error('[DesktopController] Socket error:', err.message);
302
+ this._connected = false;
303
+ }
304
+ });
305
+
306
+ socket.on('close', () => {
307
+ this._connected = false;
308
+ });
309
+
310
+ // Timeout for initial connection
311
+ setTimeout(() => {
312
+ if (!this._connected && !this.socket) {
313
+ socket.destroy();
314
+ reject(new Error(`Failed to connect to sidecar on ${this.host}:${this.port}`));
315
+ }
316
+ }, 5000);
317
+ });
318
+ }
319
+
320
+ private processBuffer(): void {
321
+ const lines = this.buffer.split('\n');
322
+ // Keep incomplete last line in buffer
323
+ this.buffer = lines.pop() || '';
324
+
325
+ for (let line of lines) {
326
+ line = line.trim();
327
+ if (!line) continue;
328
+
329
+ // Strip UTF-8 BOM if present (C# can emit it)
330
+ if (line.charCodeAt(0) === 0xFEFF) {
331
+ line = line.slice(1);
332
+ }
333
+
334
+ try {
335
+ const response = JSON.parse(line);
336
+ const id = response.id;
337
+ const p = this.pending.get(id);
338
+ if (p) {
339
+ this.pending.delete(id);
340
+ if (response.error) {
341
+ p.reject(new Error(response.error.message || 'Unknown error'));
342
+ } else {
343
+ p.resolve(response.result);
344
+ }
345
+ }
346
+ } catch {
347
+ console.warn('[DesktopController] Invalid JSON from sidecar:', line.slice(0, 100));
348
+ }
349
+ }
350
+ }
351
+
352
+ private send(method: string, params?: Record<string, unknown>): Promise<unknown> {
353
+ return new Promise((resolve, reject) => {
354
+ if (!this.socket || this.socket.destroyed) {
355
+ reject(new Error('Not connected to sidecar'));
356
+ return;
357
+ }
358
+
359
+ const id = this.nextRequestId++;
360
+ const request = JSON.stringify({
361
+ jsonrpc: '2.0',
362
+ method,
363
+ params: params || {},
364
+ id,
365
+ }) + '\n';
366
+
367
+ this.pending.set(id, { resolve, reject });
368
+
369
+ this.socket.write(request, (err) => {
370
+ if (err) {
371
+ this.pending.delete(id);
372
+ reject(err);
373
+ }
374
+ });
375
+
376
+ // Timeout per request
377
+ setTimeout(() => {
378
+ if (this.pending.has(id)) {
379
+ this.pending.delete(id);
380
+ reject(new Error(`Timeout waiting for response to ${method} (id: ${id})`));
381
+ }
382
+ }, 30_000);
383
+ });
384
+ }
385
+
386
+ private flattenTree(elements: any[], depth: number, result: FlatElement[]): void {
387
+ for (const el of elements) {
388
+ const id = el.id;
389
+ const uiElement: UIElement = {
390
+ id: String(id),
391
+ role: el.role || '',
392
+ name: el.name || '',
393
+ value: el.value || null,
394
+ bounds: el.bounds || { x: 0, y: 0, width: 0, height: 0 },
395
+ children: [],
396
+ properties: el.properties || {},
397
+ };
398
+
399
+ this.elementCache.set(id, uiElement);
400
+
401
+ result.push({
402
+ id,
403
+ role: el.role || '',
404
+ name: el.name || '',
405
+ value: el.value || null,
406
+ depth,
407
+ isEnabled: el.isEnabled !== false,
408
+ });
409
+
410
+ // Recurse into children
411
+ if (el.children && el.children.length > 0) {
412
+ this.flattenTree(el.children, depth + 1, result);
413
+ }
414
+ }
415
+ }
416
+
417
+ private toWindowInfo(raw: any): WindowInfo {
418
+ return {
419
+ pid: raw.pid || 0,
420
+ title: raw.title || '',
421
+ className: raw.className || '',
422
+ bounds: raw.bounds || { x: 0, y: 0, width: 0, height: 0 },
423
+ focused: raw.focused || false,
424
+ };
425
+ }
426
+
427
+ private parseElements(raw: any[]): UIElement[] {
428
+ return raw.map((el) => ({
429
+ id: String(el.id),
430
+ role: el.role || '',
431
+ name: el.name || '',
432
+ value: el.value || null,
433
+ bounds: el.bounds || { x: 0, y: 0, width: 0, height: 0 },
434
+ children: el.children ? this.parseElements(el.children) : [],
435
+ properties: el.properties || {},
436
+ }));
437
+ }
438
+ }
@@ -0,0 +1,64 @@
1
+ export type WindowInfo = {
2
+ pid: number;
3
+ title: string;
4
+ className: string;
5
+ bounds: { x: number; y: number; width: number; height: number };
6
+ focused: boolean;
7
+ };
8
+
9
+ export type UIElement = {
10
+ id: string;
11
+ role: string;
12
+ name: string;
13
+ value: string | null;
14
+ bounds: { x: number; y: number; width: number; height: number };
15
+ children: UIElement[];
16
+ properties: Record<string, unknown>;
17
+ };
18
+
19
+ export interface AppController {
20
+ getActiveWindow(): Promise<WindowInfo>;
21
+ getWindowTree(pid: number): Promise<UIElement[]>;
22
+ listWindows(): Promise<WindowInfo[]>;
23
+
24
+ clickElement(element: UIElement): Promise<void>;
25
+ typeText(text: string): Promise<void>;
26
+ pressKeys(keys: string[]): Promise<void>;
27
+
28
+ captureScreen(): Promise<Buffer>;
29
+ captureWindow(pid: number): Promise<Buffer>;
30
+
31
+ focusWindow(pid: number): Promise<void>;
32
+
33
+ // Optional extended operations
34
+ launchApp?(executable: string, args?: string): Promise<object>;
35
+ closeWindow?(pid: number): Promise<void>;
36
+ dragElement?(from: UIElement, to: UIElement): Promise<void>;
37
+ }
38
+
39
+ export function getAppController(): AppController {
40
+ const platform = process.platform;
41
+
42
+ switch (platform) {
43
+ case 'linux': {
44
+ // In WSL2, use DesktopController to control Windows desktop via sidecar
45
+ const { WSLBridge } = require('../terminal/wsl-bridge.ts');
46
+ if (WSLBridge.isWSL()) {
47
+ const { DesktopController } = require('./desktop-controller.ts');
48
+ return new DesktopController();
49
+ }
50
+ const { LinuxAppController } = require('./linux.ts');
51
+ return new LinuxAppController();
52
+ }
53
+ case 'win32': {
54
+ const { WindowsAppController } = require('./windows.ts');
55
+ return new WindowsAppController();
56
+ }
57
+ case 'darwin': {
58
+ const { MacAppController } = require('./macos.ts');
59
+ return new MacAppController();
60
+ }
61
+ default:
62
+ throw new Error(`Unsupported platform: ${platform}`);
63
+ }
64
+ }