@skillful-agents/agent-computer 0.0.3 → 0.0.5

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