@skillful-agents/agent-computer 0.0.3

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 (185) hide show
  1. package/README.md +458 -0
  2. package/bin/ac-core-darwin-arm64 +0 -0
  3. package/bin/ac-core-darwin-x64 +0 -0
  4. package/bin/ac-core-win32-arm64.exe +0 -0
  5. package/bin/ac-core-win32-x64.exe +0 -0
  6. package/dist/bin/ac.d.ts +3 -0
  7. package/dist/bin/ac.d.ts.map +1 -0
  8. package/dist/bin/ac.js +126 -0
  9. package/dist/bin/ac.js.map +1 -0
  10. package/dist/src/bridge.d.ts +69 -0
  11. package/dist/src/bridge.d.ts.map +1 -0
  12. package/dist/src/bridge.js +687 -0
  13. package/dist/src/bridge.js.map +1 -0
  14. package/dist/src/cdp/ax-tree.d.ts +19 -0
  15. package/dist/src/cdp/ax-tree.d.ts.map +1 -0
  16. package/dist/src/cdp/ax-tree.js +159 -0
  17. package/dist/src/cdp/ax-tree.js.map +1 -0
  18. package/dist/src/cdp/bounds.d.ts +25 -0
  19. package/dist/src/cdp/bounds.d.ts.map +1 -0
  20. package/dist/src/cdp/bounds.js +62 -0
  21. package/dist/src/cdp/bounds.js.map +1 -0
  22. package/dist/src/cdp/client.d.ts +115 -0
  23. package/dist/src/cdp/client.d.ts.map +1 -0
  24. package/dist/src/cdp/client.js +269 -0
  25. package/dist/src/cdp/client.js.map +1 -0
  26. package/dist/src/cdp/connection.d.ts +25 -0
  27. package/dist/src/cdp/connection.d.ts.map +1 -0
  28. package/dist/src/cdp/connection.js +249 -0
  29. package/dist/src/cdp/connection.js.map +1 -0
  30. package/dist/src/cdp/diff.d.ts +17 -0
  31. package/dist/src/cdp/diff.d.ts.map +1 -0
  32. package/dist/src/cdp/diff.js +52 -0
  33. package/dist/src/cdp/diff.js.map +1 -0
  34. package/dist/src/cdp/discovery.d.ts +8 -0
  35. package/dist/src/cdp/discovery.d.ts.map +1 -0
  36. package/dist/src/cdp/discovery.js +54 -0
  37. package/dist/src/cdp/discovery.js.map +1 -0
  38. package/dist/src/cdp/index.d.ts +12 -0
  39. package/dist/src/cdp/index.d.ts.map +1 -0
  40. package/dist/src/cdp/index.js +10 -0
  41. package/dist/src/cdp/index.js.map +1 -0
  42. package/dist/src/cdp/interactions.d.ts +47 -0
  43. package/dist/src/cdp/interactions.d.ts.map +1 -0
  44. package/dist/src/cdp/interactions.js +298 -0
  45. package/dist/src/cdp/interactions.js.map +1 -0
  46. package/dist/src/cdp/port-manager.d.ts +3 -0
  47. package/dist/src/cdp/port-manager.d.ts.map +1 -0
  48. package/dist/src/cdp/port-manager.js +33 -0
  49. package/dist/src/cdp/port-manager.js.map +1 -0
  50. package/dist/src/cdp/role-map.d.ts +8 -0
  51. package/dist/src/cdp/role-map.d.ts.map +1 -0
  52. package/dist/src/cdp/role-map.js +99 -0
  53. package/dist/src/cdp/role-map.js.map +1 -0
  54. package/dist/src/cdp/types.d.ts +53 -0
  55. package/dist/src/cdp/types.d.ts.map +1 -0
  56. package/dist/src/cdp/types.js +2 -0
  57. package/dist/src/cdp/types.js.map +1 -0
  58. package/dist/src/cli/commands/apps.d.ts +2 -0
  59. package/dist/src/cli/commands/apps.d.ts.map +1 -0
  60. package/dist/src/cli/commands/apps.js +62 -0
  61. package/dist/src/cli/commands/apps.js.map +1 -0
  62. package/dist/src/cli/commands/batch.d.ts +2 -0
  63. package/dist/src/cli/commands/batch.d.ts.map +1 -0
  64. package/dist/src/cli/commands/batch.js +36 -0
  65. package/dist/src/cli/commands/batch.js.map +1 -0
  66. package/dist/src/cli/commands/click.d.ts +2 -0
  67. package/dist/src/cli/commands/click.d.ts.map +1 -0
  68. package/dist/src/cli/commands/click.js +60 -0
  69. package/dist/src/cli/commands/click.js.map +1 -0
  70. package/dist/src/cli/commands/clipboard.d.ts +2 -0
  71. package/dist/src/cli/commands/clipboard.d.ts.map +1 -0
  72. package/dist/src/cli/commands/clipboard.js +30 -0
  73. package/dist/src/cli/commands/clipboard.js.map +1 -0
  74. package/dist/src/cli/commands/dialog.d.ts +2 -0
  75. package/dist/src/cli/commands/dialog.d.ts.map +1 -0
  76. package/dist/src/cli/commands/dialog.js +44 -0
  77. package/dist/src/cli/commands/dialog.js.map +1 -0
  78. package/dist/src/cli/commands/drag.d.ts +2 -0
  79. package/dist/src/cli/commands/drag.d.ts.map +1 -0
  80. package/dist/src/cli/commands/drag.js +25 -0
  81. package/dist/src/cli/commands/drag.js.map +1 -0
  82. package/dist/src/cli/commands/find.d.ts +2 -0
  83. package/dist/src/cli/commands/find.d.ts.map +1 -0
  84. package/dist/src/cli/commands/find.js +98 -0
  85. package/dist/src/cli/commands/find.js.map +1 -0
  86. package/dist/src/cli/commands/menu.d.ts +2 -0
  87. package/dist/src/cli/commands/menu.d.ts.map +1 -0
  88. package/dist/src/cli/commands/menu.js +35 -0
  89. package/dist/src/cli/commands/menu.js.map +1 -0
  90. package/dist/src/cli/commands/screenshot.d.ts +2 -0
  91. package/dist/src/cli/commands/screenshot.d.ts.map +1 -0
  92. package/dist/src/cli/commands/screenshot.js +26 -0
  93. package/dist/src/cli/commands/screenshot.js.map +1 -0
  94. package/dist/src/cli/commands/scroll.d.ts +2 -0
  95. package/dist/src/cli/commands/scroll.d.ts.map +1 -0
  96. package/dist/src/cli/commands/scroll.js +76 -0
  97. package/dist/src/cli/commands/scroll.js.map +1 -0
  98. package/dist/src/cli/commands/session.d.ts +2 -0
  99. package/dist/src/cli/commands/session.d.ts.map +1 -0
  100. package/dist/src/cli/commands/session.js +26 -0
  101. package/dist/src/cli/commands/session.js.map +1 -0
  102. package/dist/src/cli/commands/snapshot.d.ts +2 -0
  103. package/dist/src/cli/commands/snapshot.d.ts.map +1 -0
  104. package/dist/src/cli/commands/snapshot.js +23 -0
  105. package/dist/src/cli/commands/snapshot.js.map +1 -0
  106. package/dist/src/cli/commands/type.d.ts +2 -0
  107. package/dist/src/cli/commands/type.d.ts.map +1 -0
  108. package/dist/src/cli/commands/type.js +68 -0
  109. package/dist/src/cli/commands/type.js.map +1 -0
  110. package/dist/src/cli/commands/windowmgmt.d.ts +2 -0
  111. package/dist/src/cli/commands/windowmgmt.d.ts.map +1 -0
  112. package/dist/src/cli/commands/windowmgmt.js +61 -0
  113. package/dist/src/cli/commands/windowmgmt.js.map +1 -0
  114. package/dist/src/cli/commands/windows.d.ts +2 -0
  115. package/dist/src/cli/commands/windows.d.ts.map +1 -0
  116. package/dist/src/cli/commands/windows.js +9 -0
  117. package/dist/src/cli/commands/windows.js.map +1 -0
  118. package/dist/src/cli/commands.d.ts +13 -0
  119. package/dist/src/cli/commands.d.ts.map +1 -0
  120. package/dist/src/cli/commands.js +177 -0
  121. package/dist/src/cli/commands.js.map +1 -0
  122. package/dist/src/cli/output.d.ts +4 -0
  123. package/dist/src/cli/output.d.ts.map +1 -0
  124. package/dist/src/cli/output.js +249 -0
  125. package/dist/src/cli/output.js.map +1 -0
  126. package/dist/src/cli/parser.d.ts +19 -0
  127. package/dist/src/cli/parser.d.ts.map +1 -0
  128. package/dist/src/cli/parser.js +125 -0
  129. package/dist/src/cli/parser.js.map +1 -0
  130. package/dist/src/config.d.ts +14 -0
  131. package/dist/src/config.d.ts.map +1 -0
  132. package/dist/src/config.js +73 -0
  133. package/dist/src/config.js.map +1 -0
  134. package/dist/src/daemon.d.ts +21 -0
  135. package/dist/src/daemon.d.ts.map +1 -0
  136. package/dist/src/daemon.js +180 -0
  137. package/dist/src/daemon.js.map +1 -0
  138. package/dist/src/errors.d.ts +39 -0
  139. package/dist/src/errors.d.ts.map +1 -0
  140. package/dist/src/errors.js +103 -0
  141. package/dist/src/errors.js.map +1 -0
  142. package/dist/src/index.d.ts +8 -0
  143. package/dist/src/index.d.ts.map +1 -0
  144. package/dist/src/index.js +8 -0
  145. package/dist/src/index.js.map +1 -0
  146. package/dist/src/platform/index.d.ts +6 -0
  147. package/dist/src/platform/index.d.ts.map +1 -0
  148. package/dist/src/platform/index.js +14 -0
  149. package/dist/src/platform/index.js.map +1 -0
  150. package/dist/src/platform/resolve.d.ts +2 -0
  151. package/dist/src/platform/resolve.d.ts.map +1 -0
  152. package/dist/src/platform/resolve.js +54 -0
  153. package/dist/src/platform/resolve.js.map +1 -0
  154. package/dist/src/refs.d.ts +12 -0
  155. package/dist/src/refs.d.ts.map +1 -0
  156. package/dist/src/refs.js +85 -0
  157. package/dist/src/refs.js.map +1 -0
  158. package/dist/src/sdk.d.ts +258 -0
  159. package/dist/src/sdk.d.ts.map +1 -0
  160. package/dist/src/sdk.js +285 -0
  161. package/dist/src/sdk.js.map +1 -0
  162. package/dist/src/types.d.ts +93 -0
  163. package/dist/src/types.d.ts.map +1 -0
  164. package/dist/src/types.js +9 -0
  165. package/dist/src/types.js.map +1 -0
  166. package/package.json +67 -0
  167. package/schema/examples/click.response.example.json +9 -0
  168. package/schema/examples/error.response.example.json +12 -0
  169. package/schema/examples/ping.response.example.json +7 -0
  170. package/schema/examples/snapshot.response.example.json +50 -0
  171. package/schema/methods/click.request.json +26 -0
  172. package/schema/methods/click.response.json +24 -0
  173. package/schema/methods/error.response.json +19 -0
  174. package/schema/methods/ping.request.json +12 -0
  175. package/schema/methods/ping.response.json +17 -0
  176. package/schema/methods/snapshot.request.json +24 -0
  177. package/schema/methods/snapshot.response.json +23 -0
  178. package/schema/methods/status.request.json +12 -0
  179. package/schema/methods/status.response.json +20 -0
  180. package/schema/types/element.json +26 -0
  181. package/schema/types/normalized-role.json +13 -0
  182. package/schema/types/ref.json +7 -0
  183. package/schema/types/window-info.json +24 -0
  184. package/scripts/build-macos.sh +5 -0
  185. package/scripts/postinstall.js +35 -0
@@ -0,0 +1,687 @@
1
+ import { connect } from 'net';
2
+ import { execFileSync, spawn } from 'child_process';
3
+ import { existsSync, readFileSync, mkdirSync, unlinkSync } from 'fs';
4
+ import { errorFromCode, ACError, TimeoutError } from './errors.js';
5
+ import { SOCKET_PATH, DAEMON_JSON_PATH, AC_DIR, IS_NAMED_PIPE } from './platform/index.js';
6
+ import { resolveBinary } from './platform/resolve.js';
7
+ import { CDPClient } from './cdp/client.js';
8
+ import { findFreePort } from './cdp/port-manager.js';
9
+ import { waitForCDP } from './cdp/discovery.js';
10
+ let nextId = 1;
11
+ // Methods that can be routed to CDP when a Chromium app is grabbed
12
+ const CDP_CAPABLE_METHODS = new Set([
13
+ 'snapshot', 'find', 'read', 'children', 'click', 'hover', 'focus',
14
+ 'type', 'fill', 'key', 'scroll', 'select', 'check', 'uncheck',
15
+ 'box', 'is', 'changed', 'diff',
16
+ ]);
17
+ export function buildRequest(method, params = {}) {
18
+ return {
19
+ jsonrpc: '2.0',
20
+ id: nextId++,
21
+ method,
22
+ params,
23
+ };
24
+ }
25
+ export function parseResponse(raw) {
26
+ if (raw === null || raw === undefined || typeof raw !== 'object') {
27
+ throw new Error('Invalid JSON-RPC response: expected an object');
28
+ }
29
+ const resp = raw;
30
+ if (resp.jsonrpc !== '2.0') {
31
+ throw new Error('Invalid JSON-RPC response: missing or incorrect "jsonrpc" field');
32
+ }
33
+ if (!('id' in resp)) {
34
+ throw new Error('Invalid JSON-RPC response: missing "id" field');
35
+ }
36
+ if ('error' in resp && resp.error) {
37
+ const err = resp.error;
38
+ throw errorFromCode(err.code, err.message, err.data);
39
+ }
40
+ if ('result' in resp) {
41
+ return resp.result;
42
+ }
43
+ throw new Error('Invalid JSON-RPC response: missing both "result" and "error"');
44
+ }
45
+ export function _resetIdCounter() {
46
+ nextId = 1;
47
+ }
48
+ export class Bridge {
49
+ socket = null;
50
+ daemonProcess = null;
51
+ binaryPath;
52
+ timeout;
53
+ pendingRequests = new Map();
54
+ buffer = '';
55
+ bomChecked = false;
56
+ // CDP routing state
57
+ cdpClients = new Map(); // pid → CDPClient
58
+ grabbedAppInfo = null;
59
+ constructor(options = {}) {
60
+ this.timeout = options.timeout ?? 10000;
61
+ this.binaryPath = options.binaryPath ?? resolveBinary();
62
+ }
63
+ // Send a JSON-RPC request — routes to CDP or native daemon
64
+ async send(method, params = {}) {
65
+ // Special handlers
66
+ if (method === 'launch')
67
+ return this.handleLaunch(params);
68
+ if (method === 'relaunch')
69
+ return this.handleRelaunch(params);
70
+ if (method === 'grab')
71
+ return this.handleGrab(params);
72
+ if (method === 'ungrab')
73
+ return this.handleUngrab(params);
74
+ if (method === 'batch')
75
+ return this.handleBatch(params);
76
+ if (method === 'wait')
77
+ return this.handleWait(params);
78
+ // Route to CDP if applicable
79
+ if (CDP_CAPABLE_METHODS.has(method) && await this.ensureCDPIfNeeded()) {
80
+ return this.sendToCDP(method, params);
81
+ }
82
+ return this.sendToNative(method, params);
83
+ }
84
+ // Send directly to the native daemon
85
+ async sendToNative(method, params = {}) {
86
+ // Ensure daemon is running and connected
87
+ if (!this.socket || this.socket.destroyed) {
88
+ await this.ensureDaemon();
89
+ }
90
+ const request = buildRequest(method, params);
91
+ return new Promise((resolve, reject) => {
92
+ const timer = setTimeout(() => {
93
+ this.pendingRequests.delete(request.id);
94
+ reject(new TimeoutError(`Command "${method}" timed out after ${this.timeout}ms`));
95
+ }, this.timeout);
96
+ this.pendingRequests.set(request.id, { resolve, reject, timer });
97
+ const line = JSON.stringify(request) + '\n';
98
+ this.socket.write(line, (err) => {
99
+ if (err) {
100
+ this.pendingRequests.delete(request.id);
101
+ clearTimeout(timer);
102
+ reject(new Error(`Failed to send command: ${err.message}`));
103
+ }
104
+ });
105
+ });
106
+ }
107
+ // Send a JSON-RPC request via one-shot mode (exec binary per command)
108
+ sendOneShot(method, params = {}) {
109
+ const request = buildRequest(method, params);
110
+ const input = JSON.stringify(request);
111
+ try {
112
+ const stdout = execFileSync(this.binaryPath, [], {
113
+ encoding: 'utf-8',
114
+ input,
115
+ timeout: this.timeout,
116
+ }).trim();
117
+ const raw = JSON.parse(stdout);
118
+ return parseResponse(raw);
119
+ }
120
+ catch (err) {
121
+ if (err instanceof ACError)
122
+ throw err;
123
+ // Try to parse error stdout (may contain BOM on Windows)
124
+ const errOut = (err.stdout || err.output?.[1]?.toString() || '').replace(/^\uFEFF/, '').trim();
125
+ if (errOut) {
126
+ try {
127
+ const raw = JSON.parse(errOut);
128
+ return parseResponse(raw);
129
+ }
130
+ catch (parseErr) {
131
+ if (parseErr instanceof ACError)
132
+ throw parseErr;
133
+ /* fall through */
134
+ }
135
+ }
136
+ throw new Error(`One-shot command failed: ${err.message}`);
137
+ }
138
+ }
139
+ /**
140
+ * Lazily detect if the currently grabbed app has CDP available.
141
+ * Checks live every time: asks daemon for grab state, checks if Chromium,
142
+ * scans port range for a live CDP endpoint.
143
+ */
144
+ async ensureCDPIfNeeded() {
145
+ // Already connected this session
146
+ if (this.grabbedAppInfo?.isCDP)
147
+ return true;
148
+ // Ask the daemon what's grabbed (status includes app name and pid from cached window info)
149
+ let status;
150
+ try {
151
+ status = await this.sendToNative('status');
152
+ }
153
+ catch {
154
+ return false;
155
+ }
156
+ const app = status.grabbed_app;
157
+ const pid = status.grabbed_pid;
158
+ if (!app || !pid)
159
+ return false;
160
+ // Ask daemon for CDP port
161
+ let port;
162
+ try {
163
+ const result = await this.sendToNative('cdp_port', { name: app });
164
+ if (result.port)
165
+ port = result.port;
166
+ }
167
+ catch {
168
+ return false;
169
+ }
170
+ if (!port)
171
+ return false;
172
+ // Connect CDP client
173
+ try {
174
+ await waitForCDP(port, 3000);
175
+ const client = new CDPClient(port);
176
+ await client.connect();
177
+ this.cdpClients.set(pid, client);
178
+ this.grabbedAppInfo = { pid, isCDP: true, app };
179
+ return true;
180
+ }
181
+ catch {
182
+ return false;
183
+ }
184
+ }
185
+ async sendToCDP(method, params) {
186
+ const pid = this.grabbedAppInfo.pid;
187
+ const client = this.cdpClients.get(pid);
188
+ if (!client || !client.isConnected()) {
189
+ throw new Error('CDP client not connected for grabbed app');
190
+ }
191
+ // For non-snapshot commands, ensure we have state from a previous snapshot
192
+ if (method !== 'snapshot' && client.getLastRefMap().size === 0) {
193
+ // First try loading the persisted refMap (fast, for click/hover/focus)
194
+ try {
195
+ const kv = await this.sendToNative('kv_get', { key: 'cdp_refmap' });
196
+ if (kv.value && typeof kv.value === 'object') {
197
+ const map = client.getLastRefMap();
198
+ for (const [ref, nodeRef] of Object.entries(kv.value)) {
199
+ map.set(ref, nodeRef);
200
+ }
201
+ }
202
+ }
203
+ catch { /* old daemon without kv_set */ }
204
+ // For query commands that need full elements, take a fresh snapshot
205
+ const NEEDS_ELEMENTS = new Set(['find', 'read', 'is', 'children', 'changed', 'diff']);
206
+ if (NEEDS_ELEMENTS.has(method)) {
207
+ const windowInfo = await this.sendToNative('windows', { app: this.grabbedAppInfo.app });
208
+ const win = windowInfo.windows?.[0];
209
+ if (win)
210
+ await client.snapshot({}, win);
211
+ }
212
+ }
213
+ switch (method) {
214
+ case 'snapshot': {
215
+ // Get window info from native daemon
216
+ const windowInfo = await this.sendToNative('windows', { app: this.grabbedAppInfo.app });
217
+ const win = windowInfo.windows?.[0];
218
+ if (!win)
219
+ throw new Error('No window found for grabbed CDP app');
220
+ const snap = await client.snapshot({
221
+ interactive: params.interactive,
222
+ depth: params.depth,
223
+ }, win);
224
+ // Persist refMap to daemon so other CLI invocations can use it
225
+ const serialized = {};
226
+ for (const [ref, nodeRef] of client.getLastRefMap()) {
227
+ serialized[ref] = { nodeId: nodeRef.nodeId, backendDOMNodeId: nodeRef.backendDOMNodeId };
228
+ }
229
+ await this.sendToNative('kv_set', { key: 'cdp_refmap', value: serialized }).catch(() => { });
230
+ return snap;
231
+ }
232
+ case 'click': {
233
+ if (params.ref) {
234
+ await client.click(params.ref, {
235
+ right: params.right,
236
+ double: params.double,
237
+ count: params.count,
238
+ modifiers: params.modifiers,
239
+ });
240
+ return { ok: true };
241
+ }
242
+ // Coordinate clicks use screen coords — native CGEvent handles these correctly
243
+ return this.sendToNative(method, params);
244
+ }
245
+ case 'hover': {
246
+ if (params.ref) {
247
+ await client.hover(params.ref);
248
+ return { ok: true };
249
+ }
250
+ // Coordinate hovers use screen coords — route to native
251
+ return this.sendToNative(method, params);
252
+ }
253
+ case 'focus': {
254
+ await client.focus(params.ref);
255
+ return { ok: true };
256
+ }
257
+ case 'type': {
258
+ await client.type(params.text, { delay: params.delay });
259
+ return { ok: true };
260
+ }
261
+ case 'fill': {
262
+ await client.fill(params.ref, params.text);
263
+ return { ok: true };
264
+ }
265
+ case 'key': {
266
+ await client.key(params.combo, params.repeat);
267
+ return { ok: true };
268
+ }
269
+ case 'scroll': {
270
+ await client.scroll(params.direction, {
271
+ amount: params.amount,
272
+ on: params.on,
273
+ });
274
+ return { ok: true };
275
+ }
276
+ case 'select': {
277
+ await client.select(params.ref, params.value);
278
+ return { ok: true };
279
+ }
280
+ case 'check': {
281
+ await client.check(params.ref);
282
+ return { ok: true };
283
+ }
284
+ case 'uncheck': {
285
+ await client.uncheck(params.ref);
286
+ return { ok: true };
287
+ }
288
+ case 'find': {
289
+ return client.find(params.text, {
290
+ role: params.role,
291
+ first: params.first,
292
+ });
293
+ }
294
+ case 'read': {
295
+ return client.read(params.ref, params.attr);
296
+ }
297
+ case 'box': {
298
+ return client.box(params.ref);
299
+ }
300
+ case 'is': {
301
+ return client.is(params.state, params.ref);
302
+ }
303
+ case 'children': {
304
+ return client.children(params.ref);
305
+ }
306
+ case 'changed': {
307
+ const changed = await client.changed();
308
+ return { ok: true, changed };
309
+ }
310
+ case 'diff': {
311
+ const diff = await client.diff();
312
+ return { ok: true, ...diff };
313
+ }
314
+ default:
315
+ // Fall through to native for unhandled CDP methods
316
+ return this.sendToNative(method, params);
317
+ }
318
+ }
319
+ async handleLaunch(params) {
320
+ const name = params.name;
321
+ if (!name)
322
+ return this.sendToNative('launch', params);
323
+ // Check if app is Chromium-based
324
+ let isChromium = false;
325
+ try {
326
+ const result = await this.sendToNative('is_chromium', { name });
327
+ isChromium = result.is_chromium;
328
+ }
329
+ catch { /* method unavailable — old daemon */ }
330
+ if (!isChromium) {
331
+ return this.sendToNative('launch', params);
332
+ }
333
+ // Chromium app — tell daemon to launch with CDP
334
+ const port = await findFreePort();
335
+ const result = await this.sendToNative('launch_cdp', { name, port });
336
+ // Wait for CDP to be ready
337
+ await waitForCDP(port, 15000);
338
+ return result;
339
+ }
340
+ async handleRelaunch(params) {
341
+ const name = params.name;
342
+ if (!name)
343
+ throw new Error('Missing app name for relaunch');
344
+ // Quit the app first
345
+ try {
346
+ await this.sendToNative('quit', { name, force: true });
347
+ }
348
+ catch { /* ok if not running */ }
349
+ // Wait for it to exit
350
+ await sleep(2000);
351
+ // Relaunch with CDP
352
+ return this.handleLaunch({ ...params, name });
353
+ }
354
+ async handleGrab(params) {
355
+ // Send grab to native daemon first
356
+ const result = await this.sendToNative('grab', params);
357
+ // Window info is nested inside result.window
358
+ const windowInfo = (result.window ?? result);
359
+ const pid = windowInfo.process_id;
360
+ const app = windowInfo.app;
361
+ // Store grab info; CDP connection is established lazily by ensureCDPIfNeeded
362
+ this.grabbedAppInfo = pid && app ? { pid, isCDP: false, app } : null;
363
+ return result;
364
+ }
365
+ async handleUngrab(params) {
366
+ this.grabbedAppInfo = null;
367
+ return this.sendToNative('ungrab', params);
368
+ }
369
+ async handleBatch(params) {
370
+ const commands = params.commands;
371
+ const stopOnError = params.stop_on_error !== false;
372
+ if (!commands || !Array.isArray(commands)) {
373
+ return this.sendToNative('batch', params);
374
+ }
375
+ // Route each sub-command individually through the CDP/native decision
376
+ const results = [];
377
+ for (const cmd of commands) {
378
+ const [method, ...rest] = cmd;
379
+ const cmdParams = (rest[0] && typeof rest[0] === 'object') ? rest[0] : {};
380
+ try {
381
+ const result = await this.send(method, cmdParams);
382
+ results.push({ ok: true, result });
383
+ }
384
+ catch (err) {
385
+ results.push({ ok: false, error: err.message });
386
+ if (stopOnError)
387
+ break;
388
+ }
389
+ }
390
+ return { ok: true, results };
391
+ }
392
+ async handleWait(params) {
393
+ // waitForText through CDP needs to poll snapshot
394
+ if (params.text && this.grabbedAppInfo?.isCDP) {
395
+ const text = params.text;
396
+ const timeout = params.timeout ?? 10000;
397
+ const gone = params.gone === true;
398
+ const start = Date.now();
399
+ while (Date.now() - start < timeout) {
400
+ const pid = this.grabbedAppInfo.pid;
401
+ const client = this.cdpClients.get(pid);
402
+ if (client?.isConnected()) {
403
+ try {
404
+ const windowInfo = await this.sendToNative('windows', { app: this.grabbedAppInfo.app });
405
+ const win = windowInfo.windows?.[0];
406
+ if (win) {
407
+ const snap = await client.snapshot({}, win);
408
+ const found = this.flattenElements(snap.elements).some(el => el.label?.includes(text) || el.value?.includes(text));
409
+ if (gone ? !found : found)
410
+ return { ok: true };
411
+ }
412
+ }
413
+ catch { /* retry */ }
414
+ }
415
+ await sleep(500);
416
+ }
417
+ throw new TimeoutError(`Text "${text}" ${gone ? 'did not disappear' : 'not found'} within ${timeout}ms`);
418
+ }
419
+ // waitForApp and waitForWindow stay native
420
+ return this.sendToNative('wait', params);
421
+ }
422
+ flattenElements(elements) {
423
+ const result = [];
424
+ const walk = (els) => {
425
+ for (const el of els) {
426
+ result.push(el);
427
+ if (el.children)
428
+ walk(el.children);
429
+ }
430
+ };
431
+ walk(elements);
432
+ return result;
433
+ }
434
+ async ensureDaemon() {
435
+ // Check if a daemon is already running
436
+ const info = this.readDaemonInfo();
437
+ if (info && this.isProcessAlive(info.pid)) {
438
+ try {
439
+ await this.connectToSocket(info.socket);
440
+ return;
441
+ }
442
+ catch {
443
+ // Socket exists but connection failed — daemon is dead
444
+ }
445
+ }
446
+ // Clean up stale state
447
+ this.cleanupStaleFiles();
448
+ // Spawn new daemon
449
+ await this.spawnDaemon();
450
+ await this.waitForSocket();
451
+ // Poll for daemon.json to appear (the daemon may flush it slightly after the socket/pipe is ready)
452
+ const djStart = Date.now();
453
+ while (Date.now() - djStart < 2000) {
454
+ if (existsSync(DAEMON_JSON_PATH))
455
+ break;
456
+ await sleep(50);
457
+ }
458
+ const newInfo = this.readDaemonInfo();
459
+ if (!newInfo) {
460
+ throw new Error('Daemon started but daemon.json not found');
461
+ }
462
+ await this.connectToSocket(newInfo.socket);
463
+ }
464
+ async spawnDaemon() {
465
+ mkdirSync(AC_DIR, { recursive: true });
466
+ this.daemonProcess = spawn(this.binaryPath, ['--daemon'], {
467
+ detached: true,
468
+ stdio: ['ignore', 'ignore', 'pipe'],
469
+ });
470
+ this.daemonProcess.unref();
471
+ // Log daemon stderr for debugging
472
+ this.daemonProcess.stderr?.on('data', (data) => {
473
+ if (process.env.AC_VERBOSE === '1') {
474
+ process.stderr.write(data);
475
+ }
476
+ });
477
+ this.daemonProcess.on('exit', (code) => {
478
+ if (process.env.AC_VERBOSE === '1') {
479
+ process.stderr.write(`[bridge] daemon exited with code ${code}\n`);
480
+ }
481
+ });
482
+ }
483
+ async waitForSocket(maxWait = 5000) {
484
+ const start = Date.now();
485
+ if (IS_NAMED_PIPE) {
486
+ // Named pipes don't exist as files — probe by attempting connection
487
+ while (Date.now() - start < maxWait) {
488
+ try {
489
+ await this.probeConnection(SOCKET_PATH);
490
+ return;
491
+ }
492
+ catch { /* not ready yet */ }
493
+ await sleep(100);
494
+ }
495
+ throw new Error(`Daemon pipe did not become available within ${maxWait}ms`);
496
+ }
497
+ // Unix socket: wait for file to appear on disk
498
+ while (Date.now() - start < maxWait) {
499
+ if (existsSync(SOCKET_PATH)) {
500
+ await sleep(50);
501
+ return;
502
+ }
503
+ await sleep(50);
504
+ }
505
+ throw new Error(`Daemon socket did not appear within ${maxWait}ms`);
506
+ }
507
+ probeConnection(path) {
508
+ return new Promise((resolve, reject) => {
509
+ const sock = connect({ path });
510
+ const timeout = setTimeout(() => { sock.destroy(); reject(new Error('probe timeout')); }, 500);
511
+ sock.on('connect', () => { clearTimeout(timeout); sock.destroy(); resolve(); });
512
+ sock.on('error', (err) => { clearTimeout(timeout); reject(err); });
513
+ });
514
+ }
515
+ connectToSocket(socketPath) {
516
+ return new Promise((resolve, reject) => {
517
+ const sock = connect({ path: socketPath });
518
+ const timeout = setTimeout(() => {
519
+ sock.destroy();
520
+ reject(new Error('Socket connection timed out'));
521
+ }, 3000);
522
+ sock.on('connect', () => {
523
+ clearTimeout(timeout);
524
+ this.socket = sock;
525
+ this.buffer = '';
526
+ this.bomChecked = false;
527
+ this.setupSocketHandlers();
528
+ resolve();
529
+ });
530
+ sock.on('error', (err) => {
531
+ clearTimeout(timeout);
532
+ reject(err);
533
+ });
534
+ });
535
+ }
536
+ setupSocketHandlers() {
537
+ if (!this.socket)
538
+ return;
539
+ this.socket.on('data', (chunk) => {
540
+ let str = chunk.toString();
541
+ // Strip UTF-8 BOM if present (Windows .NET may emit one on first chunk)
542
+ if (!this.bomChecked) {
543
+ if (str.charCodeAt(0) === 0xFEFF)
544
+ str = str.slice(1);
545
+ this.bomChecked = true;
546
+ }
547
+ this.buffer += str;
548
+ this.processBuffer();
549
+ });
550
+ this.socket.on('close', () => {
551
+ // Reject all pending requests
552
+ for (const [id, pending] of this.pendingRequests) {
553
+ clearTimeout(pending.timer);
554
+ pending.reject(new Error('Socket closed'));
555
+ this.pendingRequests.delete(id);
556
+ }
557
+ this.socket = null;
558
+ });
559
+ this.socket.on('error', (err) => {
560
+ if (process.env.AC_VERBOSE === '1') {
561
+ process.stderr.write(`[bridge] socket error: ${err.message}\n`);
562
+ }
563
+ });
564
+ }
565
+ processBuffer() {
566
+ let newlineIdx;
567
+ while ((newlineIdx = this.buffer.indexOf('\n')) !== -1) {
568
+ const line = this.buffer.slice(0, newlineIdx);
569
+ this.buffer = this.buffer.slice(newlineIdx + 1);
570
+ if (!line.trim())
571
+ continue;
572
+ try {
573
+ const raw = JSON.parse(line);
574
+ const pending = this.pendingRequests.get(raw.id);
575
+ if (pending) {
576
+ this.pendingRequests.delete(raw.id);
577
+ clearTimeout(pending.timer);
578
+ try {
579
+ const result = parseResponse(raw);
580
+ pending.resolve(result);
581
+ }
582
+ catch (err) {
583
+ pending.reject(err);
584
+ }
585
+ }
586
+ }
587
+ catch {
588
+ // Malformed response — ignore
589
+ }
590
+ }
591
+ }
592
+ readDaemonInfo() {
593
+ try {
594
+ if (!existsSync(DAEMON_JSON_PATH))
595
+ return null;
596
+ const raw = readFileSync(DAEMON_JSON_PATH, 'utf-8');
597
+ return JSON.parse(raw);
598
+ }
599
+ catch {
600
+ return null;
601
+ }
602
+ }
603
+ isProcessAlive(pid) {
604
+ try {
605
+ process.kill(pid, 0);
606
+ return true;
607
+ }
608
+ catch {
609
+ return false;
610
+ }
611
+ }
612
+ cleanupStaleFiles() {
613
+ // Named pipes are kernel objects — no file to unlink
614
+ if (!IS_NAMED_PIPE) {
615
+ try {
616
+ unlinkSync(SOCKET_PATH);
617
+ }
618
+ catch { /* ok */ }
619
+ }
620
+ try {
621
+ unlinkSync(DAEMON_JSON_PATH);
622
+ }
623
+ catch { /* ok */ }
624
+ }
625
+ isRunning() {
626
+ return this.socket !== null && !this.socket.destroyed;
627
+ }
628
+ daemonPid() {
629
+ const info = this.readDaemonInfo();
630
+ return info?.pid ?? null;
631
+ }
632
+ async disconnect() {
633
+ // Disconnect all CDP clients
634
+ for (const [pid, client] of this.cdpClients) {
635
+ try {
636
+ await client.disconnect();
637
+ }
638
+ catch { /* ok */ }
639
+ }
640
+ this.cdpClients.clear();
641
+ this.grabbedAppInfo = null;
642
+ if (this.socket && !this.socket.destroyed) {
643
+ this.socket.destroy();
644
+ this.socket = null;
645
+ }
646
+ }
647
+ async shutdown() {
648
+ // Disconnect all CDP clients first
649
+ for (const [pid, client] of this.cdpClients) {
650
+ try {
651
+ await client.disconnect();
652
+ }
653
+ catch { /* ok */ }
654
+ }
655
+ this.cdpClients.clear();
656
+ this.grabbedAppInfo = null;
657
+ if (this.socket && !this.socket.destroyed) {
658
+ try {
659
+ await this.sendToNative('shutdown', {});
660
+ }
661
+ catch { /* ok — socket may close before response */ }
662
+ await sleep(100);
663
+ }
664
+ this.socket?.destroy();
665
+ this.socket = null;
666
+ }
667
+ // Kill the daemon process directly (for testing crash recovery)
668
+ _killDaemonProcess() {
669
+ const info = this.readDaemonInfo();
670
+ if (info) {
671
+ try {
672
+ process.kill(info.pid, 'SIGKILL');
673
+ }
674
+ catch { /* ok */ }
675
+ }
676
+ this.socket?.destroy();
677
+ this.socket = null;
678
+ }
679
+ // Send raw data directly to socket (for testing malformed input handling)
680
+ _sendRawToSocket(data) {
681
+ this.socket?.write(data);
682
+ }
683
+ }
684
+ function sleep(ms) {
685
+ return new Promise((resolve) => setTimeout(resolve, ms));
686
+ }
687
+ //# sourceMappingURL=bridge.js.map