@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.
- package/README.md +80 -85
- package/dist/config.d.ts +10 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -2
- package/dist/config.js.map +1 -1
- package/dist/generated/cua-driver-tools.d.ts +4814 -0
- package/dist/generated/cua-driver-tools.d.ts.map +1 -0
- package/dist/generated/cua-driver-tools.js +6988 -0
- package/dist/generated/cua-driver-tools.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +445 -425
- package/dist/index.js.map +1 -1
- package/dist/mcp-client.d.ts +55 -14
- package/dist/mcp-client.d.ts.map +1 -1
- package/dist/mcp-client.js +566 -62
- package/dist/mcp-client.js.map +1 -1
- package/dist/tool-groups.d.ts +16 -0
- package/dist/tool-groups.d.ts.map +1 -0
- package/dist/tool-groups.js +96 -0
- package/dist/tool-groups.js.map +1 -0
- package/dist/tool-result.d.ts +26 -0
- package/dist/tool-result.d.ts.map +1 -0
- package/dist/tool-result.js +103 -0
- package/dist/tool-result.js.map +1 -0
- package/dist/vision.d.ts +1 -1
- package/dist/vision.d.ts.map +1 -1
- package/dist/vision.js +24 -3
- package/dist/vision.js.map +1 -1
- package/package.json +18 -12
- package/preview.png +0 -0
- package/bin/darwin-arm64/.version +0 -2
- package/bin/darwin-arm64/CuaDriver.app/Contents/CodeResources +0 -0
- package/bin/darwin-arm64/CuaDriver.app/Contents/Info.plist +0 -32
- package/bin/darwin-arm64/CuaDriver.app/Contents/MacOS/cua-driver +0 -0
- package/bin/darwin-arm64/CuaDriver.app/Contents/Resources/Skills/cua-driver/README.md +0 -140
- package/bin/darwin-arm64/CuaDriver.app/Contents/Resources/Skills/cua-driver/RECORDING.md +0 -113
- package/bin/darwin-arm64/CuaDriver.app/Contents/Resources/Skills/cua-driver/SKILL.md +0 -887
- package/bin/darwin-arm64/CuaDriver.app/Contents/Resources/Skills/cua-driver/TESTS.md +0 -232
- package/bin/darwin-arm64/CuaDriver.app/Contents/Resources/Skills/cua-driver/WEB_APPS.md +0 -471
- package/bin/darwin-arm64/CuaDriver.app/Contents/_CodeSignature/CodeResources +0 -172
- package/bin/darwin-x64/.version +0 -2
- package/bin/darwin-x64/CuaDriver.app/Contents/CodeResources +0 -0
- package/bin/darwin-x64/CuaDriver.app/Contents/Info.plist +0 -32
- package/bin/darwin-x64/CuaDriver.app/Contents/MacOS/cua-driver +0 -0
- package/bin/darwin-x64/CuaDriver.app/Contents/Resources/Skills/cua-driver/README.md +0 -140
- package/bin/darwin-x64/CuaDriver.app/Contents/Resources/Skills/cua-driver/RECORDING.md +0 -113
- package/bin/darwin-x64/CuaDriver.app/Contents/Resources/Skills/cua-driver/SKILL.md +0 -887
- package/bin/darwin-x64/CuaDriver.app/Contents/Resources/Skills/cua-driver/TESTS.md +0 -232
- package/bin/darwin-x64/CuaDriver.app/Contents/Resources/Skills/cua-driver/WEB_APPS.md +0 -471
- package/bin/darwin-x64/CuaDriver.app/Contents/_CodeSignature/CodeResources +0 -172
- package/bin/linux-x64/.version +0 -2
- package/bin/linux-x64/cua-driver +0 -0
- package/bin/win32-arm64/.version +0 -2
- package/bin/win32-arm64/cua-driver-uia.exe +0 -0
- package/bin/win32-arm64/cua-driver.exe +0 -0
- package/bin/win32-x64/.version +0 -2
- package/bin/win32-x64/cua-driver-uia.exe +0 -0
- package/bin/win32-x64/cua-driver.exe +0 -0
- package/scripts/postinstall.js +0 -29
package/dist/mcp-client.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
16
|
-
|
|
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
|
|
30
|
-
if (
|
|
31
|
-
throw new Error('
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
473
|
+
if (process.platform === 'win32')
|
|
474
|
+
return;
|
|
475
|
+
try {
|
|
476
|
+
accessSync(binaryPath, constants.X_OK);
|
|
70
477
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
563
|
+
await execFileAsync(layout.binaryPath, ['stop', '--socket', socket], {
|
|
564
|
+
env: this.daemonEnvironment(layout.embedded),
|
|
565
|
+
timeout: 3_000,
|
|
566
|
+
});
|
|
81
567
|
}
|
|
82
|
-
catch
|
|
83
|
-
|
|
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
|
-
|
|
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
|