@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
package/dist/index.js CHANGED
@@ -1,12 +1,68 @@
1
+ import { lstat, realpath } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { isProjectTrusted } from '@amaster.ai/pi-shared/settings';
4
+ import { StringEnum } from '@earendil-works/pi-ai';
1
5
  import { Type } from 'typebox';
2
6
  import { loadConfigFromFile, resolveConfig } from './config.js';
3
- import { CuaDriverClient } from './mcp-client.js';
7
+ import toolManifest from './generated/cua-driver-tools.js';
8
+ import { CuaDriverClient, waitForPromise } from './mcp-client.js';
9
+ import { CORE_TOOLS, TOOL_GROUP_NAMES, TOOL_GROUPS } from './tool-groups.js';
10
+ import { toPiToolResult } from './tool-result.js';
4
11
  import { createPiVisionCaller } from './vision.js';
12
+ export { CuaDriverClient, resolveBundledTarget, resolveDriverLayout, resolveUnixSocketPath, } from './mcp-client.js';
5
13
  export { loadConfigFromFile, resolveConfig };
6
14
  const TOOL_PREFIX = 'computer_use_';
7
- const PLATFORM = process.platform;
15
+ const HIGH_RISK_TOOLS = new Set([
16
+ 'browser_download',
17
+ 'browser_prepare',
18
+ 'browser_set_input_files',
19
+ 'clipboard_read',
20
+ 'clipboard_write',
21
+ 'install_ffmpeg',
22
+ 'kill_app',
23
+ 'replay_trajectory',
24
+ 'start_recording',
25
+ ]);
26
+ function isInside(relativePath) {
27
+ return (relativePath === '' ||
28
+ (relativePath !== '..' &&
29
+ !relativePath.startsWith(`..${path.sep}`) &&
30
+ !path.isAbsolute(relativePath)));
31
+ }
32
+ async function approvedRecordingDir(value, cwd) {
33
+ const root = path.resolve(cwd);
34
+ const outputDir = path.resolve(cwd, String(value ?? ''));
35
+ const lexicalRelative = path.relative(root, outputDir);
36
+ if (!isInside(lexicalRelative))
37
+ return undefined;
38
+ try {
39
+ const canonicalRoot = await realpath(root);
40
+ let existing = outputDir;
41
+ const suffix = [];
42
+ for (;;) {
43
+ const info = await lstat(existing).catch(() => undefined);
44
+ if (info) {
45
+ if (!info.isDirectory() && !info.isSymbolicLink())
46
+ return undefined;
47
+ if (existing === outputDir && info.isSymbolicLink())
48
+ return undefined;
49
+ const canonical = path.resolve(await realpath(existing), ...suffix);
50
+ const canonicalRelative = path.relative(canonicalRoot, canonical);
51
+ return isInside(canonicalRelative) ? canonical : undefined;
52
+ }
53
+ const parent = path.dirname(existing);
54
+ if (parent === existing)
55
+ return undefined;
56
+ suffix.unshift(path.basename(existing));
57
+ existing = parent;
58
+ }
59
+ }
60
+ catch {
61
+ return undefined;
62
+ }
63
+ }
8
64
  function permissionHint() {
9
- switch (PLATFORM) {
65
+ switch (process.platform) {
10
66
  case 'darwin':
11
67
  return 'Check that Accessibility and Screen Recording permissions are granted in System Settings → Privacy & Security.';
12
68
  case 'win32':
@@ -16,7 +72,7 @@ function permissionHint() {
16
72
  }
17
73
  }
18
74
  function accessibilityHint() {
19
- switch (PLATFORM) {
75
+ switch (process.platform) {
20
76
  case 'darwin':
21
77
  return 'Accessibility permission not granted. The user needs to enable it in System Settings → Privacy & Security → Accessibility, then restart the app.';
22
78
  case 'win32':
@@ -26,7 +82,7 @@ function accessibilityHint() {
26
82
  }
27
83
  }
28
84
  function screenRecordingHint() {
29
- switch (PLATFORM) {
85
+ switch (process.platform) {
30
86
  case 'darwin':
31
87
  return 'Screen Recording permission not granted. The user needs to enable it in System Settings → Privacy & Security → Screen & System Audio Recording, then restart the app.';
32
88
  case 'win32':
@@ -35,380 +91,257 @@ function screenRecordingHint() {
35
91
  return 'Screen capture failed. Check that the compositor allows screen capture (PipeWire portal or X11 access).';
36
92
  }
37
93
  }
38
- const EXCLUDED_TOOLS = new Set([
39
- 'set_agent_cursor_enabled',
40
- 'set_agent_cursor_motion',
41
- 'set_agent_cursor_style',
42
- 'get_agent_cursor_state',
43
- 'set_recording',
44
- 'get_recording_state',
45
- 'replay_trajectory',
46
- 'check_permissions',
47
- 'get_config',
48
- 'set_config',
49
- 'move_cursor',
50
- 'zoom',
51
- 'type_text_chars',
52
- 'page',
53
- 'browser_eval',
54
- 'screenshot',
55
- ]);
56
- const FALLBACK_TOOLS = [
57
- {
58
- name: 'click',
59
- description: 'Left-click against a target pid via element_index or x/y coordinates',
60
- inputSchema: {
61
- type: 'object',
62
- additionalProperties: false,
63
- properties: {
64
- pid: { type: 'integer', description: 'Target process ID' },
65
- x: { type: 'number', description: 'Window-local screenshot X coordinate' },
66
- y: { type: 'number', description: 'Window-local screenshot Y coordinate' },
67
- element_index: { type: 'integer', description: 'Element index from last get_window_state' },
68
- window_id: { type: 'integer', description: 'Target window ID. Required for element_index' },
69
- action: {
70
- type: 'string',
71
- description: 'AX action: press, show_menu, pick, confirm, cancel, open',
72
- },
73
- modifier: {
74
- type: 'array',
75
- items: { type: 'string' },
76
- description: 'Modifier keys: cmd, shift, option/alt, ctrl',
77
- },
78
- from_zoom: {
79
- type: 'boolean',
80
- description: 'When true, x/y are in last zoom image coordinates',
81
- },
82
- },
83
- required: ['pid'],
84
- },
85
- },
86
- {
87
- name: 'double_click',
88
- description: 'Double-click at x/y or on an AX element via element_index',
89
- inputSchema: {
90
- type: 'object',
91
- additionalProperties: false,
92
- properties: {
93
- pid: { type: 'integer' },
94
- x: { type: 'number', description: 'Screen X coordinate (pixel path)' },
95
- y: { type: 'number', description: 'Screen Y coordinate (pixel path)' },
96
- element_index: { type: 'integer', description: 'Element index from last get_window_state' },
97
- window_id: {
98
- type: 'integer',
99
- description: 'CGWindowID. Required when element_index is used',
100
- },
101
- },
102
- required: ['pid'],
103
- },
104
- },
105
- {
106
- name: 'right_click',
107
- description: 'Right-click against a target pid via element_index or x/y coordinates',
108
- inputSchema: {
109
- type: 'object',
110
- additionalProperties: false,
111
- properties: {
112
- pid: { type: 'integer', description: 'Target process ID' },
113
- x: { type: 'number', description: 'X in window-local screenshot pixels' },
114
- y: { type: 'number', description: 'Y in window-local screenshot pixels' },
115
- element_index: {
116
- type: 'integer',
117
- description: 'Element index from last get_window_state. Routes through AXShowMenu',
118
- },
119
- window_id: {
120
- type: 'integer',
121
- description: 'CGWindowID. Required when element_index is used',
122
- },
123
- modifier: {
124
- type: 'array',
125
- items: { type: 'string' },
126
- description: 'Modifier keys held during the right-click (pixel path only)',
127
- },
128
- },
129
- required: ['pid'],
130
- },
131
- },
132
- {
133
- name: 'type_text',
134
- description: 'Insert text into the target pid via AX or CGEvent fallback',
135
- inputSchema: {
136
- type: 'object',
137
- additionalProperties: false,
138
- properties: {
139
- pid: { type: 'integer', description: 'Target process ID' },
140
- text: { type: 'string', description: 'Text to insert at the target cursor' },
141
- element_index: { type: 'integer', description: 'Element index from last get_window_state' },
142
- window_id: {
143
- type: 'integer',
144
- description: 'CGWindowID. Required when element_index is used',
145
- },
146
- delay_ms: {
147
- type: 'integer',
148
- minimum: 0,
149
- maximum: 200,
150
- description: 'Milliseconds between characters in CGEvent fallback. Default 30',
151
- },
152
- },
153
- required: ['pid', 'text'],
154
- },
155
- },
156
- {
157
- name: 'press_key',
158
- description: 'Press and release a single key, delivered to the target pid',
159
- inputSchema: {
160
- type: 'object',
161
- additionalProperties: false,
162
- properties: {
163
- pid: { type: 'integer' },
164
- key: {
165
- type: 'string',
166
- description: 'Key name: return, tab, escape, up, down, left, right, space, delete, etc.',
167
- },
168
- modifiers: {
169
- type: 'array',
170
- items: { type: 'string' },
171
- description: 'Modifier keys: cmd, shift, option/alt, ctrl, fn',
172
- },
173
- element_index: { type: 'integer' },
174
- window_id: { type: 'integer' },
175
- },
176
- required: ['pid', 'key'],
177
- },
178
- },
179
- {
180
- name: 'hotkey',
181
- description: 'Press a combination of keys simultaneously, e.g. ["cmd", "c"] for Copy',
182
- inputSchema: {
183
- type: 'object',
184
- additionalProperties: false,
185
- properties: {
186
- pid: { type: 'integer', description: 'Target process ID' },
187
- keys: {
188
- type: 'array',
189
- items: { type: 'string' },
190
- minItems: 2,
191
- description: 'Modifier(s) and one non-modifier key, e.g. ["cmd", "c"]',
192
- },
193
- window_id: {
194
- type: 'integer',
195
- description: 'When set, uses NSMenu path for native menu key dispatch',
196
- },
197
- },
198
- required: ['pid', 'keys'],
199
- },
200
- },
201
- {
202
- name: 'scroll',
203
- description: 'Scroll the target pid focused region by synthesized keystrokes',
204
- inputSchema: {
205
- type: 'object',
206
- additionalProperties: false,
207
- properties: {
208
- pid: { type: 'integer' },
209
- direction: {
210
- type: 'string',
211
- enum: ['up', 'down', 'left', 'right'],
212
- description: 'Scroll direction',
213
- },
214
- amount: {
215
- type: 'integer',
216
- minimum: 1,
217
- maximum: 50,
218
- description: 'Number of keystroke repetitions. Default: 3',
219
- },
220
- by: {
221
- type: 'string',
222
- enum: ['line', 'page'],
223
- description: 'Scroll granularity. Default: line',
224
- },
225
- element_index: { type: 'integer' },
226
- window_id: { type: 'integer' },
227
- },
228
- required: ['pid', 'direction'],
229
- },
230
- },
231
- {
232
- name: 'drag',
233
- description: 'Press-drag-release gesture from one point to another in window-local pixels',
234
- inputSchema: {
235
- type: 'object',
236
- additionalProperties: false,
237
- properties: {
238
- pid: { type: 'integer', description: 'Target process ID' },
239
- from_x: { type: 'number', description: 'Drag-start X in window-local screenshot pixels' },
240
- from_y: { type: 'number', description: 'Drag-start Y in window-local screenshot pixels' },
241
- to_x: { type: 'number', description: 'Drag-end X in window-local screenshot pixels' },
242
- to_y: { type: 'number', description: 'Drag-end Y in window-local screenshot pixels' },
243
- button: {
244
- type: 'string',
245
- enum: ['left', 'right', 'middle'],
246
- description: 'Mouse button. Default: left',
247
- },
248
- duration_ms: {
249
- type: 'integer',
250
- minimum: 0,
251
- maximum: 10000,
252
- description: 'Duration of drag path. Default: 500',
253
- },
254
- steps: {
255
- type: 'integer',
256
- minimum: 1,
257
- maximum: 200,
258
- description: 'Intermediate drag events. Default: 20',
259
- },
260
- modifier: {
261
- type: 'array',
262
- items: { type: 'string' },
263
- description: 'Modifier keys held across the gesture',
264
- },
265
- window_id: { type: 'integer' },
266
- from_zoom: { type: 'boolean' },
267
- },
268
- required: ['pid', 'from_x', 'from_y', 'to_x', 'to_y'],
269
- },
270
- },
271
- {
272
- name: 'set_value',
273
- description: 'Set a value on a UI element (popups, sliders, steppers, date pickers)',
274
- inputSchema: {
275
- type: 'object',
276
- additionalProperties: false,
277
- properties: {
278
- pid: { type: 'integer' },
279
- window_id: {
280
- type: 'integer',
281
- description: 'CGWindowID for the window whose get_window_state produced the element_index',
282
- },
283
- element_index: { type: 'integer' },
284
- value: {
285
- type: 'string',
286
- description: 'New value. AX will coerce to the element native type',
287
- },
288
- },
289
- required: ['pid', 'window_id', 'element_index', 'value'],
290
- },
291
- },
292
- {
293
- name: 'get_screen_size',
294
- description: 'Return the logical size of the main display in points plus backing scale factor',
295
- inputSchema: { type: 'object', additionalProperties: false, properties: {} },
296
- },
297
- {
298
- name: 'get_cursor_position',
299
- description: 'Return the current mouse cursor position in screen points',
300
- inputSchema: { type: 'object', additionalProperties: false, properties: {} },
301
- },
302
- {
303
- name: 'get_accessibility_tree',
304
- description: 'Return a lightweight desktop snapshot: running apps and visible windows with bounds and z-order',
305
- inputSchema: { type: 'object', additionalProperties: false, properties: {} },
306
- },
307
- {
308
- name: 'get_window_state',
309
- description: 'Walk an app AX tree and return a Markdown rendering of its UI with actionable element indices',
310
- inputSchema: {
311
- type: 'object',
312
- additionalProperties: false,
313
- properties: {
314
- pid: { type: 'integer', description: 'Target process ID' },
315
- window_id: { type: 'integer', description: 'Target window ID from list_windows' },
316
- query: { type: 'string', description: 'Case-insensitive filter for tree_markdown' },
317
- capture_mode: {
318
- type: 'string',
319
- enum: ['som', 'vision', 'ax'],
320
- description: 'som=AX+screenshot (default), vision=screenshot only, ax=AX only',
321
- },
322
- },
323
- required: ['pid', 'window_id'],
324
- },
325
- },
326
- {
327
- name: 'list_windows',
328
- description: 'List all layer-0 top-level windows known to WindowServer',
329
- inputSchema: {
330
- type: 'object',
331
- additionalProperties: false,
332
- properties: {
333
- pid: { type: 'integer', description: 'Optional pid filter' },
334
- on_screen_only: {
335
- type: 'boolean',
336
- description: 'When true, drop windows not on current Space. Default false',
337
- },
338
- },
339
- },
340
- },
341
- {
342
- name: 'list_apps',
343
- description: 'List macOS apps (running and installed) with state flags, pid, bundle_id',
344
- inputSchema: { type: 'object', additionalProperties: false, properties: {} },
345
- },
346
- {
347
- name: 'launch_app',
348
- description: 'Launch a macOS app in the background without stealing focus',
349
- inputSchema: {
350
- type: 'object',
351
- additionalProperties: false,
352
- properties: {
353
- bundle_id: {
354
- type: 'string',
355
- description: 'App bundle identifier, e.g. com.apple.calculator. Preferred over name',
356
- },
357
- name: {
358
- type: 'string',
359
- description: 'App display name. Used only when bundle_id is absent',
360
- },
361
- urls: {
362
- type: 'array',
363
- items: { type: 'string' },
364
- description: 'File paths or URLs to open with the app',
365
- },
366
- creates_new_application_instance: {
367
- type: 'boolean',
368
- description: 'Force a new app instance even if already running',
369
- },
370
- },
371
- },
372
- },
373
- {
374
- name: 'kill_app',
375
- description: 'Force-terminate a process by pid (kill -9 equivalent)',
376
- inputSchema: {
377
- type: 'object',
378
- additionalProperties: false,
379
- properties: { pid: { type: 'integer', description: 'PID of the process to terminate' } },
380
- required: ['pid'],
381
- },
382
- },
383
- ];
384
94
  export default function computerUseExtension(pi) {
385
95
  let config;
386
96
  let client;
387
- let connected = false;
388
- async function ensureConnected() {
389
- if (!client)
97
+ let sessionAbortController;
98
+ let macPermissionPromise;
99
+ const approvedLaunchApprovalKeys = new Set();
100
+ const driverToolNames = new Set();
101
+ const TOOLS_META_TOOL = `${TOOL_PREFIX}tools`;
102
+ async function ensureConnected(signal, requestMacPermissions = true) {
103
+ signal?.throwIfAborted();
104
+ if (!client) {
105
+ if (!config)
106
+ throw new Error('pi-computer-use: session not started');
107
+ client = new CuaDriverClient(config);
108
+ }
109
+ const sessionClient = client;
110
+ const sessionSignal = sessionAbortController?.signal;
111
+ if (!sessionSignal)
390
112
  throw new Error('pi-computer-use: session not started');
391
- if (!connected) {
392
- await client.connect();
393
- connected = true;
113
+ sessionSignal.throwIfAborted();
114
+ await sessionClient.ensureReady(signal);
115
+ if (process.platform !== 'darwin' || !requestMacPermissions)
116
+ return;
117
+ sessionSignal.throwIfAborted();
118
+ if (!macPermissionPromise) {
119
+ const permissionPromise = sessionClient
120
+ .callTool('check_permissions', { prompt: true }, sessionSignal)
121
+ .then(() => undefined, () => {
122
+ if (!sessionSignal.aborted) {
123
+ console.error('[pi-computer-use] macOS permission probe failed; requested tool will continue');
124
+ }
125
+ });
126
+ macPermissionPromise = permissionPromise;
127
+ }
128
+ await waitForPromise(macPermissionPromise, signal);
129
+ sessionSignal.throwIfAborted();
130
+ }
131
+ async function confirmToolCall(toolName, params, ctx) {
132
+ const requestedRecordingDir = params.output_dir;
133
+ if (toolName === 'launch_app' && config?.confirmAppLaunch !== false) {
134
+ const target = String(params.bundle_id ??
135
+ params.aumid ??
136
+ params.name ??
137
+ params.app_name ??
138
+ params.path ??
139
+ params.command ??
140
+ 'unknown app');
141
+ const approvalKey = JSON.stringify(Object.fromEntries(Object.entries(params).sort(([a], [b]) => a.localeCompare(b))));
142
+ if (approvedLaunchApprovalKeys.has(approvalKey))
143
+ return true;
144
+ if (!ctx.hasUI)
145
+ return false;
146
+ const approved = await ctx.ui.confirm('Allow computer use?', `Allow pi-computer-use to launch and control ${target} for this session?`);
147
+ if (approved)
148
+ approvedLaunchApprovalKeys.add(approvalKey);
149
+ return approved;
150
+ }
151
+ if (toolName === 'start_recording') {
152
+ const outputDir = await approvedRecordingDir(requestedRecordingDir, ctx.cwd);
153
+ if (!outputDir)
154
+ return false;
155
+ params.output_dir = outputDir;
156
+ }
157
+ if (toolName === 'start_recording' ||
158
+ (HIGH_RISK_TOOLS.has(toolName) && config?.confirmDangerousActions !== false)) {
159
+ if (!ctx.hasUI)
160
+ return false;
161
+ const approved = await ctx.ui.confirm('Confirm high-risk computer action', `Allow ${toolName} with arguments ${JSON.stringify(params)}?`);
162
+ if (!approved)
163
+ return false;
164
+ if (toolName === 'start_recording') {
165
+ const outputDir = await approvedRecordingDir(requestedRecordingDir, ctx.cwd);
166
+ if (!outputDir || outputDir !== params.output_dir)
167
+ return false;
168
+ params.output_dir = outputDir;
169
+ }
170
+ return true;
171
+ }
172
+ return true;
173
+ }
174
+ function formatGroupList() {
175
+ const active = new Set(pi.getActiveTools());
176
+ const lines = ['Computer-use tool groups:'];
177
+ for (const [name, group] of Object.entries(TOOL_GROUPS)) {
178
+ const registered = group.tools.filter((tool) => driverToolNames.has(`${TOOL_PREFIX}${tool}`));
179
+ const activeCount = registered.filter((tool) => active.has(`${TOOL_PREFIX}${tool}`)).length;
180
+ const suffix = registered.length === 0
181
+ ? 'unavailable until the driver is registered'
182
+ : activeCount === 0
183
+ ? 'inactive'
184
+ : activeCount === registered.length
185
+ ? 'active'
186
+ : `${activeCount}/${registered.length} active`;
187
+ lines.push(`- ${name} (${group.tools.length} tools, ${suffix}): ${group.summary}`);
394
188
  }
189
+ lines.push(`Activate a group with ${TOOLS_META_TOOL} group=<name> or /computer-use-tools <name>.`);
190
+ return lines.join('\n');
191
+ }
192
+ function activateGroup(group) {
193
+ const definition = TOOL_GROUPS[group];
194
+ const active = pi.getActiveTools();
195
+ const wanted = (definition?.tools ?? [])
196
+ .map((tool) => `${TOOL_PREFIX}${tool}`)
197
+ .filter((name) => driverToolNames.has(name));
198
+ const toAdd = wanted.filter((name) => !active.includes(name));
199
+ if (toAdd.length > 0)
200
+ pi.setActiveTools([...active, ...toAdd]);
201
+ return {
202
+ activated: wanted,
203
+ total: definition?.tools.length ?? 0,
204
+ alreadyActive: toAdd.length === 0 && wanted.length > 0,
205
+ };
206
+ }
207
+ function applyToolProfile() {
208
+ if (config?.toolProfile !== 'core')
209
+ return;
210
+ const core = new Set(CORE_TOOLS);
211
+ pi.setActiveTools(pi
212
+ .getActiveTools()
213
+ .filter((name) => !driverToolNames.has(name) || core.has(name.slice(TOOL_PREFIX.length))));
214
+ }
215
+ function registerToolGroupSurface() {
216
+ const groupSummary = Object.entries(TOOL_GROUPS)
217
+ .map(([name, group]) => `${name} (${group.summary})`)
218
+ .join(', ');
219
+ pi.registerTool({
220
+ name: TOOLS_META_TOOL,
221
+ label: TOOLS_META_TOOL,
222
+ description: `List and activate additional computer-use tool groups: ${groupSummary}. Call without arguments to list groups and their activation status.`,
223
+ parameters: Type.Object({
224
+ group: Type.Optional(StringEnum(TOOL_GROUP_NAMES, {
225
+ description: 'Tool group to activate. Omit to list available groups.',
226
+ })),
227
+ }),
228
+ promptSnippet: 'List or activate extra computer-use tool groups when the core tools are insufficient',
229
+ promptGuidelines: [
230
+ `Only the core computer-use tools are active by default. When a task needs browser automation, recording, escalated sessions, cursor overlay, window management, clipboard, or diagnostics capabilities, call ${TOOLS_META_TOOL} with the matching group first — the group's tools become available on the next turn.`,
231
+ ],
232
+ async execute(_toolCallId, params) {
233
+ const group = typeof params.group === 'string' ? params.group : undefined;
234
+ if (!group) {
235
+ return {
236
+ content: [{ type: 'text', text: formatGroupList() }],
237
+ details: undefined,
238
+ };
239
+ }
240
+ if (driverToolNames.size === 0) {
241
+ return {
242
+ content: [
243
+ {
244
+ type: 'text',
245
+ text: `Driver tools are not registered yet — connect the driver first with ${TOOL_PREFIX}connect, then activate the "${group}" group again.`,
246
+ },
247
+ ],
248
+ details: undefined,
249
+ isError: true,
250
+ };
251
+ }
252
+ const { activated, total, alreadyActive } = activateGroup(group);
253
+ if (activated.length === 0) {
254
+ return {
255
+ content: [
256
+ {
257
+ type: 'text',
258
+ text: `Group "${group}" has no tools registered on this platform's driver.`,
259
+ },
260
+ ],
261
+ details: undefined,
262
+ isError: true,
263
+ };
264
+ }
265
+ return {
266
+ content: [
267
+ {
268
+ type: 'text',
269
+ text: `${alreadyActive ? 'Already active' : 'Activated'} group "${group}" (${activated.length}/${total} tools): ${activated.join(', ')}. These tools are available from the next turn.`,
270
+ },
271
+ ],
272
+ details: undefined,
273
+ };
274
+ },
275
+ });
276
+ pi.registerCommand('computer-use-tools', {
277
+ description: 'List or activate computer-use tool groups',
278
+ getArgumentCompletions: (argumentPrefix) => {
279
+ const prefix = argumentPrefix.trim().toLowerCase();
280
+ return Object.entries(TOOL_GROUPS)
281
+ .filter(([name]) => name.startsWith(prefix))
282
+ .map(([name, group]) => ({ value: name, label: name, description: group.summary }));
283
+ },
284
+ async handler(args, ctx) {
285
+ const group = String(args ?? '')
286
+ .trim()
287
+ .toLowerCase();
288
+ if (!group) {
289
+ ctx.ui.notify(formatGroupList(), 'info');
290
+ return;
291
+ }
292
+ if (!TOOL_GROUP_NAMES.includes(group)) {
293
+ ctx.ui.notify(`pi-computer-use: unknown group "${group}". Available: ${TOOL_GROUP_NAMES.join(', ')}`, 'warning');
294
+ return;
295
+ }
296
+ const { activated, alreadyActive } = activateGroup(group);
297
+ ctx.ui.notify(activated.length === 0
298
+ ? `pi-computer-use: group "${group}" has no registered tools (driver not connected?).`
299
+ : `pi-computer-use: ${alreadyActive ? 'already active' : 'activated'} "${group}" (${activated.length} tools).`, 'info');
300
+ },
301
+ });
395
302
  }
396
303
  function registerTools(tools) {
397
304
  for (const tool of tools) {
398
- if (EXCLUDED_TOOLS.has(tool.name))
399
- continue;
400
305
  const prefixedName = `${TOOL_PREFIX}${tool.name}`;
401
306
  const originalName = tool.name;
307
+ driverToolNames.add(prefixedName);
402
308
  pi.registerTool({
403
309
  name: prefixedName,
404
310
  label: prefixedName,
405
311
  description: tool.description ?? '',
406
312
  parameters: Type.Unsafe(tool.inputSchema),
407
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
313
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
314
+ if (!(await confirmToolCall(originalName, params, ctx))) {
315
+ return {
316
+ content: [
317
+ {
318
+ type: 'text',
319
+ text: `${originalName} requires interactive user confirmation.`,
320
+ },
321
+ ],
322
+ details: undefined,
323
+ isError: true,
324
+ };
325
+ }
408
326
  try {
409
- await ensureConnected();
327
+ await ensureConnected(signal, originalName !== 'check_permissions');
328
+ const result = await client.callTool(originalName, params, signal);
329
+ if (result.isError) {
330
+ const errorText = result.content?.map((c) => c.text ?? '').join('') ?? '';
331
+ const friendlyError = formatToolError(originalName, errorText, params);
332
+ if (friendlyError) {
333
+ return {
334
+ content: [{ type: 'text', text: friendlyError }],
335
+ details: undefined,
336
+ isError: true,
337
+ };
338
+ }
339
+ }
340
+ return toPiToolResult(result);
410
341
  }
411
342
  catch (connErr) {
343
+ if (signal?.aborted)
344
+ throw connErr;
412
345
  const msg = connErr instanceof Error ? connErr.message : String(connErr);
413
346
  ctx.ui.notify(`pi-computer-use: cannot connect to cua-driver — ${msg}`, 'warning');
414
347
  return {
@@ -422,36 +355,61 @@ export default function computerUseExtension(pi) {
422
355
  isError: true,
423
356
  };
424
357
  }
425
- const result = await client.callTool(originalName, params);
426
- if (result.isError) {
427
- const errorText = result.content?.map((c) => c.text ?? '').join('') ?? '';
428
- const friendlyError = formatToolError(originalName, errorText, params);
429
- if (friendlyError) {
430
- return {
431
- content: [{ type: 'text', text: friendlyError }],
432
- details: undefined,
433
- isError: true,
434
- };
435
- }
436
- }
437
- const content = [];
438
- if (result.content) {
439
- for (const item of result.content) {
440
- if (item.type === 'text' && item.text) {
441
- content.push({ type: 'text', text: item.text });
442
- }
443
- }
444
- }
445
- if (content.length === 0) {
446
- content.push({ type: 'text', text: 'Action executed.' });
447
- }
448
- return result.isError
449
- ? { content, details: undefined, isError: true }
450
- : { content, details: undefined };
451
358
  },
452
359
  });
453
360
  }
454
361
  }
362
+ async function discoverAndRegisterTools(signal) {
363
+ await ensureConnected(signal);
364
+ const liveTools = await client.listAllTools(signal);
365
+ registerTools(liveTools);
366
+ return liveTools.length;
367
+ }
368
+ function registerRecoverySurface() {
369
+ pi.registerTool({
370
+ name: `${TOOL_PREFIX}connect`,
371
+ label: `${TOOL_PREFIX}connect`,
372
+ description: 'Retry Cua Driver startup and register the exact live tool contract for this platform.',
373
+ parameters: Type.Object({}),
374
+ async execute(_toolCallId, _params, signal) {
375
+ try {
376
+ const count = await discoverAndRegisterTools(signal);
377
+ applyToolProfile();
378
+ return {
379
+ content: [
380
+ {
381
+ type: 'text',
382
+ text: `Cua Driver connected; registered ${count} platform tools. Continue with the requested computer_use tool.`,
383
+ },
384
+ ],
385
+ details: { registered_tools: count },
386
+ };
387
+ }
388
+ catch (error) {
389
+ if (signal?.aborted)
390
+ throw error;
391
+ return {
392
+ content: [{ type: 'text', text: 'Cua Driver is still unavailable.' }],
393
+ details: undefined,
394
+ isError: true,
395
+ };
396
+ }
397
+ },
398
+ });
399
+ pi.registerCommand('computer-use-connect', {
400
+ description: 'Retry Cua Driver startup and discover platform tools',
401
+ async handler(_args, ctx) {
402
+ try {
403
+ const count = await discoverAndRegisterTools(ctx.signal);
404
+ applyToolProfile();
405
+ ctx.ui.notify(`pi-computer-use: registered ${count} platform tools.`, 'info');
406
+ }
407
+ catch {
408
+ ctx.ui.notify('pi-computer-use: Cua Driver is still unavailable.', 'warning');
409
+ }
410
+ },
411
+ });
412
+ }
455
413
  function registerVisionTool() {
456
414
  if (!config?.visionModel)
457
415
  return;
@@ -459,17 +417,23 @@ export default function computerUseExtension(pi) {
459
417
  pi.registerTool({
460
418
  name: `${TOOL_PREFIX}analyze_screenshot`,
461
419
  label: `${TOOL_PREFIX}analyze_screenshot`,
462
- description: 'Take a screenshot and analyze it visually using a vision model. Use when you need to identify elements by visual attributes (color, layout, position) or need precise pixel coordinates.',
420
+ description: 'Capture a window through get_window_state and analyze its image using the configured vision model. Pass pid and window_id from list_windows.',
463
421
  parameters: Type.Object({
422
+ pid: Type.Integer({ description: 'Target process ID from list_windows.' }),
423
+ window_id: Type.Integer({
424
+ description: 'Required CGWindowID / kCGWindowNumber to capture.',
425
+ }),
464
426
  instruction: Type.Optional(Type.String({
465
427
  description: 'What to identify or analyze visually (e.g., "Find the coordinates of the blue submit button").',
466
428
  })),
467
429
  }),
468
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
430
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
469
431
  try {
470
- await ensureConnected();
432
+ await ensureConnected(signal);
471
433
  }
472
434
  catch (connErr) {
435
+ if (signal?.aborted)
436
+ throw connErr;
473
437
  const msg = connErr instanceof Error ? connErr.message : String(connErr);
474
438
  ctx.ui.notify(`pi-computer-use: cannot connect to cua-driver — ${msg}`, 'warning');
475
439
  return {
@@ -483,11 +447,46 @@ export default function computerUseExtension(pi) {
483
447
  isError: true,
484
448
  };
485
449
  }
486
- const screenshotResult = await client.callTool('screenshot', {});
450
+ const screenshotArgs = {
451
+ pid: params.pid,
452
+ window_id: params.window_id,
453
+ include_screenshot: true,
454
+ max_elements: 1,
455
+ };
456
+ let screenshotResult;
457
+ try {
458
+ screenshotResult = await client.callTool('get_window_state', screenshotArgs, signal);
459
+ }
460
+ catch (error) {
461
+ if (signal?.aborted)
462
+ throw error;
463
+ const message = error instanceof Error ? error.message : String(error);
464
+ return {
465
+ content: [
466
+ { type: 'text', text: `Failed to capture window state: ${message}` },
467
+ ],
468
+ details: undefined,
469
+ isError: true,
470
+ };
471
+ }
487
472
  const imageContent = screenshotResult.content?.find((c) => c.type === 'image' && c.data);
473
+ if (process.env.DEBUG?.includes('pi-computer-use')) {
474
+ console.error('[pi-computer-use analyze_screenshot] screenshot result', JSON.stringify({
475
+ window_id: params.window_id,
476
+ isError: screenshotResult.isError,
477
+ contentTypes: screenshotResult.content?.map((c) => c.type),
478
+ imageDataLength: imageContent?.data?.length,
479
+ imageMimeType: imageContent?.mimeType,
480
+ }, null, 2));
481
+ }
488
482
  if (!imageContent?.data) {
483
+ const errorText = screenshotResult.content
484
+ ?.filter((c) => c.type === 'text' && c.text)
485
+ .map((c) => c.text)
486
+ .join('\n') || 'Failed to capture screenshot.';
487
+ const formatted = formatToolError('get_window_state', errorText, params);
489
488
  return {
490
- content: [{ type: 'text', text: 'Failed to capture screenshot.' }],
489
+ content: [{ type: 'text', text: formatted ?? errorText }],
491
490
  details: undefined,
492
491
  isError: true,
493
492
  };
@@ -495,7 +494,12 @@ export default function computerUseExtension(pi) {
495
494
  const callVision = createPiVisionCaller(visionConfig, ctx);
496
495
  const instruction = params.instruction ??
497
496
  'Describe the full screen: identify all visible windows, UI elements, buttons, text fields, and their positions.';
498
- const analysis = await callVision(instruction, imageContent.data, imageContent.mimeType ?? 'image/png');
497
+ const analysis = await callVision(instruction, imageContent.data, imageContent.mimeType ?? 'image/png', signal);
498
+ if (process.env.DEBUG?.includes('pi-computer-use')) {
499
+ console.error('[pi-computer-use analyze_screenshot] vision analysis', JSON.stringify({
500
+ analysisLength: analysis.length,
501
+ }, null, 2));
502
+ }
499
503
  return {
500
504
  content: [{ type: 'text', text: analysis }],
501
505
  details: undefined,
@@ -503,45 +507,61 @@ export default function computerUseExtension(pi) {
503
507
  },
504
508
  });
505
509
  }
510
+ registerToolGroupSurface();
506
511
  pi.on('session_start', async (_event, ctx) => {
507
- config = resolveConfig(loadConfigFromFile({ cwd: ctx.cwd }));
508
- client = new CuaDriverClient(config);
509
- connected = false;
510
- let upstreamTools;
512
+ config = resolveConfig(loadConfigFromFile({
513
+ cwd: ctx.cwd,
514
+ projectTrusted: isProjectTrusted(ctx),
515
+ }));
516
+ client = undefined;
517
+ sessionAbortController = new AbortController();
518
+ macPermissionPromise = undefined;
519
+ approvedLaunchApprovalKeys.clear();
520
+ driverToolNames.clear();
521
+ if (process.platform === 'darwin') {
522
+ registerTools(toolManifest.tools);
523
+ registerVisionTool();
524
+ applyToolProfile();
525
+ return;
526
+ }
527
+ let connectedAtStartup = false;
511
528
  try {
512
- await client.connect();
513
- connected = true;
514
- upstreamTools = await client.listAllTools();
529
+ await discoverAndRegisterTools(ctx.signal);
530
+ connectedAtStartup = true;
515
531
  }
516
- catch (err) {
517
- const msg = err instanceof Error ? err.message : String(err);
518
- ctx.ui.notify(`pi-computer-use: cua-driver failed to start — ${msg}. Tools registered but may fail until permissions are granted.`, 'warning');
532
+ catch (error) {
533
+ registerRecoverySurface();
534
+ const errorName = error instanceof Error ? error.name : 'UnknownError';
535
+ console.error(`[pi-computer-use] startup discovery failed (${errorName})`);
536
+ ctx.ui.notify('pi-computer-use: driver unavailable; use computer_use_connect to retry discovery.', 'warning');
519
537
  }
520
- registerTools(upstreamTools ?? FALLBACK_TOOLS);
521
538
  registerVisionTool();
522
- if (connected) {
523
- try {
524
- const permResult = await client.callTool('check_permissions', {});
525
- const structured = permResult.structuredContent;
526
- if (structured) {
527
- if (!structured.accessibility) {
528
- ctx.ui.notify(`pi-computer-use: ${accessibilityHint()}`, 'warning');
529
- }
530
- if (!structured.screen_recording) {
531
- ctx.ui.notify(`pi-computer-use: ${screenRecordingHint()}`, 'warning');
532
- }
533
- }
534
- }
535
- catch {
536
- // permission check is best-effort
539
+ applyToolProfile();
540
+ if (!connectedAtStartup)
541
+ return;
542
+ try {
543
+ const permissions = await client.callTool('check_permissions', { prompt: false }, ctx.signal);
544
+ const status = permissions.structuredContent;
545
+ if (status?.accessibility === false || status?.screen_recording === false) {
546
+ ctx.ui.notify(`pi-computer-use: ${permissionHint()}`, 'warning');
537
547
  }
538
548
  }
549
+ catch (error) {
550
+ const errorName = error instanceof Error ? error.name : 'UnknownError';
551
+ console.error(`[pi-computer-use] startup probe failed (${errorName})`);
552
+ }
539
553
  });
540
554
  pi.on('session_shutdown', async () => {
541
- if (connected && client) {
542
- await client.close();
543
- connected = false;
544
- }
555
+ const closingClient = client;
556
+ const sessionAbort = sessionAbortController;
557
+ const pendingPermission = macPermissionPromise;
558
+ client = undefined;
559
+ config = undefined;
560
+ sessionAbortController = undefined;
561
+ macPermissionPromise = undefined;
562
+ approvedLaunchApprovalKeys.clear();
563
+ sessionAbort?.abort(new Error('pi-computer-use: session shut down'));
564
+ await Promise.allSettled([pendingPermission, closingClient?.close()]);
545
565
  });
546
566
  }
547
567
  function formatToolError(toolName, errorText, params) {
@@ -551,7 +571,7 @@ function formatToolError(toolName, errorText, params) {
551
571
  if (errorText.includes('sc_not_granted')) {
552
572
  return screenRecordingHint();
553
573
  }
554
- if (toolName === 'screenshot' || errorText.includes('screencapture failed')) {
574
+ if (toolName === 'get_window_state' || errorText.includes('screencapture failed')) {
555
575
  const windowId = params.window_id;
556
576
  if (windowId !== undefined) {
557
577
  if (errorText.includes('screencapture failed')) {
@@ -560,7 +580,7 @@ function formatToolError(toolName, errorText, params) {
560
580
  `1. ${screenRecordingHint()}`,
561
581
  '2. The window_id is stale — the window may have been closed or recreated (e.g. after navigation in Electron apps). Re-fetch window list to get current IDs.',
562
582
  '3. The window is minimized or not yet rendered.',
563
- `Try capturing without window_id (full screen) as a fallback, or verify the window still exists.`,
583
+ 'Re-run list_windows and retry get_window_state with the current window_id.',
564
584
  ].join('\n');
565
585
  }
566
586
  if (errorText.includes('empty output')) {