@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/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
|
|
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
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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,
|
|
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: '
|
|
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,
|
|
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
|
|
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:
|
|
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({
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
|
513
|
-
|
|
514
|
-
upstreamTools = await client.listAllTools();
|
|
529
|
+
await discoverAndRegisterTools(ctx.signal);
|
|
530
|
+
connectedAtStartup = true;
|
|
515
531
|
}
|
|
516
|
-
catch (
|
|
517
|
-
|
|
518
|
-
|
|
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
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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 === '
|
|
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
|
-
|
|
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')) {
|