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