@kortix/agent-tunnel 0.1.4 → 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 +202 -493
  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 -539
  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
package/src/client/cli.ts CHANGED
@@ -1,115 +1,72 @@
1
1
  /**
2
- * Agent Tunnel CLI interact with the user's local machine via Agent Tunnel.
2
+ * Agent Tunnel CLI: filesystem, shell, and CUA Driver-backed computer control.
3
3
  *
4
4
  * Usage: agent-tunnel-cli <command> [args as JSON]
5
- *
6
- * Commands:
7
- * status — list all tunnel connections
8
- * fs_read '{"path":"/Users/me/file.txt"}' — read a file
9
- * fs_write '{"path":"/tmp/out.txt","content":"hello"}' — write a file
10
- * fs_list '{"path":"/Users/me"}' — list directory
11
- * shell '{"command":"git","args":["status"]}' — run a command
12
- * screenshot — take a screenshot
13
- * click '{"x":100,"y":200}' — click at coordinates
14
- * mouse_move '{"x":100,"y":200}' — move mouse
15
- * mouse_drag '{"fromX":0,"fromY":0,"toX":100,"toY":100}' — drag
16
- * mouse_scroll '{"x":500,"y":500,"deltaY":3}' — scroll
17
- * type '{"text":"hello world"}' — type text
18
- * key '{"keys":["cmd","s"]}' — press key combo
19
- * window_list — list windows
20
- * window_focus '{"windowId":123}' — focus a window
21
- * app_launch '{"app":"Safari"}' — launch app
22
- * app_quit '{"app":"Safari"}' — quit app
23
- * clipboard_read — read clipboard
24
- * clipboard_write '{"text":"copied"}' — write clipboard
25
- * screen_info — get screen resolution
26
- * cursor_image — screenshot around cursor
27
- * ax_tree '{"pid":1234}' — accessibility tree
28
- * ax_action '{"elementId":"0.3.1","action":"AXPress"}' — perform AX action
29
- * ax_set_value '{"elementId":"0.3.1","value":"hello"}' — set element value
30
- * ax_focus '{"elementId":"0.3.1"}' — focus element
31
- * ax_search '{"query":"Submit"}' — search AX tree
32
5
  */
33
6
 
34
- import { writeFileSync, mkdirSync, readFileSync } from "fs";
35
- import { join } from "path";
36
- import { tmpdir } from "os";
37
- import { randomBytes } from "crypto";
38
- import { TunnelClient, TunnelClientError } from "./tunnel-client";
39
- import type { AXElement } from "./tunnel-client";
7
+ import { readFileSync } from 'fs';
8
+ import { TunnelClient, TunnelClientError } from './tunnel-client';
40
9
 
41
- // ── Env resolution (s6 → process.env) ─────────────────────────────────────
10
+ const S6_ENV_DIR = process.env.S6_ENV_DIR || '/run/s6/container_environment';
11
+ const FALLBACK_API_URL = 'http://localhost:8008';
42
12
 
43
- const S6_ENV_DIR = process.env.S6_ENV_DIR || "/run/s6/container_environment";
13
+ const ALL_COMMANDS = [
14
+ 'status',
15
+ 'fs_read',
16
+ 'fs_write',
17
+ 'fs_list',
18
+ 'shell',
19
+ 'cua_ensure',
20
+ 'cua_start_daemon',
21
+ 'cua_status',
22
+ 'cua_version',
23
+ 'cua_list_tools',
24
+ 'cua_describe',
25
+ 'cua_call',
26
+ 'cua_list_apps',
27
+ 'cua_list_windows',
28
+ 'cua_get_window_state',
29
+ 'cua_click',
30
+ 'cua_type_text',
31
+ 'cua_hotkey',
32
+ ];
44
33
 
45
34
  function getEnv(key: string): string | undefined {
46
35
  try {
47
- const val = readFileSync(`${S6_ENV_DIR}/${key}`, "utf-8").trim();
48
- if (val) return val;
36
+ const value = readFileSync(`${S6_ENV_DIR}/${key}`, 'utf-8').trim();
37
+ if (value) return value;
49
38
  } catch {}
50
39
  return process.env[key];
51
40
  }
52
41
 
53
- // ── Client setup ──────────────────────────────────────────────────────────
54
-
55
- const FALLBACK_API_URL = "http://localhost:8008";
56
-
57
42
  function getApiBase(): string {
58
- const raw = getEnv("TUNNEL_API_URL") || FALLBACK_API_URL;
59
- const url = raw.startsWith("http") ? raw : FALLBACK_API_URL;
60
- // Strip /v1/router suffix — TUNNEL_API_URL sometimes includes the router path
61
- // but the CLI appends its own /v1/tunnel path.
62
- return url.replace(/\/+$/, "").replace(/\/v1\/router\/?$/, "");
43
+ const raw = getEnv('TUNNEL_API_URL') || FALLBACK_API_URL;
44
+ const url = raw.startsWith('http') ? raw : FALLBACK_API_URL;
45
+ return url.replace(/\/+$/, '').replace(/\/v1\/router\/?$/, '');
63
46
  }
64
47
 
65
48
  const client = new TunnelClient({
66
49
  apiUrl: `${getApiBase()}/v1/tunnel`,
67
- token: getEnv("TUNNEL_TOKEN") || "",
68
- tunnelId: getEnv("TUNNEL_ID"),
50
+ token: getEnv('TUNNEL_TOKEN') || '',
51
+ tunnelId: getEnv('TUNNEL_ID'),
69
52
  });
70
53
 
71
- // ── Helpers ───────────────────────────────────────────────────────────────
72
-
73
54
  function out(data: unknown): void {
74
55
  console.log(JSON.stringify(data, null, 2));
75
56
  }
76
57
 
77
- function saveImage(base64: string, format: string): string {
78
- const ext = format === "jpeg" || format === "jpg" ? "jpg" : "png";
79
- const dir = join(tmpdir(), "tunnel-screenshots");
80
- mkdirSync(dir, { recursive: true });
81
- const path = join(
82
- dir,
83
- `screenshot-${randomBytes(4).toString("hex")}.${ext}`
84
- );
85
- writeFileSync(path, Buffer.from(base64, "base64"));
86
- return path;
87
- }
88
-
89
- function formatAXTree(el: AXElement, indent = 0): string {
90
- const pad = " ".repeat(indent);
91
- const parts: string[] = [];
92
-
93
- const label = el.title || el.value || el.description || "(unnamed)";
94
- const flags: string[] = [];
95
- if (!el.enabled) flags.push("disabled");
96
- if (el.focused) flags.push("focused");
97
- if (el.actions.length > 0) flags.push(`actions: ${el.actions.join(",")}`);
98
- const flagStr = flags.length > 0 ? ` [${flags.join(", ")}]` : "";
99
-
100
- parts.push(`${pad}[${el.role}] ${label} (id: ${el.id})${flagStr}`);
101
-
102
- for (const child of el.children) {
103
- parts.push(formatAXTree(child, indent + 1));
104
- }
105
-
106
- return parts.join("\n");
58
+ function fail(message: string): void {
59
+ out({ success: false, error: message });
60
+ process.exitCode = 1;
107
61
  }
108
62
 
109
63
  async function rpcSafe(
110
64
  method: string,
111
- params: Record<string, unknown> = {}
112
- ): Promise<{ result: unknown; permissionRequired: false } | { result: null; permissionRequired: true; requestId: string; message: string }> {
65
+ params: Record<string, unknown> = {},
66
+ ): Promise<
67
+ | { result: unknown; permissionRequired: false }
68
+ | { result: null; permissionRequired: true; requestId: string; message: string }
69
+ > {
113
70
  try {
114
71
  const result = await client.rpc(method, params);
115
72
  return { result, permissionRequired: false };
@@ -118,45 +75,36 @@ async function rpcSafe(
118
75
  return {
119
76
  result: null,
120
77
  permissionRequired: true,
121
- requestId: err.requestId || "unknown",
122
- message: `Permission required. A permission request (${err.requestId}) has been sent to the user for approval. The user needs to approve this request before you can access their local machine. Please inform the user and try again after they approve.`,
78
+ requestId: err.requestId || 'unknown',
79
+ message: `Permission required. A permission request (${err.requestId}) has been sent to the user for approval.`,
123
80
  };
124
81
  }
125
82
  throw err;
126
83
  }
127
84
  }
128
85
 
129
- // Wrapper: call rpcSafe, handle permission, return result or null
130
- async function call(
131
- method: string,
132
- params: Record<string, unknown> = {}
133
- ): Promise<unknown | null> {
134
- const r = await rpcSafe(method, params);
135
- if (r.permissionRequired) {
86
+ async function call(method: string, params: Record<string, unknown> = {}): Promise<unknown | null> {
87
+ const response = await rpcSafe(method, params);
88
+ if (response.permissionRequired) {
136
89
  out({
137
90
  success: false,
138
91
  permissionRequired: true,
139
- requestId: r.requestId,
140
- message: r.message,
92
+ requestId: response.requestId,
93
+ message: response.message,
141
94
  });
142
95
  return null;
143
96
  }
144
- return r.result;
97
+ return response.result;
145
98
  }
146
99
 
147
- // ── Commands ──────────────────────────────────────────────────────────────
148
-
149
100
  async function status() {
150
- const connections = (await client.getConnections()) as Array<
151
- Record<string, unknown>
152
- >;
101
+ const connections = (await client.getConnections()) as Array<Record<string, unknown>>;
153
102
 
154
103
  if (connections.length === 0) {
155
104
  return out({
156
105
  success: true,
157
106
  connections: [],
158
- message:
159
- "No tunnel connections found. The user needs to set up Agent Tunnel first.",
107
+ message: 'No tunnel connections found. The user needs to set up Agent Tunnel first.',
160
108
  });
161
109
  }
162
110
 
@@ -164,9 +112,9 @@ async function status() {
164
112
  const mapped = connections.map((data) => {
165
113
  if (data.isLive) hasOnline = true;
166
114
  return {
167
- name: data.name || "Unnamed",
115
+ name: data.name || 'Unnamed',
168
116
  tunnelId: data.tunnelId,
169
- status: data.isLive ? "ONLINE" : "OFFLINE",
117
+ status: data.isLive ? 'ONLINE' : 'OFFLINE',
170
118
  capabilities: (data.capabilities as string[]) || [],
171
119
  machineInfo: data.machineInfo || {},
172
120
  };
@@ -178,30 +126,25 @@ async function status() {
178
126
  hasOnline,
179
127
  message: hasOnline
180
128
  ? undefined
181
- : "No tunnel is currently online. Ask the user to run `npx @kortix/agent-tunnel connect` on their local machine.",
129
+ : 'No tunnel is currently online. Ask the user to run `npx --yes @kortix/agent-tunnel@latest connect` on their local machine.',
182
130
  });
183
131
  }
184
132
 
185
133
  async function fsRead(args: Record<string, unknown>) {
186
- const result = await call("fs.read", {
134
+ const result = await call('fs.read', {
187
135
  path: args.path,
188
- encoding: (args.encoding as string) || "utf-8",
136
+ encoding: (args.encoding as string) || 'utf-8',
189
137
  });
190
138
  if (result === null) return;
191
139
  const data = result as Record<string, unknown>;
192
- out({
193
- success: true,
194
- path: data.path || args.path,
195
- size: data.size,
196
- content: data.content,
197
- });
140
+ out({ success: true, path: data.path || args.path, size: data.size, content: data.content });
198
141
  }
199
142
 
200
143
  async function fsWrite(args: Record<string, unknown>) {
201
- const result = await call("fs.write", {
144
+ const result = await call('fs.write', {
202
145
  path: args.path,
203
146
  content: args.content,
204
- encoding: (args.encoding as string) || "utf-8",
147
+ encoding: (args.encoding as string) || 'utf-8',
205
148
  });
206
149
  if (result === null) return;
207
150
  const data = result as Record<string, unknown>;
@@ -209,30 +152,17 @@ async function fsWrite(args: Record<string, unknown>) {
209
152
  }
210
153
 
211
154
  async function fsList(args: Record<string, unknown>) {
212
- const result = await call("fs.list", {
155
+ const result = await call('fs.list', {
213
156
  path: args.path,
214
157
  recursive: args.recursive || false,
215
158
  });
216
159
  if (result === null) return;
217
- const data = result as {
218
- entries: Array<{
219
- name: string;
220
- path: string;
221
- isDirectory: boolean;
222
- isFile: boolean;
223
- }>;
224
- count: number;
225
- };
226
- out({
227
- success: true,
228
- path: args.path,
229
- count: data.count,
230
- entries: data.entries,
231
- });
160
+ const data = result as { entries: unknown[]; count: number };
161
+ out({ success: true, path: args.path, count: data.count, entries: data.entries });
232
162
  }
233
163
 
234
164
  async function shell(args: Record<string, unknown>) {
235
- const result = await call("shell.exec", {
165
+ const result = await call('shell.exec', {
236
166
  command: args.command,
237
167
  args: (args.args as string[]) || [],
238
168
  cwd: args.cwd,
@@ -258,456 +188,103 @@ async function shell(args: Record<string, unknown>) {
258
188
  });
259
189
  }
260
190
 
261
- async function screenshot(args: Record<string, unknown>) {
262
- const params: Record<string, unknown> = {};
263
- if (
264
- args.x !== undefined &&
265
- args.y !== undefined &&
266
- args.width !== undefined &&
267
- args.height !== undefined
268
- ) {
269
- params.region = {
270
- x: args.x,
271
- y: args.y,
272
- width: args.width,
273
- height: args.height,
274
- };
275
- }
276
- if (args.windowId !== undefined) params.windowId = args.windowId;
277
-
278
- const result = await call("desktop.screenshot", params);
279
- if (result === null) return;
280
- const data = result as {
281
- image: string;
282
- width: number;
283
- height: number;
284
- format?: string;
285
- };
286
- const format = data.format || "png";
287
- const sizeKB = Math.round((data.image.length * 0.75) / 1024);
288
- const path = saveImage(data.image, format);
289
- out({
290
- success: true,
291
- path,
292
- width: data.width,
293
- height: data.height,
294
- format,
295
- sizeKB,
296
- message: `Screenshot saved: ${path} (${data.width}x${data.height} ${format.toUpperCase()}, ${sizeKB}KB). Use the Read tool to view.`,
297
- });
298
- }
299
-
300
- async function click(args: Record<string, unknown>) {
301
- const result = await call("desktop.mouse.click", {
302
- x: args.x,
303
- y: args.y,
304
- button: args.button,
305
- clicks: args.clicks,
306
- modifiers: args.modifiers,
307
- });
308
- if (result === null) return;
309
- out({
310
- success: true,
311
- x: args.x,
312
- y: args.y,
313
- button: args.button || "left",
314
- clicks: args.clicks || 1,
315
- });
316
- }
317
-
318
- async function mouseMove(args: Record<string, unknown>) {
319
- const result = await call("desktop.mouse.move", { x: args.x, y: args.y });
320
- if (result === null) return;
321
- out({ success: true, x: args.x, y: args.y });
322
- }
323
-
324
- async function mouseDrag(args: Record<string, unknown>) {
325
- const result = await call("desktop.mouse.drag", {
326
- fromX: args.fromX,
327
- fromY: args.fromY,
328
- toX: args.toX,
329
- toY: args.toY,
330
- button: args.button,
331
- });
332
- if (result === null) return;
333
- out({
334
- success: true,
335
- fromX: args.fromX,
336
- fromY: args.fromY,
337
- toX: args.toX,
338
- toY: args.toY,
339
- });
340
- }
341
-
342
- async function mouseScroll(args: Record<string, unknown>) {
343
- const result = await call("desktop.mouse.scroll", {
344
- x: args.x,
345
- y: args.y,
346
- deltaX: args.deltaX,
347
- deltaY: args.deltaY,
348
- });
349
- if (result === null) return;
350
- out({
351
- success: true,
352
- x: args.x,
353
- y: args.y,
354
- deltaX: args.deltaX || 0,
355
- deltaY: args.deltaY || 0,
356
- });
357
- }
358
-
359
- async function typeText(args: Record<string, unknown>) {
360
- const result = await call("desktop.keyboard.type", {
361
- text: args.text,
362
- delay: args.delay,
363
- });
364
- if (result === null) return;
365
- out({ success: true, chars: (args.text as string).length });
366
- }
367
-
368
- async function pressKey(args: Record<string, unknown>) {
369
- const result = await call("desktop.keyboard.key", { keys: args.keys });
370
- if (result === null) return;
371
- out({ success: true, keys: args.keys });
372
- }
373
-
374
- async function windowList() {
375
- const result = await call("desktop.window.list", {});
376
- if (result === null) return;
377
- const data = result as {
378
- windows: Array<{
379
- id: number;
380
- app: string;
381
- title: string;
382
- bounds: { x: number; y: number; width: number; height: number };
383
- minimized: boolean;
384
- }>;
385
- };
386
- out({ success: true, windows: data.windows });
387
- }
388
-
389
- async function windowFocus(args: Record<string, unknown>) {
390
- const result = await call("desktop.window.focus", {
391
- windowId: args.windowId,
392
- });
393
- if (result === null) return;
394
- out({ success: true, windowId: args.windowId });
395
- }
396
-
397
- async function appLaunch(args: Record<string, unknown>) {
398
- const result = await call("desktop.app.launch", { app: args.app });
399
- if (result === null) return;
400
- out({ success: true, app: args.app });
401
- }
402
-
403
- async function appQuit(args: Record<string, unknown>) {
404
- const result = await call("desktop.app.quit", { app: args.app });
405
- if (result === null) return;
406
- out({ success: true, app: args.app });
407
- }
408
-
409
- async function clipboardRead() {
410
- const result = await call("desktop.clipboard.read", {});
411
- if (result === null) return;
412
- const data = result as { text: string };
413
- out({ success: true, text: data.text || "" });
414
- }
415
-
416
- async function clipboardWrite(args: Record<string, unknown>) {
417
- const result = await call("desktop.clipboard.write", { text: args.text });
418
- if (result === null) return;
419
- out({ success: true, chars: (args.text as string).length });
420
- }
421
-
422
- async function screenInfo() {
423
- const result = await call("desktop.screen.info", {});
424
- if (result === null) return;
425
- const data = result as {
426
- width: number;
427
- height: number;
428
- scaleFactor: number;
429
- };
430
- out({ success: true, ...data });
431
- }
432
-
433
- async function cursorImage(args: Record<string, unknown>) {
434
- const result = await call("desktop.cursor.image", { radius: args.radius });
435
- if (result === null) return;
436
- const data = result as {
437
- image: string;
438
- width: number;
439
- height: number;
440
- format?: string;
441
- };
442
- const format = data.format || "png";
443
- const sizeKB = Math.round((data.image.length * 0.75) / 1024);
444
- const path = saveImage(data.image, format);
445
- out({
446
- success: true,
447
- path,
448
- width: data.width,
449
- height: data.height,
450
- format,
451
- sizeKB,
452
- message: `Cursor area saved: ${path} (${data.width}x${data.height}). Use the Read tool to view.`,
453
- });
454
- }
455
-
456
- async function axTree(args: Record<string, unknown>) {
457
- const params: Record<string, unknown> = {};
458
- if (args.pid !== undefined) params.pid = args.pid;
459
- if (args.maxDepth !== undefined) params.maxDepth = args.maxDepth;
460
- if (args.roles !== undefined) params.roles = args.roles;
461
-
462
- const result = await call("desktop.ax.tree", params);
191
+ async function cuaRpc(method: string, args: Record<string, unknown> = {}) {
192
+ const result = await call(method, args);
463
193
  if (result === null) return;
464
- const data = result as { root: AXElement; elementCount: number };
465
- if (!data.root)
466
- return out({
467
- success: true,
468
- tree: null,
469
- message: "No accessibility tree available",
470
- });
471
-
472
- out({
473
- success: true,
474
- elementCount: data.elementCount,
475
- tree: formatAXTree(data.root),
476
- });
477
- }
478
-
479
- async function axAction(args: Record<string, unknown>) {
480
- const result = await call("desktop.ax.action", {
481
- elementId: args.elementId,
482
- action: args.action,
483
- pid: args.pid,
484
- });
485
- if (result === null) return;
486
- const data = result as {
487
- ok: boolean;
488
- action: string;
489
- elementId: string;
490
- before: { focused: boolean; value: string };
491
- after: { focused: boolean; value: string };
492
- stateChanged: boolean;
493
- role: string;
494
- title: string;
495
- };
496
- out({
497
- success: data.ok,
498
- action: data.action,
499
- elementId: data.elementId,
500
- role: data.role,
501
- title: data.title,
502
- stateChanged: data.stateChanged,
503
- before: data.before,
504
- after: data.after,
505
- });
194
+ out({ success: true, result });
506
195
  }
507
196
 
508
- async function axSetValue(args: Record<string, unknown>) {
509
- const result = await call("desktop.ax.set_value", {
510
- elementId: args.elementId,
511
- value: args.value,
512
- pid: args.pid,
513
- });
514
- if (result === null) return;
515
- const data = result as {
516
- ok: boolean;
517
- elementId: string;
518
- requestedValue: string;
519
- actualValue: string;
520
- error?: string;
521
- };
522
- out({
523
- success: data.ok,
524
- elementId: data.elementId,
525
- requestedValue: data.requestedValue,
526
- actualValue: data.actualValue,
527
- error: data.error,
528
- });
529
- }
197
+ async function cuaCall(args: Record<string, unknown>) {
198
+ const tool = args.tool as string;
199
+ if (!tool) return fail('tool is required');
530
200
 
531
- async function axFocus(args: Record<string, unknown>) {
532
- const result = await call("desktop.ax.focus", {
533
- elementId: args.elementId,
534
- pid: args.pid,
201
+ const result = await call('desktop.cua.call', {
202
+ tool,
203
+ args: (args.args || {}) as Record<string, unknown>,
535
204
  });
536
205
  if (result === null) return;
537
- const data = result as {
538
- ok: boolean;
539
- elementId: string;
540
- role: string;
541
- title: string;
542
- before: { focused: boolean };
543
- after: { focused: boolean };
544
- error?: string;
545
- };
546
- out({
547
- success: data.ok,
548
- elementId: data.elementId,
549
- role: data.role,
550
- title: data.title,
551
- before: data.before,
552
- after: data.after,
553
- error: data.error,
554
- });
206
+ out({ success: true, tool, result });
555
207
  }
556
208
 
557
- async function axSearch(args: Record<string, unknown>) {
558
- const params: Record<string, unknown> = { query: args.query };
559
- if (args.role !== undefined) params.role = args.role;
560
- if (args.pid !== undefined) params.pid = args.pid;
561
- if (args.maxResults !== undefined) params.maxResults = args.maxResults;
562
-
563
- const result = await call("desktop.ax.search", params);
564
- if (result === null) return;
565
- const data = result as { elements: AXElement[] };
566
-
567
- if (!data.elements || data.elements.length === 0) {
568
- return out({
569
- success: true,
570
- query: args.query,
571
- elements: [],
572
- message: `No elements found matching "${args.query}"`,
573
- });
574
- }
575
-
576
- const elements = data.elements.map((el) => ({
577
- id: el.id,
578
- role: el.role,
579
- title: el.title || el.value || el.description || "(unnamed)",
580
- bounds: el.bounds,
581
- enabled: el.enabled,
582
- focused: el.focused,
583
- actions: el.actions,
584
- }));
585
-
586
- out({ success: true, query: args.query, count: elements.length, elements });
209
+ async function cuaDescribe(args: Record<string, unknown>) {
210
+ const tool = args.tool as string;
211
+ if (!tool) return fail('tool is required');
212
+ await cuaRpc('desktop.cua.describe', { tool });
587
213
  }
588
214
 
589
- // ── Dispatch ──────────────────────────────────────────────────────────────
590
-
591
- const ALL_COMMANDS = [
592
- "status",
593
- "fs_read",
594
- "fs_write",
595
- "fs_list",
596
- "shell",
597
- "screenshot",
598
- "click",
599
- "mouse_move",
600
- "mouse_drag",
601
- "mouse_scroll",
602
- "type",
603
- "key",
604
- "window_list",
605
- "window_focus",
606
- "app_launch",
607
- "app_quit",
608
- "clipboard_read",
609
- "clipboard_write",
610
- "screen_info",
611
- "cursor_image",
612
- "ax_tree",
613
- "ax_action",
614
- "ax_set_value",
615
- "ax_focus",
616
- "ax_search",
617
- ];
618
-
619
215
  const [cmd, rawArgs] = process.argv.slice(2);
620
216
 
621
217
  if (!cmd) {
622
- console.error(
623
- `Usage: agent-tunnel-cli <command> [args as JSON]\n\nAvailable: ${ALL_COMMANDS.join(" | ")}`
624
- );
218
+ console.error(`Usage: agent-tunnel-cli <command> [args as JSON]\n\nAvailable: ${ALL_COMMANDS.join(' | ')}`);
625
219
  process.exit(1);
626
220
  }
627
221
 
628
- const args = rawArgs ? JSON.parse(rawArgs) : {};
222
+ let args: Record<string, unknown> = {};
223
+ try {
224
+ args = rawArgs ? JSON.parse(rawArgs) : {};
225
+ } catch (err) {
226
+ fail(err instanceof Error ? err.message : String(err));
227
+ process.exit(1);
228
+ }
629
229
 
630
230
  try {
631
231
  switch (cmd) {
632
- case "status":
232
+ case 'status':
633
233
  await status();
634
234
  break;
635
- case "fs_read":
235
+ case 'fs_read':
636
236
  await fsRead(args);
637
237
  break;
638
- case "fs_write":
238
+ case 'fs_write':
639
239
  await fsWrite(args);
640
240
  break;
641
- case "fs_list":
241
+ case 'fs_list':
642
242
  await fsList(args);
643
243
  break;
644
- case "shell":
244
+ case 'shell':
645
245
  await shell(args);
646
246
  break;
647
- case "screenshot":
648
- await screenshot(args);
649
- break;
650
- case "click":
651
- await click(args);
652
- break;
653
- case "mouse_move":
654
- await mouseMove(args);
655
- break;
656
- case "mouse_drag":
657
- await mouseDrag(args);
658
- break;
659
- case "mouse_scroll":
660
- await mouseScroll(args);
661
- break;
662
- case "type":
663
- await typeText(args);
664
- break;
665
- case "key":
666
- await pressKey(args);
667
- break;
668
- case "window_list":
669
- await windowList();
247
+ case 'cua_ensure':
248
+ await cuaRpc('desktop.cua.ensure');
670
249
  break;
671
- case "window_focus":
672
- await windowFocus(args);
250
+ case 'cua_start_daemon':
251
+ await cuaRpc('desktop.cua.start_daemon');
673
252
  break;
674
- case "app_launch":
675
- await appLaunch(args);
253
+ case 'cua_status':
254
+ await cuaRpc('desktop.cua.status');
676
255
  break;
677
- case "app_quit":
678
- await appQuit(args);
256
+ case 'cua_version':
257
+ await cuaRpc('desktop.cua.version');
679
258
  break;
680
- case "clipboard_read":
681
- await clipboardRead();
259
+ case 'cua_list_tools':
260
+ await cuaRpc('desktop.cua.list_tools');
682
261
  break;
683
- case "clipboard_write":
684
- await clipboardWrite(args);
262
+ case 'cua_describe':
263
+ await cuaDescribe(args);
685
264
  break;
686
- case "screen_info":
687
- await screenInfo();
265
+ case 'cua_call':
266
+ await cuaCall(args);
688
267
  break;
689
- case "cursor_image":
690
- await cursorImage(args);
268
+ case 'cua_list_apps':
269
+ await cuaRpc('desktop.cua.list_apps', args);
691
270
  break;
692
- case "ax_tree":
693
- await axTree(args);
271
+ case 'cua_list_windows':
272
+ await cuaRpc('desktop.cua.list_windows', args);
694
273
  break;
695
- case "ax_action":
696
- await axAction(args);
274
+ case 'cua_get_window_state':
275
+ await cuaRpc('desktop.cua.get_window_state', args);
697
276
  break;
698
- case "ax_set_value":
699
- await axSetValue(args);
277
+ case 'cua_click':
278
+ await cuaRpc('desktop.cua.click', args);
700
279
  break;
701
- case "ax_focus":
702
- await axFocus(args);
280
+ case 'cua_type_text':
281
+ await cuaRpc('desktop.cua.type_text', args);
703
282
  break;
704
- case "ax_search":
705
- await axSearch(args);
283
+ case 'cua_hotkey':
284
+ await cuaRpc('desktop.cua.hotkey', args);
706
285
  break;
707
286
  default:
708
- console.error(
709
- `Unknown command: ${cmd}\n\nAvailable: ${ALL_COMMANDS.join(" | ")}`
710
- );
287
+ console.error(`Unknown command: ${cmd}\n\nAvailable: ${ALL_COMMANDS.join(' | ')}`);
711
288
  process.exit(1);
712
289
  }
713
290
  } catch (err) {