@amaster.ai/pi-computer-use 0.1.2-beta.8 → 0.1.2-beta.80

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 (60) hide show
  1. package/README.md +80 -85
  2. package/dist/config.d.ts +10 -0
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +3 -2
  5. package/dist/config.js.map +1 -1
  6. package/dist/generated/cua-driver-tools.d.ts +4814 -0
  7. package/dist/generated/cua-driver-tools.d.ts.map +1 -0
  8. package/dist/generated/cua-driver-tools.js +6988 -0
  9. package/dist/generated/cua-driver-tools.js.map +1 -0
  10. package/dist/index.d.ts +1 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +445 -425
  13. package/dist/index.js.map +1 -1
  14. package/dist/mcp-client.d.ts +55 -14
  15. package/dist/mcp-client.d.ts.map +1 -1
  16. package/dist/mcp-client.js +566 -62
  17. package/dist/mcp-client.js.map +1 -1
  18. package/dist/tool-groups.d.ts +16 -0
  19. package/dist/tool-groups.d.ts.map +1 -0
  20. package/dist/tool-groups.js +96 -0
  21. package/dist/tool-groups.js.map +1 -0
  22. package/dist/tool-result.d.ts +26 -0
  23. package/dist/tool-result.d.ts.map +1 -0
  24. package/dist/tool-result.js +103 -0
  25. package/dist/tool-result.js.map +1 -0
  26. package/dist/vision.d.ts +1 -1
  27. package/dist/vision.d.ts.map +1 -1
  28. package/dist/vision.js +24 -3
  29. package/dist/vision.js.map +1 -1
  30. package/package.json +18 -12
  31. package/preview.png +0 -0
  32. package/bin/darwin-arm64/.version +0 -2
  33. package/bin/darwin-arm64/CuaDriver.app/Contents/CodeResources +0 -0
  34. package/bin/darwin-arm64/CuaDriver.app/Contents/Info.plist +0 -32
  35. package/bin/darwin-arm64/CuaDriver.app/Contents/MacOS/cua-driver +0 -0
  36. package/bin/darwin-arm64/CuaDriver.app/Contents/Resources/Skills/cua-driver/README.md +0 -140
  37. package/bin/darwin-arm64/CuaDriver.app/Contents/Resources/Skills/cua-driver/RECORDING.md +0 -113
  38. package/bin/darwin-arm64/CuaDriver.app/Contents/Resources/Skills/cua-driver/SKILL.md +0 -887
  39. package/bin/darwin-arm64/CuaDriver.app/Contents/Resources/Skills/cua-driver/TESTS.md +0 -232
  40. package/bin/darwin-arm64/CuaDriver.app/Contents/Resources/Skills/cua-driver/WEB_APPS.md +0 -471
  41. package/bin/darwin-arm64/CuaDriver.app/Contents/_CodeSignature/CodeResources +0 -172
  42. package/bin/darwin-x64/.version +0 -2
  43. package/bin/darwin-x64/CuaDriver.app/Contents/CodeResources +0 -0
  44. package/bin/darwin-x64/CuaDriver.app/Contents/Info.plist +0 -32
  45. package/bin/darwin-x64/CuaDriver.app/Contents/MacOS/cua-driver +0 -0
  46. package/bin/darwin-x64/CuaDriver.app/Contents/Resources/Skills/cua-driver/README.md +0 -140
  47. package/bin/darwin-x64/CuaDriver.app/Contents/Resources/Skills/cua-driver/RECORDING.md +0 -113
  48. package/bin/darwin-x64/CuaDriver.app/Contents/Resources/Skills/cua-driver/SKILL.md +0 -887
  49. package/bin/darwin-x64/CuaDriver.app/Contents/Resources/Skills/cua-driver/TESTS.md +0 -232
  50. package/bin/darwin-x64/CuaDriver.app/Contents/Resources/Skills/cua-driver/WEB_APPS.md +0 -471
  51. package/bin/darwin-x64/CuaDriver.app/Contents/_CodeSignature/CodeResources +0 -172
  52. package/bin/linux-x64/.version +0 -2
  53. package/bin/linux-x64/cua-driver +0 -0
  54. package/bin/win32-arm64/.version +0 -2
  55. package/bin/win32-arm64/cua-driver-uia.exe +0 -0
  56. package/bin/win32-arm64/cua-driver.exe +0 -0
  57. package/bin/win32-x64/.version +0 -2
  58. package/bin/win32-x64/cua-driver-uia.exe +0 -0
  59. package/bin/win32-x64/cua-driver.exe +0 -0
  60. package/scripts/postinstall.js +0 -29
@@ -1,91 +1,595 @@
1
+ import { execFile, spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
1
3
  import { accessSync, chmodSync, constants, existsSync } from 'node:fs';
4
+ import { createRequire } from 'node:module';
5
+ import { tmpdir } from 'node:os';
2
6
  import path from 'node:path';
3
- import { fileURLToPath } from 'node:url';
7
+ import { promisify } from 'node:util';
4
8
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
5
- import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
9
+ import { getDefaultEnvironment, StdioClientTransport, } from '@modelcontextprotocol/sdk/client/stdio.js';
10
+ import { AjvJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/ajv';
6
11
  const MCP_TIMEOUT_MS = 60_000;
7
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+ const DAEMON_START_TIMEOUT_MS = 10_000;
13
+ const PERMANENT_STATUS_ERROR_CODES = new Set(['EACCES', 'ENOENT', 'EPERM']);
14
+ const DAEMON_LEASE_SCRIPT = `
15
+ if IFS= read -r _; then exit 0; fi
16
+ attempt=0
17
+ while [ "$attempt" -lt 100 ]; do
18
+ if [ -S "$2" ]; then
19
+ "$1" stop --socket "$2" >/dev/null 2>&1
20
+ status=$("$1" status --socket "$2" 2>&1)
21
+ if [ -S "$2" ] && [ "$status" = "Cua Driver daemon is not running" ]; then
22
+ /bin/rm -f -- "$2"
23
+ fi
24
+ exit 0
25
+ fi
26
+ attempt=$((attempt + 1))
27
+ sleep 0.1
28
+ done
29
+ `;
30
+ const execFileAsync = promisify(execFile);
31
+ const localRequire = createRequire(import.meta.url);
32
+ function requestOptions(signal) {
33
+ return signal ? { signal, timeout: MCP_TIMEOUT_MS } : { timeout: MCP_TIMEOUT_MS };
34
+ }
35
+ function abortError(signal) {
36
+ if (signal?.reason instanceof Error)
37
+ return signal.reason;
38
+ return new Error('Cua Driver operation aborted.');
39
+ }
40
+ export async function waitForPromise(promise, signal) {
41
+ if (!signal)
42
+ return promise;
43
+ if (signal.aborted)
44
+ throw abortError(signal);
45
+ return new Promise((resolve, reject) => {
46
+ const onAbort = () => reject(abortError(signal));
47
+ signal.addEventListener('abort', onAbort, { once: true });
48
+ void promise.then((value) => {
49
+ signal.removeEventListener('abort', onAbort);
50
+ resolve(value);
51
+ }, (error) => {
52
+ signal.removeEventListener('abort', onAbort);
53
+ reject(error);
54
+ });
55
+ });
56
+ }
57
+ function delay(milliseconds, signal) {
58
+ return new Promise((resolve, reject) => {
59
+ if (signal?.aborted) {
60
+ reject(abortError(signal));
61
+ return;
62
+ }
63
+ const onAbort = () => {
64
+ clearTimeout(timeout);
65
+ reject(abortError(signal));
66
+ };
67
+ const timeout = setTimeout(() => {
68
+ signal?.removeEventListener('abort', onAbort);
69
+ resolve();
70
+ }, milliseconds);
71
+ signal?.addEventListener('abort', onAbort, { once: true });
72
+ });
73
+ }
74
+ export function resolveBundledTarget(platform, arch) {
75
+ if (platform === 'darwin')
76
+ return 'darwin-universal';
77
+ return `${platform}-${arch}`;
78
+ }
79
+ function resolveDriverPackageRoot(platform, arch) {
80
+ const target = resolveBundledTarget(platform, arch);
81
+ const packageName = `@amaster.ai/pi-computer-use-cua-driver-${target}`;
82
+ try {
83
+ return path.dirname(localRequire.resolve(`${packageName}/package.json`));
84
+ }
85
+ catch {
86
+ throw new Error(`Cua Driver runtime package is not installed for ${target}.`);
87
+ }
88
+ }
89
+ export function resolveUnixSocketPath(tempDir, suffix) {
90
+ const filename = `pi-cua-${suffix}.sock`;
91
+ const configuredTmpPath = path.join(tempDir, filename);
92
+ // sockaddr_un.sun_path is only 104 bytes on macOS and 108 bytes on Linux.
93
+ return Buffer.byteLength(configuredTmpPath) <= 90
94
+ ? configuredTmpPath
95
+ : path.join('/tmp', filename);
96
+ }
97
+ export async function waitForDaemonStatus(binaryPath, socket, signal, checkStatus = () => execFileAsync(binaryPath, ['status', '--socket', socket], {
98
+ signal,
99
+ timeout: 250,
100
+ windowsHide: true,
101
+ })) {
102
+ const deadline = Date.now() + DAEMON_START_TIMEOUT_MS;
103
+ let lastError;
104
+ while (Date.now() < deadline) {
105
+ try {
106
+ await checkStatus();
107
+ return;
108
+ }
109
+ catch (error) {
110
+ if (signal?.aborted)
111
+ throw abortError(signal);
112
+ lastError = error;
113
+ const code = error && typeof error === 'object' && 'code' in error && typeof error.code === 'string'
114
+ ? error.code
115
+ : undefined;
116
+ if (code && PERMANENT_STATUS_ERROR_CODES.has(code)) {
117
+ throw new Error(`Cua Driver status check failed (${code}).`, { cause: error });
118
+ }
119
+ }
120
+ await delay(50, signal);
121
+ }
122
+ throw new Error('Cua Driver daemon did not respond to a status check.', { cause: lastError });
123
+ }
124
+ export function resolveDriverLayout(config, rootDir = undefined, platform = process.platform, arch = process.arch) {
125
+ if (config.mode === 'path' && config.binaryPath) {
126
+ return { binaryPath: config.binaryPath, embedded: platform === 'darwin' };
127
+ }
128
+ const binDir = rootDir
129
+ ? path.join(rootDir, 'bin', resolveBundledTarget(platform, arch))
130
+ : path.join(resolveDriverPackageRoot(platform, arch), 'bin');
131
+ if (platform === 'darwin') {
132
+ const appPath = path.join(binDir, 'CuaDriver.app');
133
+ return {
134
+ appPath,
135
+ binaryPath: path.join(appPath, 'Contents', 'MacOS', 'cua-driver'),
136
+ embedded: false,
137
+ };
138
+ }
139
+ return {
140
+ binaryPath: path.join(binDir, platform === 'win32' ? 'cua-driver.exe' : 'cua-driver'),
141
+ embedded: false,
142
+ };
143
+ }
144
+ /** schemars emits these non-standard `format` values for Rust integer types; ajv warns on each. */
145
+ const INTEGER_FORMAT_BOUNDS = {
146
+ int8: { minimum: -128, maximum: 127 },
147
+ int16: { minimum: -32_768, maximum: 32_767 },
148
+ int32: { minimum: -2_147_483_648, maximum: 2_147_483_647 },
149
+ int64: {},
150
+ uint8: { minimum: 0, maximum: 255 },
151
+ uint16: { minimum: 0, maximum: 65_535 },
152
+ uint32: { minimum: 0, maximum: 4_294_967_295 },
153
+ uint64: { minimum: 0 },
154
+ };
155
+ /** Schema keys whose values are instance data, not subschemas — never rewrite inside them. */
156
+ const NON_SCHEMA_KEYS = new Set(['const', 'default', 'enum', 'examples']);
157
+ /**
158
+ * Keywords whose values are maps of property name → subschema. Inside these maps the
159
+ * keys are property names, not schema keywords, so NON_SCHEMA_KEYS must not apply:
160
+ * a property literally named "default" or "enum" still needs its subschema sanitized.
161
+ */
162
+ const PROPERTY_MAP_KEYS = new Set([
163
+ 'properties',
164
+ 'patternProperties',
165
+ 'dependentSchemas',
166
+ '$defs',
167
+ 'definitions',
168
+ ]);
169
+ /**
170
+ * Returns a copy of the schema with schemars' non-standard integer `format`
171
+ * annotations ("uint64", ...) replaced by standard constraints ajv can enforce.
172
+ * The input schema is not modified.
173
+ */
174
+ export function sanitizeSchemaFormats(schema) {
175
+ return sanitizeSchemaNode(schema);
176
+ }
177
+ function sanitizeSchemaNode(node) {
178
+ if (Array.isArray(node))
179
+ return node.map(sanitizeSchemaNode);
180
+ if (node === null || typeof node !== 'object')
181
+ return node;
182
+ // Null-prototype copy: a schema from the wire may carry an own "__proto__"
183
+ // key (JSON.parse creates one), and assigning it on a plain object would hit
184
+ // the inherited setter — dropping the key and replacing the copy's prototype.
185
+ const result = Object.create(null);
186
+ for (const [key, value] of Object.entries(node)) {
187
+ result[key] = sanitizeSchemaValue(key, value);
188
+ }
189
+ const format = result.format;
190
+ if (typeof format !== 'string')
191
+ return result;
192
+ // Own-property check: a plain table lookup would match Object.prototype keys
193
+ // ("constructor", "toString", ...) on hostile or malformed schemas.
194
+ const bounds = Object.hasOwn(INTEGER_FORMAT_BOUNDS, format)
195
+ ? INTEGER_FORMAT_BOUNDS[format]
196
+ : undefined;
197
+ if (!bounds)
198
+ return result;
199
+ // Only rewrite integer schemas; injecting numeric bounds into a node that
200
+ // declares another type would misannotate it.
201
+ if (!isIntegerType(result.type))
202
+ return result;
203
+ delete result.format;
204
+ result.type ??= 'integer';
205
+ if (bounds.minimum !== undefined && result.minimum === undefined) {
206
+ result.minimum = bounds.minimum;
207
+ }
208
+ if (bounds.maximum !== undefined && result.maximum === undefined) {
209
+ result.maximum = bounds.maximum;
210
+ }
211
+ return result;
212
+ }
213
+ function sanitizeSchemaValue(key, value) {
214
+ if (NON_SCHEMA_KEYS.has(key))
215
+ return value;
216
+ if (PROPERTY_MAP_KEYS.has(key) && isRecord(value)) {
217
+ const map = Object.create(null);
218
+ for (const [name, subschema] of Object.entries(value)) {
219
+ map[name] = sanitizeSchemaNode(subschema);
220
+ }
221
+ return map;
222
+ }
223
+ return sanitizeSchemaNode(value);
224
+ }
225
+ function isRecord(value) {
226
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
227
+ }
228
+ function isIntegerType(type) {
229
+ if (type === undefined)
230
+ return true;
231
+ if (Array.isArray(type))
232
+ return type.includes('integer');
233
+ return type === 'integer';
234
+ }
235
+ /**
236
+ * Wraps the SDK's default ajv validator, sanitizing non-standard integer formats
237
+ * before compilation so ajv stays silent and the integer bounds stay enforced.
238
+ */
239
+ export class SanitizingJsonSchemaValidator {
240
+ inner = new AjvJsonSchemaValidator();
241
+ getValidator(schema) {
242
+ return this.inner.getValidator(sanitizeSchemaFormats(schema));
243
+ }
244
+ }
245
+ /** Owns the Cua Driver daemon and its stdio MCP proxy as one adapter. */
8
246
  export class CuaDriverClient {
9
- client = null;
10
- transport = null;
11
247
  config;
248
+ client = null;
249
+ daemonProcess = null;
250
+ daemonLease = null;
251
+ daemonSocket = null;
252
+ activeLayout = null;
253
+ state = 'disconnected';
254
+ connectPromise = null;
255
+ lifecycleController = new AbortController();
256
+ generation = 0;
257
+ hasConnected = false;
258
+ explicitlyClosed = false;
12
259
  constructor(config) {
13
260
  this.config = config;
14
261
  }
15
- async connect() {
16
- const binaryPath = await this.resolveBinaryPath();
17
- const args = ['mcp', ...(this.config.extraArgs ?? [])];
18
- this.transport = new StdioClientTransport({
19
- command: binaryPath,
20
- args,
21
- stderr: 'pipe',
22
- });
23
- this.transport.onerror = (error) => {
24
- console.error(`[pi-computer-use] cua-driver transport error: ${error.message}`);
25
- };
26
- this.client = new Client({ name: 'pi-computer-use', version: '0.1.0' }, { capabilities: {} });
27
- await this.client.connect(this.transport);
262
+ getState() {
263
+ return this.state;
28
264
  }
29
- async listAllTools() {
30
- if (!this.client)
31
- throw new Error('CuaDriverClient not connected');
32
- const allTools = [];
33
- let cursor;
34
- do {
35
- const result = await this.client.listTools(cursor ? { cursor } : undefined, {
36
- timeout: MCP_TIMEOUT_MS,
265
+ async connect(signal) {
266
+ if (this.explicitlyClosed)
267
+ throw new Error('Cua Driver client is closed.');
268
+ if (this.state === 'ready')
269
+ return;
270
+ if (!this.connectPromise) {
271
+ const connection = this.openConnection(this.lifecycleController.signal);
272
+ this.connectPromise = connection;
273
+ const clearConnection = () => {
274
+ if (this.connectPromise === connection)
275
+ this.connectPromise = null;
276
+ };
277
+ void connection.then(clearConnection, clearConnection);
278
+ }
279
+ await waitForPromise(this.connectPromise, signal);
280
+ }
281
+ async openConnection(signal) {
282
+ this.state = this.hasConnected ? 'reconnecting' : 'connecting';
283
+ const generation = ++this.generation;
284
+ let client = null;
285
+ try {
286
+ const layout = resolveDriverLayout(this.config);
287
+ this.ensureExecutable(layout.binaryPath);
288
+ this.activeLayout = layout;
289
+ const socket = await this.ensureDaemon(layout, signal);
290
+ const mcpArgs = [
291
+ 'mcp',
292
+ ...(layout.embedded ? ['--embedded'] : []),
293
+ '--socket',
294
+ socket,
295
+ ...(this.config.extraArgs ?? []),
296
+ ];
297
+ const env = layout.embedded
298
+ ? { ...getDefaultEnvironment(), CUA_DRIVER_EMBEDDED: '1' }
299
+ : getDefaultEnvironment();
300
+ const transport = new StdioClientTransport({
301
+ command: layout.binaryPath,
302
+ args: mcpArgs,
303
+ env,
304
+ stderr: 'pipe',
37
305
  });
38
- allTools.push(...result.tools);
39
- cursor = result.nextCursor;
40
- } while (cursor);
41
- return allTools;
306
+ client = new Client({ name: 'pi-computer-use', version: '0.2.0' }, { capabilities: {}, jsonSchemaValidator: new SanitizingJsonSchemaValidator() });
307
+ this.client = client;
308
+ transport.onerror = (error) => {
309
+ if (generation !== this.generation)
310
+ return;
311
+ console.error(`[pi-computer-use] cua-driver transport error (${error.name})`);
312
+ void this.disconnectUnhealthyClient(generation);
313
+ };
314
+ transport.onclose = () => void this.disconnectUnhealthyClient(generation);
315
+ await client.connect(transport, requestOptions(signal));
316
+ if (generation !== this.generation)
317
+ return;
318
+ this.state = 'ready';
319
+ this.hasConnected = true;
320
+ }
321
+ catch (error) {
322
+ if (generation === this.generation) {
323
+ ++this.generation;
324
+ this.client = null;
325
+ this.state = 'failed';
326
+ }
327
+ try {
328
+ await client?.close();
329
+ }
330
+ catch {
331
+ // A failed transport may already be closed.
332
+ }
333
+ await this.stopDaemon();
334
+ if (signal?.aborted)
335
+ throw abortError(signal);
336
+ const errorName = error instanceof Error ? error.name : 'UnknownError';
337
+ console.error(`[pi-computer-use] cua-driver connection failed (${errorName})`);
338
+ if (process.env.DEBUG?.includes('pi-computer-use')) {
339
+ const message = error instanceof Error ? error.message : String(error);
340
+ console.error(`[pi-computer-use] connection detail: ${message}`);
341
+ }
342
+ throw new Error('Cua Driver connection failed.');
343
+ }
42
344
  }
43
- async callTool(name, args) {
44
- if (!this.client)
45
- throw new Error('CuaDriverClient not connected');
46
- return (await this.client.callTool({ name, arguments: args }, undefined, {
47
- timeout: MCP_TIMEOUT_MS,
48
- }));
345
+ createSocketPath() {
346
+ const suffix = `${process.pid}-${randomUUID().slice(0, 8)}`;
347
+ return process.platform === 'win32'
348
+ ? `\\\\.\\pipe\\pi-computer-use-${suffix}`
349
+ : resolveUnixSocketPath(tmpdir(), suffix);
49
350
  }
50
- async close() {
51
- if (this.client) {
52
- await this.client.close();
53
- this.client = null;
351
+ daemonEnvironment(embedded) {
352
+ return embedded ? { ...process.env, CUA_DRIVER_EMBEDDED: '1' } : process.env;
353
+ }
354
+ async ensureDaemon(layout, signal) {
355
+ if (this.daemonSocket)
356
+ return this.daemonSocket;
357
+ const socket = this.createSocketPath();
358
+ this.daemonSocket = socket;
359
+ if (process.platform !== 'win32') {
360
+ // This anonymous pipe is the lease: owner death closes it even after SIGKILL.
361
+ const lease = spawn('/bin/sh', ['-c', DAEMON_LEASE_SCRIPT, 'pi-computer-use-lease', layout.binaryPath, socket], {
362
+ env: this.daemonEnvironment(layout.embedded),
363
+ stdio: ['pipe', 'ignore', 'ignore'],
364
+ });
365
+ this.daemonLease = lease;
366
+ await new Promise((resolve, reject) => {
367
+ const onError = (error) => {
368
+ lease.removeListener('spawn', onSpawn);
369
+ reject(error);
370
+ };
371
+ const onSpawn = () => {
372
+ lease.removeListener('error', onError);
373
+ resolve();
374
+ };
375
+ lease.once('error', onError);
376
+ lease.once('spawn', onSpawn);
377
+ });
378
+ lease.unref?.();
379
+ lease.stdin?.unref?.();
380
+ lease.once('error', () => {
381
+ if (this.daemonLease !== lease)
382
+ return;
383
+ console.error('[pi-computer-use] failed to start daemon lease');
384
+ void this.stopDaemon();
385
+ });
386
+ }
387
+ if (process.platform === 'darwin' && layout.appPath && !layout.embedded) {
388
+ const launcher = spawn('/usr/bin/open', ['-n', '-g', layout.appPath, '--args', 'serve', '--socket', socket, '--no-overlay'], { stdio: 'ignore' });
389
+ await this.waitForLauncher(launcher, signal);
390
+ await this.waitForSocket(socket, signal);
391
+ return socket;
54
392
  }
55
- this.transport = null;
393
+ const args = [
394
+ 'serve',
395
+ ...(layout.embedded ? ['--embedded'] : []),
396
+ '--socket',
397
+ socket,
398
+ '--no-overlay',
399
+ ];
400
+ const daemon = spawn(layout.binaryPath, args, {
401
+ env: this.daemonEnvironment(layout.embedded),
402
+ stdio: ['ignore', 'ignore', 'pipe'],
403
+ });
404
+ this.daemonProcess = daemon;
405
+ await this.waitForDaemonReady(daemon, socket, signal);
406
+ if (process.platform === 'win32') {
407
+ await waitForDaemonStatus(layout.binaryPath, socket, signal);
408
+ }
409
+ return socket;
410
+ }
411
+ waitForLauncher(child, signal) {
412
+ return new Promise((resolve, reject) => {
413
+ const timeout = setTimeout(() => reject(new Error('Cua Driver LaunchServices startup timed out.')), DAEMON_START_TIMEOUT_MS);
414
+ const finish = (error) => {
415
+ clearTimeout(timeout);
416
+ signal?.removeEventListener('abort', onAbort);
417
+ error ? reject(error) : resolve();
418
+ };
419
+ const onAbort = () => {
420
+ child.kill();
421
+ finish(abortError(signal));
422
+ };
423
+ signal?.addEventListener('abort', onAbort, { once: true });
424
+ child.once('error', finish);
425
+ child.once('exit', (code) => code === 0 ? finish() : finish(new Error(`LaunchServices exited with code ${code ?? -1}.`)));
426
+ });
56
427
  }
57
- async resolveBinaryPath() {
58
- if (this.config.mode === 'path' && this.config.binaryPath) {
59
- return this.config.binaryPath;
428
+ async waitForSocket(socket, signal) {
429
+ const deadline = Date.now() + DAEMON_START_TIMEOUT_MS;
430
+ while (Date.now() < deadline) {
431
+ if (existsSync(socket))
432
+ return;
433
+ await delay(50, signal);
60
434
  }
61
- const platform = process.platform === 'win32' ? 'win32-x64' : `${process.platform}-${process.arch}`;
62
- const binDir = path.resolve(__dirname, '..', 'bin', platform);
63
- let binPath;
64
- if (process.platform === 'darwin') {
65
- binPath = path.join(binDir, 'CuaDriver.app', 'Contents', 'MacOS', 'cua-driver');
435
+ throw new Error('Cua Driver daemon socket did not become ready.');
436
+ }
437
+ waitForDaemonReady(daemon, socket, signal) {
438
+ return new Promise((resolve, reject) => {
439
+ let stderr = '';
440
+ const timeout = setTimeout(() => finish(new Error('Cua Driver daemon startup timed out.')), DAEMON_START_TIMEOUT_MS);
441
+ const finish = (error) => {
442
+ clearTimeout(timeout);
443
+ signal?.removeEventListener('abort', onAbort);
444
+ daemon.stderr?.removeListener('data', onData);
445
+ error ? reject(error) : resolve();
446
+ };
447
+ const onAbort = () => {
448
+ daemon.kill();
449
+ finish(abortError(signal));
450
+ };
451
+ const onData = (chunk) => {
452
+ stderr = `${stderr}${chunk.toString()}`.slice(-4_096);
453
+ if (stderr.includes('daemon listening on'))
454
+ finish();
455
+ };
456
+ signal?.addEventListener('abort', onAbort, { once: true });
457
+ daemon.stderr?.on('data', onData);
458
+ daemon.once('error', finish);
459
+ daemon.once('exit', (code) => {
460
+ if (this.state !== 'closing') {
461
+ const detail = process.env.DEBUG?.includes('pi-computer-use') ? `: ${stderr.trim()}` : '';
462
+ finish(new Error(`Cua Driver daemon exited with code ${code ?? -1}${detail}.`));
463
+ }
464
+ });
465
+ if (process.platform !== 'win32' && existsSync(socket))
466
+ finish();
467
+ });
468
+ }
469
+ ensureExecutable(binaryPath) {
470
+ if (!existsSync(binaryPath)) {
471
+ throw new Error(`cua-driver binary not found for ${resolveBundledTarget(process.platform, process.arch)}.`);
66
472
  }
67
- else {
68
- const ext = process.platform === 'win32' ? '.exe' : '';
69
- binPath = path.join(binDir, `cua-driver${ext}`);
473
+ if (process.platform === 'win32')
474
+ return;
475
+ try {
476
+ accessSync(binaryPath, constants.X_OK);
70
477
  }
71
- if (!existsSync(binPath)) {
72
- throw new Error(`[pi-computer-use] cua-driver binary not found at ${binPath}. ` +
73
- `Platform "${platform}" may not be supported, or set mode: "path" with binaryPath in config.`);
478
+ catch {
479
+ try {
480
+ chmodSync(binaryPath, 0o755);
481
+ }
482
+ catch {
483
+ throw new Error('cua-driver binary is not executable.');
484
+ }
74
485
  }
486
+ }
487
+ markDisconnected(generation) {
488
+ if (generation !== this.generation || this.state === 'closing')
489
+ return;
490
+ ++this.generation;
491
+ this.client = null;
492
+ this.state = 'disconnected';
493
+ }
494
+ async disconnectUnhealthyClient(generation) {
495
+ if (generation !== this.generation || this.state === 'closing')
496
+ return;
497
+ const failedClient = this.client;
498
+ this.markDisconnected(generation);
499
+ // Clear owned-daemon state before awaiting the MCP close so an immediate retry
500
+ // cannot attach to a socket that is in the process of being torn down.
501
+ const stopPromise = this.stopDaemon();
502
+ try {
503
+ await failedClient?.close();
504
+ }
505
+ catch {
506
+ console.error('[pi-computer-use] failed to close unhealthy MCP client');
507
+ }
508
+ finally {
509
+ await stopPromise;
510
+ }
511
+ }
512
+ async ensureReady(signal) {
513
+ if (this.explicitlyClosed)
514
+ throw new Error('Cua Driver client is closed.');
515
+ if (this.state === 'ready' && this.client)
516
+ return;
517
+ await this.connect(signal);
518
+ }
519
+ async listAllTools(signal) {
520
+ await this.ensureReady(signal);
521
+ const client = this.client;
522
+ if (!client)
523
+ throw new Error('Cua Driver client not connected.');
524
+ const allTools = [];
525
+ let cursor;
526
+ do {
527
+ const result = await client.listTools(cursor ? { cursor } : undefined, requestOptions(signal));
528
+ allTools.push(...result.tools);
529
+ cursor = result.nextCursor;
530
+ } while (cursor);
531
+ return allTools;
532
+ }
533
+ async callTool(name, args, signal) {
534
+ await this.ensureReady(signal);
535
+ const client = this.client;
536
+ if (!client)
537
+ throw new Error('Cua Driver client not connected.');
75
538
  try {
76
- accessSync(binPath, constants.X_OK);
539
+ return (await client.callTool({ name, arguments: args }, undefined, requestOptions(signal)));
77
540
  }
78
541
  catch {
542
+ if (signal?.aborted)
543
+ throw abortError(signal);
544
+ await this.disconnectUnhealthyClient(this.generation);
545
+ throw new Error('Cua Driver connection lost; retry the tool.');
546
+ }
547
+ }
548
+ async stopDaemon() {
549
+ const socket = this.daemonSocket;
550
+ const layout = this.activeLayout;
551
+ const daemonProcess = this.daemonProcess;
552
+ const daemonLease = this.daemonLease;
553
+ this.daemonSocket = null;
554
+ this.activeLayout = null;
555
+ this.daemonProcess = null;
556
+ this.daemonLease = null;
557
+ if (daemonLease?.stdin?.writable)
558
+ daemonLease.stdin.end('cancel\n');
559
+ else
560
+ daemonLease?.kill();
561
+ if (socket && layout) {
79
562
  try {
80
- chmodSync(binPath, 0o755);
563
+ await execFileAsync(layout.binaryPath, ['stop', '--socket', socket], {
564
+ env: this.daemonEnvironment(layout.embedded),
565
+ timeout: 3_000,
566
+ });
81
567
  }
82
- catch (chmodErr) {
83
- throw new Error(`[pi-computer-use] cua-driver binary at ${binPath} is not executable. ` +
84
- `Auto-fix failed (${chmodErr instanceof Error ? chmodErr.message : String(chmodErr)}). ` +
85
- `Run: sudo chmod +x "${binPath}"`);
568
+ catch {
569
+ console.error('[pi-computer-use] failed to stop owned daemon');
86
570
  }
87
571
  }
88
- return binPath;
572
+ daemonProcess?.kill();
573
+ }
574
+ async close() {
575
+ if (this.explicitlyClosed)
576
+ return;
577
+ this.explicitlyClosed = true;
578
+ this.state = 'closing';
579
+ const connection = this.connectPromise;
580
+ this.lifecycleController.abort(new Error('Cua Driver client is closing.'));
581
+ if (connection)
582
+ await Promise.allSettled([connection]);
583
+ const client = this.client;
584
+ ++this.generation;
585
+ this.client = null;
586
+ try {
587
+ await client?.close();
588
+ }
589
+ finally {
590
+ await this.stopDaemon();
591
+ this.state = 'disconnected';
592
+ }
89
593
  }
90
594
  }
91
595
  //# sourceMappingURL=mcp-client.js.map