@kortix/agent-tunnel 0.1.3 → 0.12.7

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 (67) hide show
  1. package/README.md +65 -0
  2. package/dist/agent-cli.js +4676 -3174
  3. package/dist/client-cli.js +203 -494
  4. package/package.json +24 -29
  5. package/src/agent/agent.ts +67 -17
  6. package/src/agent/capabilities/desktop/cua-driver.ts +284 -0
  7. package/src/agent/capabilities/desktop.ts +100 -167
  8. package/src/agent/capabilities/enabled-registry.ts +24 -0
  9. package/src/agent/capabilities/filesystem.ts +136 -32
  10. package/src/agent/capabilities/index.ts +1 -1
  11. package/src/agent/capabilities/security.test.ts +204 -0
  12. package/src/agent/capabilities/shell.ts +37 -3
  13. package/src/agent/cli-device-auth.test.ts +179 -0
  14. package/src/agent/cli-help.test.ts +25 -0
  15. package/src/agent/cli.ts +475 -38
  16. package/src/agent/config.test.ts +53 -0
  17. package/src/agent/config.ts +169 -7
  18. package/src/agent/index.ts +1 -0
  19. package/src/agent/security/command-validator.ts +4 -2
  20. package/src/agent/security/path-validator.ts +73 -18
  21. package/src/agent/security/permission-guard.test.ts +52 -0
  22. package/src/agent/security/permission-guard.ts +35 -8
  23. package/src/agent/service.test.ts +63 -0
  24. package/src/agent/service.ts +410 -0
  25. package/src/client/cli.test.ts +150 -547
  26. package/src/client/cli.ts +116 -537
  27. package/src/client/index.ts +1 -1
  28. package/src/client/tools.ts +95 -356
  29. package/src/client/tunnel-client.ts +50 -80
  30. package/src/index.ts +7 -1
  31. package/src/node-ws-polyfill.test.ts +18 -0
  32. package/src/node-ws-polyfill.ts +5 -3
  33. package/src/server/heartbeat.ts +13 -6
  34. package/src/server/relay.test.ts +72 -0
  35. package/src/server/relay.ts +50 -9
  36. package/src/server/server.test.ts +33 -0
  37. package/src/server/server.ts +26 -6
  38. package/src/server/ws-handler.test.ts +158 -0
  39. package/src/server/ws-handler.ts +94 -36
  40. package/src/shared/crypto.ts +2 -3
  41. package/src/shared/index.ts +8 -0
  42. package/src/shared/permissions.ts +292 -0
  43. package/src/shared/types.ts +70 -41
  44. package/dist/agent/index.d.ts +0 -140
  45. package/dist/agent/index.js +0 -21
  46. package/dist/agent/index.js.map +0 -1
  47. package/dist/chunk-7N7GSU6K.js +0 -34
  48. package/dist/client/index.d.ts +0 -183
  49. package/dist/client/index.js +0 -8
  50. package/dist/client/index.js.map +0 -1
  51. package/dist/index.d.ts +0 -7
  52. package/dist/index.js +0 -55
  53. package/dist/index.js.map +0 -1
  54. package/dist/server/index.d.ts +0 -89
  55. package/dist/server/index.js +0 -14
  56. package/dist/server/index.js.map +0 -1
  57. package/dist/shared/index.d.ts +0 -10
  58. package/dist/shared/index.js +0 -20
  59. package/dist/shared/index.js.map +0 -1
  60. package/dist/types-Dpwrd8Ai.d.ts +0 -194
  61. package/src/agent/capabilities/desktop/atspi-helper.ts +0 -345
  62. package/src/agent/capabilities/desktop/csharp-helper.ts +0 -914
  63. package/src/agent/capabilities/desktop/linux-driver.ts +0 -368
  64. package/src/agent/capabilities/desktop/macos-driver.ts +0 -601
  65. package/src/agent/capabilities/desktop/swift-helper.ts +0 -736
  66. package/src/agent/capabilities/desktop/types.ts +0 -201
  67. package/src/agent/capabilities/desktop/windows-driver.ts +0 -220
@@ -1,5 +1,5 @@
1
1
  export { TunnelClient, TunnelClientError } from './tunnel-client';
2
- export type { TunnelClientConfig, AXElement } from './tunnel-client';
2
+ export type { TunnelClientConfig } from './tunnel-client';
3
3
  export { createTunnelTools } from './tools';
4
4
  export type { TunnelToolDefinition, TunnelToolParameter } from './tools';
5
5
  // CLI entrypoint: src/client/cli.ts (run via `bun run cli.ts <command> [json]`)
@@ -1,8 +1,4 @@
1
- import { writeFileSync, mkdirSync } from 'fs';
2
- import { join } from 'path';
3
- import { tmpdir } from 'os';
4
- import { randomBytes } from 'crypto';
5
- import type { TunnelClient, AXElement } from './tunnel-client';
1
+ import type { TunnelClient } from './tunnel-client';
6
2
 
7
3
  export interface TunnelToolParameter {
8
4
  type: string;
@@ -19,52 +15,28 @@ export interface TunnelToolDefinition {
19
15
  execute: (args: Record<string, unknown>) => Promise<string>;
20
16
  }
21
17
 
22
- function saveImage(base64: string, format: string): string {
23
- const ext = format === 'jpeg' || format === 'jpg' ? 'jpg' : 'png';
24
- const dir = join(tmpdir(), 'tunnel-screenshots');
25
- mkdirSync(dir, { recursive: true });
26
- const path = join(dir, `screenshot-${randomBytes(4).toString('hex')}.${ext}`);
27
- writeFileSync(path, Buffer.from(base64, 'base64'));
28
- return path;
29
- }
30
-
31
- function formatAXTree(el: AXElement, indent: number = 0): string {
32
- const pad = ' '.repeat(indent);
33
- const parts: string[] = [];
34
-
35
- const label = el.title || el.value || el.description || '(unnamed)';
36
- const flags: string[] = [];
37
- if (!el.enabled) flags.push('disabled');
38
- if (el.focused) flags.push('focused');
39
- if (el.actions.length > 0) flags.push(`actions: ${el.actions.join(',')}`);
40
- const flagStr = flags.length > 0 ? ` [${flags.join(', ')}]` : '';
41
-
42
- parts.push(`${pad}[${el.role}] ${label} (id: ${el.id})${flagStr}`);
43
-
44
- for (const child of el.children) {
45
- parts.push(formatAXTree(child, indent + 1));
46
- }
47
-
48
- return parts.join('\n');
49
- }
50
-
51
18
  const tunnelIdParam: TunnelToolParameter = {
52
19
  type: 'string',
53
20
  description: 'Tunnel connection ID (auto-discovered if omitted)',
54
21
  required: false,
55
22
  };
56
23
 
24
+ function stringifyResult(result: unknown): string {
25
+ if (typeof result === 'string') return result;
26
+ return JSON.stringify(result, null, 2);
27
+ }
28
+
57
29
  export function createTunnelTools(client: TunnelClient): TunnelToolDefinition[] {
58
30
  return [
59
31
  {
60
32
  name: 'tunnel_status',
61
- description: `Check the status of all Agent Tunnel connections to the user's local machine. Lists every registered tunnel with its live/offline status, capabilities, and machine info.`,
33
+ description: `Check the status of all Agent Tunnel connections to local computers. Lists every registered machine with live/offline status, capabilities, and machine info.`,
62
34
  parameters: {},
63
35
  async execute() {
64
36
  const connections = (await client.getConnections()) as Array<Record<string, unknown>>;
65
37
 
66
38
  if (connections.length === 0) {
67
- return 'No tunnel connections found. The user needs to set up Agent Tunnel first:\n1. Create a tunnel connection\n2. Run `npx @kortix/agent-tunnel connect` on their local machine';
39
+ return 'No tunnel connections found. Connect this computer from the Kortix desktop app or run the tunnel connect command on another computer.';
68
40
  }
69
41
 
70
42
  const sections: string[] = [];
@@ -77,7 +49,7 @@ export function createTunnelTools(client: TunnelClient): TunnelToolDefinition[]
77
49
  const machineInfo = (data.machineInfo as Record<string, unknown>) || {};
78
50
 
79
51
  const lines = [
80
- `=== Tunnel: ${data.name || 'Unnamed'} — ${status} ===`,
52
+ `=== Computer: ${data.name || 'Unnamed'} — ${status} ===`,
81
53
  `ID: ${data.tunnelId}`,
82
54
  `Capabilities: ${capabilities.length > 0 ? capabilities.join(', ') : '(none registered)'}`,
83
55
  ];
@@ -90,7 +62,7 @@ export function createTunnelTools(client: TunnelClient): TunnelToolDefinition[]
90
62
  }
91
63
 
92
64
  if (!hasOnline) {
93
- sections.push('\nNo tunnel is currently online. Ask the user to run `npx @kortix/agent-tunnel connect` on their local machine.');
65
+ sections.push('\nNo tunnel is currently online. Connect this computer from the Kortix desktop app or run the tunnel connect command on the target computer.');
94
66
  }
95
67
 
96
68
  return sections.join('\n\n');
@@ -98,10 +70,10 @@ export function createTunnelTools(client: TunnelClient): TunnelToolDefinition[]
98
70
  },
99
71
  {
100
72
  name: 'tunnel_fs_read',
101
- description: `Read a file from the user's local machine via Agent Tunnel. Requires filesystem permission.`,
73
+ description: `Read a file from a connected computer via Agent Tunnel. Requires filesystem permission.`,
102
74
  parameters: {
103
75
  tunnel_id: tunnelIdParam,
104
- path: { type: 'string', description: 'Absolute path to the file on the user\'s local machine', required: true },
76
+ path: { type: 'string', description: 'Absolute path to the file on the connected computer', required: true },
105
77
  encoding: { type: 'string', description: 'File encoding (default: utf-8)', required: false },
106
78
  },
107
79
  async execute(args) {
@@ -116,10 +88,10 @@ export function createTunnelTools(client: TunnelClient): TunnelToolDefinition[]
116
88
  },
117
89
  {
118
90
  name: 'tunnel_fs_write',
119
- description: `Write a file to the user's local machine via Agent Tunnel. Creates parent directories if needed. Requires filesystem write permission.`,
91
+ description: `Write a file to a connected computer via Agent Tunnel. Creates parent directories if needed. Requires filesystem write permission.`,
120
92
  parameters: {
121
93
  tunnel_id: tunnelIdParam,
122
- path: { type: 'string', description: 'Absolute path for the file on the user\'s local machine', required: true },
94
+ path: { type: 'string', description: 'Absolute path for the file on the connected computer', required: true },
123
95
  content: { type: 'string', description: 'File content to write', required: true },
124
96
  encoding: { type: 'string', description: 'File encoding (default: utf-8)', required: false },
125
97
  },
@@ -136,10 +108,10 @@ export function createTunnelTools(client: TunnelClient): TunnelToolDefinition[]
136
108
  },
137
109
  {
138
110
  name: 'tunnel_fs_list',
139
- description: `List directory contents on the user's local machine via Agent Tunnel. Requires filesystem permission.`,
111
+ description: `List directory contents on a connected computer via Agent Tunnel. Requires filesystem permission.`,
140
112
  parameters: {
141
113
  tunnel_id: tunnelIdParam,
142
- path: { type: 'string', description: 'Absolute path to the directory on the user\'s local machine', required: true },
114
+ path: { type: 'string', description: 'Absolute path to the directory on the connected computer', required: true },
143
115
  recursive: { type: 'boolean', description: 'Include subdirectory contents (default: false)', required: false },
144
116
  },
145
117
  async execute(args) {
@@ -162,10 +134,10 @@ export function createTunnelTools(client: TunnelClient): TunnelToolDefinition[]
162
134
  },
163
135
  {
164
136
  name: 'tunnel_shell_exec',
165
- description: `Execute a command on the user's local machine via Agent Tunnel. Commands are executed without shell interpolation (array args) for security. Requires shell permission.`,
137
+ description: `Execute a command on a connected computer via Agent Tunnel. Commands are executed without shell interpolation (array args) for security. Requires shell permission.`,
166
138
  parameters: {
167
139
  tunnel_id: tunnelIdParam,
168
- command: { type: 'string', description: "Command executable name (e.g., 'ls', 'git', 'python')", required: true },
140
+ command: { type: 'string', description: "Command executable name (e.g. 'ls', 'git', 'python')", required: true },
169
141
  args: { type: 'array', description: 'Command arguments as separate strings (no shell interpolation)', required: false, items: { type: 'string' } },
170
142
  cwd: { type: 'string', description: 'Working directory for the command', required: false },
171
143
  timeout: { type: 'number', description: 'Timeout in milliseconds (default: 30000, max: 120000)', required: false },
@@ -202,401 +174,168 @@ export function createTunnelTools(client: TunnelClient): TunnelToolDefinition[]
202
174
  },
203
175
  },
204
176
  {
205
- name: 'tunnel_screenshot',
206
- description: `Take a screenshot of the user's screen via Agent Tunnel. Saves the image to a temp file and returns the path.`,
207
- parameters: {
208
- tunnel_id: tunnelIdParam,
209
- x: { type: 'number', description: 'Region X coordinate', required: false },
210
- y: { type: 'number', description: 'Region Y coordinate', required: false },
211
- width: { type: 'number', description: 'Region width', required: false },
212
- height: { type: 'number', description: 'Region height', required: false },
213
- windowId: { type: 'number', description: 'Capture a specific window by ID', required: false },
214
- },
215
- async execute(args) {
216
- const params: Record<string, unknown> = {};
217
- if (args.x !== undefined && args.y !== undefined && args.width !== undefined && args.height !== undefined) {
218
- params.region = { x: args.x, y: args.y, width: args.width, height: args.height };
219
- }
220
- if (args.windowId !== undefined) params.windowId = args.windowId;
221
-
222
- const result = await client.rpcWithPermissionFlow('desktop.screenshot', params);
223
- if (typeof result === 'string') return result;
224
-
225
- const data = result as { image: string; width: number; height: number; format?: string };
226
- const format = data.format || 'png';
227
- const sizeKB = Math.round(data.image.length * 0.75 / 1024);
228
- const path = saveImage(data.image, format);
229
- return `Screenshot saved: ${path}\nDimensions: ${data.width}x${data.height} ${format.toUpperCase()} (${sizeKB}KB)\n\nUse the Read tool to view this image.`;
230
- },
231
- },
232
- {
233
- name: 'tunnel_click',
234
- description: `Click at a specific screen coordinate on the user's machine via Agent Tunnel.`,
235
- parameters: {
236
- tunnel_id: tunnelIdParam,
237
- x: { type: 'number', description: 'X coordinate to click', required: true },
238
- y: { type: 'number', description: 'Y coordinate to click', required: true },
239
- button: { type: 'string', description: 'Mouse button (default: left)', required: false, enum: ['left', 'right', 'middle'] },
240
- clicks: { type: 'number', description: 'Number of clicks (default: 1, use 2 for double-click)', required: false },
241
- modifiers: { type: 'array', description: 'Modifier keys held during click: cmd, shift, alt, ctrl', required: false, items: { type: 'string' } },
242
- },
243
- async execute(args) {
244
- const result = await client.rpcWithPermissionFlow('desktop.mouse.click', {
245
- x: args.x, y: args.y, button: args.button, clicks: args.clicks, modifiers: args.modifiers,
246
- });
247
- if (typeof result === 'string') return result;
248
- return `Clicked at (${args.x}, ${args.y}) [${args.button || 'left'}]${args.clicks && (args.clicks as number) > 1 ? ` x${args.clicks}` : ''}`;
249
- },
250
- },
251
- {
252
- name: 'tunnel_mouse_move',
253
- description: `Move the mouse cursor to a specific position on the user's screen via Agent Tunnel.`,
254
- parameters: {
255
- tunnel_id: tunnelIdParam,
256
- x: { type: 'number', description: 'Target X coordinate', required: true },
257
- y: { type: 'number', description: 'Target Y coordinate', required: true },
258
- },
259
- async execute(args) {
260
- const result = await client.rpcWithPermissionFlow('desktop.mouse.move', { x: args.x, y: args.y });
261
- if (typeof result === 'string') return result;
262
- return `Mouse moved to (${args.x}, ${args.y})`;
263
- },
264
- },
265
- {
266
- name: 'tunnel_mouse_drag',
267
- description: `Drag from one point to another on the user's screen via Agent Tunnel.`,
268
- parameters: {
269
- tunnel_id: tunnelIdParam,
270
- fromX: { type: 'number', description: 'Start X coordinate', required: true },
271
- fromY: { type: 'number', description: 'Start Y coordinate', required: true },
272
- toX: { type: 'number', description: 'End X coordinate', required: true },
273
- toY: { type: 'number', description: 'End Y coordinate', required: true },
274
- button: { type: 'string', description: 'Mouse button (default: left)', required: false, enum: ['left', 'right'] },
275
- },
276
- async execute(args) {
277
- const result = await client.rpcWithPermissionFlow('desktop.mouse.drag', {
278
- fromX: args.fromX, fromY: args.fromY, toX: args.toX, toY: args.toY, button: args.button,
279
- });
280
- if (typeof result === 'string') return result;
281
- return `Dragged from (${args.fromX}, ${args.fromY}) to (${args.toX}, ${args.toY})`;
282
- },
283
- },
284
- {
285
- name: 'tunnel_mouse_scroll',
286
- description: `Scroll the mouse wheel at a specific position on the user's screen via Agent Tunnel.`,
287
- parameters: {
288
- tunnel_id: tunnelIdParam,
289
- x: { type: 'number', description: 'X coordinate to scroll at', required: true },
290
- y: { type: 'number', description: 'Y coordinate to scroll at', required: true },
291
- deltaX: { type: 'number', description: 'Horizontal scroll amount (positive=right)', required: false },
292
- deltaY: { type: 'number', description: 'Vertical scroll amount (positive=down)', required: false },
293
- },
294
- async execute(args) {
295
- const result = await client.rpcWithPermissionFlow('desktop.mouse.scroll', {
296
- x: args.x, y: args.y, deltaX: args.deltaX, deltaY: args.deltaY,
297
- });
298
- if (typeof result === 'string') return result;
299
- return `Scrolled at (${args.x}, ${args.y}) [dx=${args.deltaX || 0}, dy=${args.deltaY || 0}]`;
300
- },
301
- },
302
-
303
- {
304
- name: 'tunnel_type',
305
- description: `Type text into the currently focused application on the user's machine via Agent Tunnel.`,
306
- parameters: {
307
- tunnel_id: tunnelIdParam,
308
- text: { type: 'string', description: 'Text to type', required: true },
309
- delay: { type: 'number', description: 'Delay between characters in ms', required: false },
310
- },
311
- async execute(args) {
312
- const result = await client.rpcWithPermissionFlow('desktop.keyboard.type', {
313
- text: args.text, delay: args.delay,
314
- });
315
- if (typeof result === 'string') return result;
316
- return `Typed ${(args.text as string).length} characters`;
317
- },
318
- },
319
- {
320
- name: 'tunnel_key',
321
- description: `Press a key combination on the user's machine via Agent Tunnel. Use for keyboard shortcuts like cmd+s, ctrl+c, enter, tab, etc.`,
177
+ name: 'tunnel_cua_ensure',
178
+ description: `Ensure CUA Driver is installed on the connected computer and return its local binary path/version. Requires desktop computer_use permission.`,
322
179
  parameters: {
323
180
  tunnel_id: tunnelIdParam,
324
- keys: { type: 'array', description: "Keys to press simultaneously. Examples: ['cmd', 's'] for save, ['enter'] for enter", required: true, items: { type: 'string' } },
325
181
  },
326
- async execute(args) {
327
- const result = await client.rpcWithPermissionFlow('desktop.keyboard.key', { keys: args.keys });
328
- if (typeof result === 'string') return result;
329
- return `Pressed: ${(args.keys as string[]).join('+')}`;
182
+ async execute() {
183
+ return stringifyResult(await client.rpcWithPermissionFlow('desktop.cua.ensure', {}));
330
184
  },
331
185
  },
332
-
333
186
  {
334
- name: 'tunnel_window_list',
335
- description: `List all visible windows on the user's machine via Agent Tunnel. Returns window IDs, app names, titles, positions, and sizes.`,
187
+ name: 'tunnel_cua_start_daemon',
188
+ description: `Start the CUA Driver daemon/background service on the connected computer so CUA sessions and element indices remain stable.`,
336
189
  parameters: {
337
190
  tunnel_id: tunnelIdParam,
338
191
  },
339
192
  async execute() {
340
- const result = await client.rpcWithPermissionFlow('desktop.window.list', {});
341
- if (typeof result === 'string') return result;
342
-
343
- const data = result as { windows: Array<{ id: number; app: string; title: string; bounds: { x: number; y: number; width: number; height: number }; minimized: boolean }> };
344
- if (data.windows.length === 0) return 'No windows found';
345
-
346
- const lines = [`=== Windows (${data.windows.length}) ===`];
347
- for (const w of data.windows) {
348
- const min = w.minimized ? ' [minimized]' : '';
349
- lines.push(` #${w.id} | ${w.app} — "${w.title}" | ${w.bounds.x},${w.bounds.y} ${w.bounds.width}x${w.bounds.height}${min}`);
350
- }
351
- return lines.join('\n');
193
+ return stringifyResult(await client.rpcWithPermissionFlow('desktop.cua.start_daemon', {}));
352
194
  },
353
195
  },
354
196
  {
355
- name: 'tunnel_window_focus',
356
- description: `Bring a window to the front on the user's machine via Agent Tunnel.`,
197
+ name: 'tunnel_cua_status',
198
+ description: `Read CUA Driver daemon status from the connected computer.`,
357
199
  parameters: {
358
200
  tunnel_id: tunnelIdParam,
359
- windowId: { type: 'number', description: 'Window ID from tunnel_window_list', required: true },
360
201
  },
361
- async execute(args) {
362
- const result = await client.rpcWithPermissionFlow('desktop.window.focus', { windowId: args.windowId });
363
- if (typeof result === 'string') return result;
364
- return `Window #${args.windowId} focused`;
365
- },
366
- },
367
-
368
- {
369
- name: 'tunnel_app_launch',
370
- description: `Launch an application on the user's machine via Agent Tunnel.`,
371
- parameters: {
372
- tunnel_id: tunnelIdParam,
373
- app: { type: 'string', description: 'Application name to launch', required: true },
374
- },
375
- async execute(args) {
376
- const result = await client.rpcWithPermissionFlow('desktop.app.launch', { app: args.app });
377
- if (typeof result === 'string') return result;
378
- return `Launched: ${args.app}`;
202
+ async execute() {
203
+ return stringifyResult(await client.rpcWithPermissionFlow('desktop.cua.status', {}));
379
204
  },
380
205
  },
381
206
  {
382
- name: 'tunnel_app_quit',
383
- description: `Quit an application on the user's machine via Agent Tunnel.`,
207
+ name: 'tunnel_cua_version',
208
+ description: `Read the installed CUA Driver version on the connected computer.`,
384
209
  parameters: {
385
210
  tunnel_id: tunnelIdParam,
386
- app: { type: 'string', description: 'Application name to quit', required: true },
387
211
  },
388
- async execute(args) {
389
- const result = await client.rpcWithPermissionFlow('desktop.app.quit', { app: args.app });
390
- if (typeof result === 'string') return result;
391
- return `Quit: ${args.app}`;
212
+ async execute() {
213
+ return stringifyResult(await client.rpcWithPermissionFlow('desktop.cua.version', {}));
392
214
  },
393
215
  },
394
-
395
216
  {
396
- name: 'tunnel_clipboard_read',
397
- description: `Read the clipboard contents from the user's machine via Agent Tunnel.`,
217
+ name: 'tunnel_cua_list_tools',
218
+ description: `List every CUA Driver tool exposed by the connected computer's installed driver.`,
398
219
  parameters: {
399
220
  tunnel_id: tunnelIdParam,
400
221
  },
401
222
  async execute() {
402
- const result = await client.rpcWithPermissionFlow('desktop.clipboard.read', {});
403
- if (typeof result === 'string') return result;
404
- const data = result as { text: string };
405
- if (!data.text) return '(clipboard is empty)';
406
- return `=== Clipboard ===\n${data.text}`;
223
+ return stringifyResult(await client.rpcWithPermissionFlow('desktop.cua.list_tools', {}));
407
224
  },
408
225
  },
409
226
  {
410
- name: 'tunnel_clipboard_write',
411
- description: `Write text to the clipboard on the user's machine via Agent Tunnel.`,
227
+ name: 'tunnel_cua_describe',
228
+ description: `Describe one CUA Driver tool before calling it.`,
412
229
  parameters: {
413
230
  tunnel_id: tunnelIdParam,
414
- text: { type: 'string', description: 'Text to write to clipboard', required: true },
231
+ tool: { type: 'string', description: 'CUA Driver tool name', required: true },
415
232
  },
416
233
  async execute(args) {
417
- const result = await client.rpcWithPermissionFlow('desktop.clipboard.write', { text: args.text });
418
- if (typeof result === 'string') return result;
419
- return `Clipboard updated (${(args.text as string).length} chars)`;
234
+ return stringifyResult(await client.rpcWithPermissionFlow('desktop.cua.describe', {
235
+ tool: args.tool,
236
+ }));
420
237
  },
421
238
  },
422
-
423
239
  {
424
- name: 'tunnel_screen_info',
425
- description: `Get screen resolution and scale factor from the user's machine via Agent Tunnel.`,
240
+ name: 'tunnel_cua_list_apps',
241
+ description: `List installed and running desktop apps through CUA Driver, including bundle IDs and PIDs.`,
426
242
  parameters: {
427
243
  tunnel_id: tunnelIdParam,
428
244
  },
429
245
  async execute() {
430
- const result = await client.rpcWithPermissionFlow('desktop.screen.info', {});
431
- if (typeof result === 'string') return result;
432
- const data = result as { width: number; height: number; scaleFactor: number };
433
- return `Screen: ${data.width}x${data.height} @ ${data.scaleFactor}x scale`;
246
+ return stringifyResult(await client.rpcWithPermissionFlow('desktop.cua.list_apps', {}));
434
247
  },
435
248
  },
436
-
437
249
  {
438
- name: 'tunnel_cursor_image',
439
- description: `Take a small screenshot around the current cursor position on the user's machine via Agent Tunnel.`,
250
+ name: 'tunnel_cua_list_windows',
251
+ description: `List top-level local desktop windows through CUA Driver. Use this before tunnel_cua_get_window_state.`,
440
252
  parameters: {
441
253
  tunnel_id: tunnelIdParam,
442
- radius: { type: 'number', description: 'Radius in pixels around cursor (default: 50)', required: false },
254
+ pid: { type: 'number', description: 'Optional process ID filter', required: false },
255
+ on_screen_only: { type: 'boolean', description: 'Only include windows on the current Space', required: false },
443
256
  },
444
257
  async execute(args) {
445
- const result = await client.rpcWithPermissionFlow('desktop.cursor.image', { radius: args.radius });
446
- if (typeof result === 'string') return result;
447
-
448
- const data = result as { image: string; width: number; height: number; format?: string };
449
- const format = data.format || 'png';
450
- const sizeKB = Math.round(data.image.length * 0.75 / 1024);
451
- const path = saveImage(data.image, format);
452
- return `Cursor area saved: ${path}\nDimensions: ${data.width}x${data.height} ${format.toUpperCase()} (${sizeKB}KB)\n\nUse the Read tool to view this image.`;
258
+ return stringifyResult(await client.rpcWithPermissionFlow('desktop.cua.list_windows', {
259
+ pid: args.pid,
260
+ on_screen_only: args.on_screen_only,
261
+ }));
453
262
  },
454
263
  },
455
-
456
264
  {
457
- name: 'tunnel_ax_tree',
458
- description: `Get the accessibility tree of an application on the user's machine via Agent Tunnel. Returns a structured tree of UI elements with roles, labels, states, and available actions.`,
265
+ name: 'tunnel_cua_get_window_state',
266
+ description: `Get a CUA window snapshot: screenshot plus accessibility tree markdown with element_index values. Call this once per turn before element-indexed actions.`,
459
267
  parameters: {
460
268
  tunnel_id: tunnelIdParam,
461
- pid: { type: 'number', description: 'Process ID of the target application (omit for all apps)', required: false },
462
- maxDepth: { type: 'number', description: 'Maximum tree depth (default: 8)', required: false },
463
- roles: { type: 'array', description: "Filter by element roles (e.g., ['button', 'textfield'])", required: false, items: { type: 'string' } },
269
+ pid: { type: 'number', description: 'Target process ID', required: true },
270
+ window_id: { type: 'number', description: 'Target window ID from tunnel_cua_list_windows', required: true },
271
+ query: { type: 'string', description: 'Optional filter for tree markdown', required: false },
272
+ capture_mode: { type: 'string', description: 'som, vision, or ax', required: false, enum: ['som', 'vision', 'ax'] },
464
273
  },
465
274
  async execute(args) {
466
- const params: Record<string, unknown> = {};
467
- if (args.pid !== undefined) params.pid = args.pid;
468
- if (args.maxDepth !== undefined) params.maxDepth = args.maxDepth;
469
- if (args.roles !== undefined) params.roles = args.roles;
470
-
471
- const result = await client.rpcWithPermissionFlow('desktop.ax.tree', params);
472
- if (typeof result === 'string') return result;
473
-
474
- const data = result as { root: AXElement; elementCount: number };
475
- if (!data.root) return 'No accessibility tree available';
476
-
477
- const tree = formatAXTree(data.root);
478
- return `=== Accessibility Tree (${data.elementCount} elements) ===\n${tree}`;
275
+ return stringifyResult(await client.rpcWithPermissionFlow('desktop.cua.get_window_state', {
276
+ pid: args.pid,
277
+ window_id: args.window_id,
278
+ query: args.query,
279
+ capture_mode: args.capture_mode,
280
+ }));
479
281
  },
480
282
  },
481
283
  {
482
- name: 'tunnel_ax_action',
483
- description: `Perform an accessibility action on a UI element via Agent Tunnel. Returns before/after state to verify the action worked. Common actions: AXPress, AXConfirm, AXCancel, AXRaise, AXShowMenu.`,
284
+ name: 'tunnel_cua_click',
285
+ description: `Click with CUA Driver by element_index from the last window state, or by window-local screenshot coordinates.`,
484
286
  parameters: {
485
287
  tunnel_id: tunnelIdParam,
486
- elementId: { type: 'string', description: "Element ID from the accessibility tree (e.g., '0.3.1')", required: true },
487
- action: { type: 'string', description: 'Action to perform: AXPress, AXConfirm, AXCancel, AXRaise, AXShowMenu', required: true },
488
- pid: { type: 'number', description: 'Process ID of the target application', required: false },
288
+ pid: { type: 'number', description: 'Target process ID', required: true },
289
+ window_id: { type: 'number', description: 'Target window ID; required for element_index', required: false },
290
+ element_index: { type: 'number', description: 'Element index from tunnel_cua_get_window_state', required: false },
291
+ x: { type: 'number', description: 'Window-local screenshot X coordinate', required: false },
292
+ y: { type: 'number', description: 'Window-local screenshot Y coordinate', required: false },
293
+ action: { type: 'string', description: 'AX action: press, show_menu, pick, confirm, cancel, open', required: false },
489
294
  },
490
295
  async execute(args) {
491
- const result = await client.rpcWithPermissionFlow('desktop.ax.action', {
492
- elementId: args.elementId, action: args.action, pid: args.pid,
493
- });
494
- if (typeof result === 'string') return result;
495
-
496
- const data = result as {
497
- ok: boolean; action: string; elementId: string;
498
- before: { focused: boolean; value: string };
499
- after: { focused: boolean; value: string };
500
- stateChanged: boolean; role: string; title: string;
501
- };
502
-
503
- const lines = [`Action "${data.action}" on [${data.role}] "${data.title}" (${data.elementId})`];
504
- lines.push(`State changed: ${data.stateChanged ? 'YES' : 'NO'}`);
505
- lines.push(`Before: focused=${data.before.focused}, value="${data.before.value}"`);
506
- lines.push(`After: focused=${data.after.focused}, value="${data.after.value}"`);
507
- if (!data.stateChanged) {
508
- lines.push('WARNING: No state change detected. The action may not have had any effect.');
509
- }
510
- return lines.join('\n');
296
+ return stringifyResult(await client.rpcWithPermissionFlow('desktop.cua.click', args));
511
297
  },
512
298
  },
513
299
  {
514
- name: 'tunnel_ax_set_value',
515
- description: `Directly set the value of a UI element (text field, search box, etc.) via the accessibility API. Much more reliable than clicking and typing.`,
300
+ name: 'tunnel_cua_type_text',
301
+ description: `Type text with CUA Driver into a target PID, optionally directed to an element_index from the last window state.`,
516
302
  parameters: {
517
303
  tunnel_id: tunnelIdParam,
518
- elementId: { type: 'string', description: "Element ID from the accessibility tree (e.g., '0.3.1')", required: true },
519
- value: { type: 'string', description: 'The value to set (e.g., text to put in a search field)', required: true },
520
- pid: { type: 'number', description: 'Process ID of the target application', required: false },
304
+ pid: { type: 'number', description: 'Target process ID', required: true },
305
+ text: { type: 'string', description: 'Text to type', required: true },
306
+ window_id: { type: 'number', description: 'Target window ID; required for element_index', required: false },
307
+ element_index: { type: 'number', description: 'Element index from tunnel_cua_get_window_state', required: false },
521
308
  },
522
309
  async execute(args) {
523
- const result = await client.rpcWithPermissionFlow('desktop.ax.set_value', {
524
- elementId: args.elementId, value: args.value, pid: args.pid,
525
- });
526
- if (typeof result === 'string') return result;
527
-
528
- const data = result as {
529
- ok: boolean; elementId: string;
530
- requestedValue: string; actualValue: string; error?: string;
531
- };
532
-
533
- if (data.ok) {
534
- return `Value set successfully on ${data.elementId}\nRequested: "${data.requestedValue}"\nVerified: "${data.actualValue}"`;
535
- } else {
536
- return `FAILED to set value on ${data.elementId}\nRequested: "${data.requestedValue}"\nActual: "${data.actualValue}"\nError: ${data.error || 'unknown'}`;
537
- }
310
+ return stringifyResult(await client.rpcWithPermissionFlow('desktop.cua.type_text', args));
538
311
  },
539
312
  },
540
313
  {
541
- name: 'tunnel_ax_focus',
542
- description: `Focus a UI element directly via the accessibility API. More reliable than clicking to focus.`,
314
+ name: 'tunnel_cua_hotkey',
315
+ description: `Press a CUA Driver hotkey against a target PID, for example ["cmd","c"].`,
543
316
  parameters: {
544
317
  tunnel_id: tunnelIdParam,
545
- elementId: { type: 'string', description: "Element ID from the accessibility tree (e.g., '0.3.1')", required: true },
546
- pid: { type: 'number', description: 'Process ID of the target application', required: false },
318
+ pid: { type: 'number', description: 'Target process ID', required: true },
319
+ keys: { type: 'array', description: 'Modifier(s) and one key, e.g. ["cmd","c"]', required: true, items: { type: 'string' } },
320
+ window_id: { type: 'number', description: 'Optional target window ID for native menu dispatch', required: false },
547
321
  },
548
322
  async execute(args) {
549
- const result = await client.rpcWithPermissionFlow('desktop.ax.focus', {
550
- elementId: args.elementId, pid: args.pid,
551
- });
552
- if (typeof result === 'string') return result;
553
-
554
- const data = result as {
555
- ok: boolean; elementId: string; role: string; title: string;
556
- before: { focused: boolean }; after: { focused: boolean }; error?: string;
557
- };
558
-
559
- if (data.ok) {
560
- return `Focused [${data.role}] "${data.title}" (${data.elementId})\nBefore: focused=${data.before.focused}\nAfter: focused=${data.after.focused}`;
561
- } else {
562
- return `FAILED to focus [${data.role}] "${data.title}" (${data.elementId})\nError: ${data.error || 'unknown'}\nBefore: focused=${data.before.focused}\nAfter: focused=${data.after.focused}`;
563
- }
323
+ return stringifyResult(await client.rpcWithPermissionFlow('desktop.cua.hotkey', args));
564
324
  },
565
325
  },
566
326
  {
567
- name: 'tunnel_ax_search',
568
- description: `Search the accessibility tree for UI elements matching a query via Agent Tunnel. Case-insensitive substring match on titles, values, and descriptions.`,
327
+ name: 'tunnel_cua_call',
328
+ description: `Call any CUA Driver tool by name with raw JSON args. Prefer the specific tunnel_cua_* tools when available.`,
569
329
  parameters: {
570
330
  tunnel_id: tunnelIdParam,
571
- query: { type: 'string', description: 'Search text (matches against title, value, description)', required: true },
572
- role: { type: 'string', description: "Filter by element role (e.g., 'button', 'textfield')", required: false },
573
- pid: { type: 'number', description: 'Process ID of the target application', required: false },
574
- maxResults: { type: 'number', description: 'Maximum results to return (default: 20)', required: false },
331
+ tool: { type: 'string', description: 'CUA Driver tool name', required: true },
332
+ args: { type: 'object', description: 'Raw CUA Driver tool arguments', required: false },
575
333
  },
576
334
  async execute(args) {
577
- const params: Record<string, unknown> = { query: args.query };
578
- if (args.role !== undefined) params.role = args.role;
579
- if (args.pid !== undefined) params.pid = args.pid;
580
- if (args.maxResults !== undefined) params.maxResults = args.maxResults;
581
-
582
- const result = await client.rpcWithPermissionFlow('desktop.ax.search', params);
583
- if (typeof result === 'string') return result;
584
-
585
- const data = result as { elements: AXElement[] };
586
- if (!data.elements || data.elements.length === 0) return `No elements found matching "${args.query}"`;
587
-
588
- const lines = [`=== AX Search: "${args.query}" (${data.elements.length} results) ===`];
589
- for (const el of data.elements) {
590
- const label = el.title || el.value || el.description || '(unnamed)';
591
- const flags: string[] = [];
592
- if (!el.enabled) flags.push('disabled');
593
- if (el.focused) flags.push('focused');
594
- if (el.actions.length > 0) flags.push(`actions: ${el.actions.join(',')}`);
595
- const flagStr = flags.length > 0 ? ` [${flags.join(', ')}]` : '';
596
- const b = el.bounds;
597
- lines.push(` [${el.role}] ${label} (id: ${el.id}) @ ${b.x},${b.y} ${b.width}x${b.height}${flagStr}`);
598
- }
599
- return lines.join('\n');
335
+ return stringifyResult(await client.rpcWithPermissionFlow('desktop.cua.call', {
336
+ tool: args.tool,
337
+ args: args.args || {},
338
+ }));
600
339
  },
601
340
  },
602
341
  ];