@phnx-labs/agents-cli 1.22.103 → 1.22.104

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 (66) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +1 -1
  3. package/dist/commands/accounts.js +2 -2
  4. package/dist/commands/computer.d.ts +100 -79
  5. package/dist/commands/computer.js +290 -830
  6. package/dist/commands/setup-computer.js +20 -1
  7. package/dist/commands/setup-secrets.d.ts +2 -2
  8. package/dist/commands/setup-secrets.js +1 -1
  9. package/dist/commands/view.js +4 -6
  10. package/dist/lib/account-catalog.d.ts +22 -1
  11. package/dist/lib/account-catalog.js +72 -38
  12. package/dist/lib/accounting/usage.js +18 -14
  13. package/dist/lib/agent-spec/agents.d.ts +1 -0
  14. package/dist/lib/agent-spec/agents.js +1 -1
  15. package/dist/lib/browser/drivers/ssh.js +1 -1
  16. package/dist/lib/computer/context.d.ts +83 -0
  17. package/dist/lib/computer/context.js +91 -0
  18. package/dist/lib/computer/policy.d.ts +46 -0
  19. package/dist/lib/computer/policy.js +160 -0
  20. package/dist/lib/computer/record.d.ts +38 -0
  21. package/dist/lib/computer/record.js +86 -0
  22. package/dist/lib/computer/sessions-list.js +6 -6
  23. package/dist/lib/computer-client.d.ts +150 -0
  24. package/dist/lib/computer-client.js +222 -0
  25. package/dist/lib/exec.js +35 -1
  26. package/dist/lib/harness/adapters/claude.d.ts +37 -0
  27. package/dist/lib/harness/adapters/claude.js +69 -0
  28. package/dist/lib/helper-download.d.ts +1 -1
  29. package/dist/lib/helper-download.js +1 -1
  30. package/dist/lib/helper-versions.d.ts +9 -4
  31. package/dist/lib/helper-versions.js +8 -8
  32. package/dist/lib/installations/shims.js +8 -4
  33. package/dist/lib/menubar/download-menubar.d.ts +2 -1
  34. package/dist/lib/menubar/download-menubar.js +2 -1
  35. package/dist/lib/secrets-client.d.ts +3 -3
  36. package/dist/lib/secrets-client.js +15 -38
  37. package/dist/lib/session/db.js +1 -1
  38. package/dist/lib/sha256-asset.d.ts +2 -1
  39. package/dist/lib/sha256-asset.js +2 -1
  40. package/dist/lib/ssh-tunnel.d.ts +61 -0
  41. package/dist/lib/ssh-tunnel.js +105 -0
  42. package/dist/lib/summarizer/summarize.d.ts +2 -2
  43. package/dist/lib/summarizer/summarize.js +10 -3
  44. package/package.json +2 -3
  45. package/dist/commands/computer-actions.d.ts +0 -55
  46. package/dist/commands/computer-actions.js +0 -594
  47. package/dist/computer.d.ts +0 -2
  48. package/dist/computer.js +0 -7
  49. package/dist/lib/computer/actions.d.ts +0 -36
  50. package/dist/lib/computer/actions.js +0 -162
  51. package/dist/lib/computer/computer-rpc.d.ts +0 -39
  52. package/dist/lib/computer/computer-rpc.js +0 -447
  53. package/dist/lib/computer/des.d.ts +0 -1
  54. package/dist/lib/computer/des.js +0 -114
  55. package/dist/lib/computer/dispatch.d.ts +0 -10
  56. package/dist/lib/computer/dispatch.js +0 -133
  57. package/dist/lib/computer/download.d.ts +0 -54
  58. package/dist/lib/computer/download.js +0 -83
  59. package/dist/lib/computer/loop.d.ts +0 -62
  60. package/dist/lib/computer/loop.js +0 -98
  61. package/dist/lib/computer/model.d.ts +0 -44
  62. package/dist/lib/computer/model.js +0 -157
  63. package/dist/lib/computer/rfb-client.d.ts +0 -53
  64. package/dist/lib/computer/rfb-client.js +0 -562
  65. package/dist/lib/computer/ssh-tunnel.d.ts +0 -189
  66. package/dist/lib/computer/ssh-tunnel.js +0 -584
@@ -1,594 +0,0 @@
1
- // Action verbs for `agents computer` — the interaction surface over the
2
- // computer-helper daemon's RPC methods (click, type, key, drag, scroll,
3
- // describe, ax-action, focus, ...). These mirror `agents browser`'s verb
4
- // layout: flat verbs under the noun, bundle-id targeting, --json on reads.
5
- //
6
- // The daemon already implements every method; this file is the thin, typed
7
- // CLI skin over it plus a shared target resolver so callers stay in bundle-id
8
- // space and never hand-manage pids.
9
- import { execFileSync } from 'child_process';
10
- import * as fs from 'fs';
11
- import * as path from 'path';
12
- import { openComputerClient, describeTransport, } from '../lib/computer/computer-rpc.js';
13
- import { emitComputerAction, resolveTargetPidDecision } from '../lib/computer/actions.js';
14
- export { emitComputerAction, pickTarget, resolveTargetPidDecision } from '../lib/computer/actions.js';
15
- // Pure target picker — exercised by unit tests. Precedence: explicit --pid,
16
- // then --bundle, then the frontmost active allow-listed app (the same default
17
- // `screenshot` uses). Kept side-effect-free so the resolution rules are
18
- // testable without a live daemon.
19
- // Parse an "x,y" coordinate pair. Pure + tested.
20
- export function parseXY(s, flag) {
21
- const parts = s.split(',').map((v) => v.trim());
22
- if (parts.length !== 2) {
23
- throw new Error(`${flag} must be "x,y" (got: ${s})`);
24
- }
25
- const x = Number(parts[0]);
26
- const y = Number(parts[1]);
27
- if (!Number.isFinite(x) || !Number.isFinite(y)) {
28
- throw new Error(`${flag} must be two numbers "x,y" (got: ${s})`);
29
- }
30
- return { x, y };
31
- }
32
- // Build the element-or-coords target spec shared by click/type/scroll/etc.
33
- // Pure + tested: returns the params fragment or an error string.
34
- export function buildElementOrCoords(opts) {
35
- if (opts.id)
36
- return { ok: true, params: { element_id: opts.id } };
37
- if (opts.x != null && opts.y != null)
38
- return { ok: true, params: { x: opts.x, y: opts.y } };
39
- return { ok: false, error: 'pass --id <@eN> (from `describe`) or --x <n> --y <n>' };
40
- }
41
- // Build the focus_window params for `raise`. Pure + tested: window_id and
42
- // title are both optional refinements over the app-level activate.
43
- export function buildRaiseParams(opts) {
44
- const params = {};
45
- if (opts.windowId != null)
46
- params.window_id = opts.windowId;
47
- if (opts.title)
48
- params.title = opts.title;
49
- return params;
50
- }
51
- // Inter-character typing delay for type-text. Default 4ms matches the daemon's
52
- // historical fixed rate; lossy keyboard relays (Parallels/VM guests) drop chars
53
- // at that rate, so callers can raise it. Clamp to [1, 250]ms CLI-side (the
54
- // daemon clamps too — defense in depth). Returns undefined when unset so the
55
- // daemon applies its own default. Pure + tested.
56
- export const CHAR_DELAY_MIN_MS = 1;
57
- export const CHAR_DELAY_MAX_MS = 250;
58
- export function clampCharDelay(ms) {
59
- if (ms === undefined || !Number.isFinite(ms))
60
- return undefined;
61
- return Math.min(CHAR_DELAY_MAX_MS, Math.max(CHAR_DELAY_MIN_MS, Math.trunc(ms)));
62
- }
63
- // Build the wait RPC params. Pure + tested. Three modes, mirroring the
64
- // daemon's Wait.run: --duration (unconditional sleep), --id + --until
65
- // (cached-element poll), or --role/--label/--identifier (live locator poll).
66
- export function buildWaitParams(opts) {
67
- if (opts.duration != null) {
68
- return { ok: true, params: { duration_ms: opts.duration } };
69
- }
70
- const params = {};
71
- if (opts.until)
72
- params.until = opts.until;
73
- if (opts.timeout != null)
74
- params.timeout_ms = opts.timeout;
75
- if (opts.id) {
76
- return { ok: true, params: { ...params, element_id: opts.id } };
77
- }
78
- const locator = {};
79
- if (opts.role)
80
- locator.role = opts.role;
81
- if (opts.label)
82
- locator.label = opts.label;
83
- if (opts.identifier)
84
- locator.identifier = opts.identifier;
85
- if (Object.keys(locator).length === 0) {
86
- return { ok: false, error: 'pass --duration <ms>, --id <@eN>, or a locator (--role/--label/--identifier)' };
87
- }
88
- return { ok: true, params: { ...params, locator } };
89
- }
90
- // postToPid keyboard delivery is dropped by key-window-gated apps (Parallels
91
- // VMs and friends) — when the daemon reports the target was not frontmost,
92
- // the keystrokes may have landed nowhere. Surface that loudly on stderr.
93
- function warnIfNotFrontmost(res) {
94
- if (res.frontmost === false) {
95
- console.error('warning: target was not the frontmost app — keystrokes may have been dropped. Run `agents computer raise` first.');
96
- }
97
- }
98
- function reportMissingHelper() {
99
- console.error('helper not built. Run: ./native/computer-mac/scripts/build.sh debug');
100
- process.exit(1);
101
- }
102
- // Open a client, run fn, always close. Fails fast if no helper is present.
103
- export async function withClient(fn) {
104
- if (describeTransport().kind === 'none')
105
- reportMissingHelper();
106
- const client = openComputerClient();
107
- try {
108
- return await fn(client);
109
- }
110
- finally {
111
- await client.close();
112
- }
113
- }
114
- // Unwrap an RPC response: print + exit on error, else return result.
115
- export function unwrap(r) {
116
- if (r.error) {
117
- console.error(`error: ${r.error.code}: ${r.error.message}`);
118
- process.exit(1);
119
- }
120
- return r.result ?? {};
121
- }
122
- // Resolve the target pid via list_apps + pickTarget, printing a precise error
123
- // and exiting when no target matches.
124
- async function resolveTargetPid(client, opts, gate) {
125
- const resolved = await resolveTargetPidDecision(client, opts, gate);
126
- if (!resolved.ok) {
127
- console.error(resolved.error);
128
- process.exit(1);
129
- }
130
- return resolved.pid;
131
- }
132
- // Focus-safety policy for input verbs. Element mode (--id) drives an app through
133
- // Accessibility actions (AXPress / set AXValue) that never activate the app or move
134
- // the cursor, so the user keeps working while an agent acts. The two paths that DO
135
- // take over the screen are (a) --raise (brings the app to the front + steals keyboard
136
- // focus) and (b) coordinate mode (--x/--y warps the physical cursor and needs the app
137
- // frontmost). Both are surfaced as notes, and --raise is ignored in element mode so a
138
- // reflexive flag cannot hijack the user's session.
139
- // Pure, unit-tested: the focus/cursor costs an action will impose, as human notes.
140
- export function focusStealNotes(opts) {
141
- const notes = [];
142
- const elementMode = opts.id != null;
143
- if (opts.raise) {
144
- notes.push(elementMode
145
- ? 'note: --raise ignored in element mode (--id) — element actions do not need the app frontmost, so your focus is left alone.'
146
- : 'note: --raise brings the target app to the front and takes keyboard focus from you. Element mode (`describe` then --id) drives apps without stealing focus.');
147
- }
148
- if (!elementMode && (opts.x != null || opts.y != null)) {
149
- notes.push('note: coordinate mode moves your real cursor and needs the app frontmost. Prefer element mode (`describe` then --id) to act without moving your pointer.');
150
- }
151
- return notes;
152
- }
153
- // Pure, unit-tested: whether --raise is actually honored. Element mode suppresses it
154
- // so an element-targeted action never steals the user's foreground.
155
- export function shouldRaise(opts) {
156
- return Boolean(opts.raise) && opts.id == null;
157
- }
158
- // Apply the focus-safety policy for an input verb: print the cost notes, and raise
159
- // only when raising is actually warranted (non-element mode with --raise).
160
- async function applyFocusPolicy(client, pid, opts) {
161
- for (const note of focusStealNotes(opts))
162
- console.error(note);
163
- if (shouldRaise(opts))
164
- unwrap(await client.call('focus_window', { pid }));
165
- }
166
- // Electron/webview steering. macOS accepts an AX action (AXPress / set-AXValue)
167
- // on an Electron/Chromium window, but it does NOT run the web app's real DOM
168
- // handlers — React ignores it — so a reported `clicked`/`typed` on a webview can
169
- // be a silent no-op. Detect Electron targets and steer the caller to CDP
170
- // (`agents browser --electron`), which drives the webview for real. We warn, not
171
- // block: the caller may still want the raw action (e.g. to focus + coordinate).
172
- const electronCache = new Map();
173
- // Resolve a bundle id to its .app path via Spotlight. Best-effort; null on miss.
174
- function appPathForBundle(bundleId) {
175
- try {
176
- const out = execFileSync('mdfind', [`kMDItemCFBundleIdentifier == '${bundleId}'`], {
177
- encoding: 'utf-8',
178
- timeout: 3000,
179
- });
180
- return out.split('\n').map((s) => s.trim()).find((s) => s.endsWith('.app')) ?? null;
181
- }
182
- catch {
183
- return null;
184
- }
185
- }
186
- // Pure + unit-tested: does the .app at this path bundle the Electron framework?
187
- export function appPathIsElectron(appPath, exists = fs.existsSync) {
188
- if (!appPath)
189
- return false;
190
- return exists(path.join(appPath, 'Contents', 'Frameworks', 'Electron Framework.framework'));
191
- }
192
- // Is the app for this bundle id an Electron/webview app? macOS-only; memoized so
193
- // the mdfind lookup runs at most once per bundle id per process.
194
- function isElectronApp(bundleId) {
195
- if (!bundleId || process.platform !== 'darwin')
196
- return false;
197
- const cached = electronCache.get(bundleId);
198
- if (cached !== undefined)
199
- return cached;
200
- const result = appPathIsElectron(appPathForBundle(bundleId));
201
- electronCache.set(bundleId, result);
202
- return result;
203
- }
204
- // Pure + unit-tested: the CDP-steer note printed for a webview target.
205
- export function electronWebviewTip(appLabel) {
206
- return `note: ${appLabel} is an Electron/web UI — an AX click/type may not reach the webview `
207
- + `(a reported success can be a no-op). To drive it reliably, relaunch it with `
208
- + '`--remote-debugging-port=9222` and use `agents browser --electron` (CDP).';
209
- }
210
- // Print the CDP steer when the target is a known Electron app. Keyed off --bundle
211
- // (the recommended way to target); a frontmost-resolved target without --bundle is
212
- // left alone to avoid a second RPC on the hot path. Skipped for remote --device.
213
- function warnIfElectronWebview(opts) {
214
- if (opts.device)
215
- return;
216
- if (opts.bundle && isElectronApp(opts.bundle))
217
- console.error(electronWebviewTip(opts.bundle));
218
- }
219
- function emit(result, json, human) {
220
- if (json) {
221
- console.log(JSON.stringify(result, null, 2));
222
- }
223
- else {
224
- console.log(human());
225
- }
226
- }
227
- // Record one `computer.action` per verb invocation, one call site per command
228
- // below. Session/agent/machine identity is stamped for free by emitEvent's
229
- // provenance floor (events.ts resolveProvenance) — this only carries the
230
- // action-specific facts: which verb, against which target pid/bundle/device.
231
- // NOTE: the field is `targetPid`, never `pid` — `pid` is a reserved envelope
232
- // key (the emitting process's OWN pid, events.ts RESERVED_META_KEYS) that
233
- // sanitizePayload() silently strips from the payload before it can collide.
234
- // emitComputerAction lives in lib/computer/actions.ts (re-exported above).
235
- // Add the shared --pid/--bundle/--device target options to a verb. `--device`
236
- // routes the verb at a remote Windows device: the `computer` preAction hook
237
- // hydrates COMPUTER_HELPER_TCP from the tunnel `start --device` recorded, so
238
- // withClient's openComputerClient() transparently selects the TCP transport.
239
- function addTargetOpts(cmd) {
240
- return cmd
241
- .option('--bundle <id>', 'Bundle id of the target app (default: frontmost allow-listed app)')
242
- .option('--pid <n>', 'Target pid directly (overrides --bundle)', (v) => parseInt(v, 10))
243
- .option('--device <name>', 'Drive a remote Windows device (requires `agents computer start --device <name>` first)');
244
- }
245
- // Add the shared --id/--x/--y element-or-coords options to a verb.
246
- function addElementOrCoordOpts(cmd) {
247
- return cmd
248
- .option('--id <@eN>', 'Element id from `describe` (focus-safe: no foreground steal, no cursor move)')
249
- .option('--x <n>', 'X coordinate (global, points; moves your real cursor, needs app frontmost)', (v) => parseInt(v, 10))
250
- .option('--y <n>', 'Y coordinate (global, points; moves your real cursor, needs app frontmost)', (v) => parseInt(v, 10));
251
- }
252
- export function registerActionCommands(program) {
253
- // apps — list_apps
254
- addTargetOpts(program
255
- .command('apps')
256
- .description('List apps the daemon may drive (allow-listed + running)')
257
- .option('--json', 'Emit JSON')).action(async (opts) => {
258
- await withClient(async (client) => {
259
- const res = unwrap(await client.call('list_apps'));
260
- const list = res.apps || [];
261
- emitComputerAction('apps', undefined, opts);
262
- emit(res, Boolean(opts.json), () => list.length === 0
263
- ? '(no allow-listed apps running)'
264
- : list
265
- .map((a) => `${a.active ? '*' : ' '} ${String(a.pid).padStart(6)} ${a.bundle_id} ${a.name}`)
266
- .join('\n'));
267
- });
268
- });
269
- // describe — AX tree
270
- addTargetOpts(program
271
- .command('describe')
272
- .description('Dump the accessibility tree (element ids feed click/type --id)')
273
- .option('--depth <n>', 'Max tree depth', (v) => parseInt(v, 10))
274
- .option('--json', 'Emit compact JSON (default: pretty)')).action(async (opts) => {
275
- await withClient(async (client) => {
276
- const pid = await resolveTargetPid(client, opts, { verb: 'describe' });
277
- const params = { pid };
278
- if (opts.depth != null)
279
- params.max_depth = opts.depth;
280
- const res = unwrap(await client.call('describe', params));
281
- emitComputerAction('describe', pid, opts, { depth: opts.depth });
282
- // The tree is inherently structured — always JSON, pretty unless --json.
283
- console.log(JSON.stringify(opts.json ? res : res.tree ?? res, null, 2));
284
- });
285
- });
286
- // click
287
- addElementOrCoordOpts(addTargetOpts(program
288
- .command('click')
289
- .description('Click an element (--id) or screen coordinate (--x --y)')
290
- .option('--count <n>', 'Click count (2 = double-click)', (v) => parseInt(v, 10))
291
- .option('--background', 'Focus-safe postToPid delivery (plain AppKit only; skips HID tap)')
292
- .option('--raise', 'Bring the target app to the front first (steals your foreground + keyboard focus; ignored in element mode --id)')
293
- .option('--json', 'Emit JSON'))).action(async (opts) => {
294
- await withClient(async (client) => {
295
- const pid = await resolveTargetPid(client, opts, { verb: 'click' });
296
- warnIfElectronWebview(opts);
297
- const spec = buildElementOrCoords(opts);
298
- if (!spec.ok) {
299
- console.error(spec.error);
300
- process.exit(1);
301
- }
302
- await applyFocusPolicy(client, pid, opts);
303
- const params = { pid, ...spec.params };
304
- if (opts.count != null)
305
- params.count = opts.count;
306
- if (opts.background)
307
- params.background = true;
308
- const res = unwrap(await client.call('click', params));
309
- emitComputerAction('click', pid, opts, { id: opts.id, count: opts.count });
310
- emit(res, Boolean(opts.json), () => `clicked (${res.action ?? 'ok'})`);
311
- });
312
- });
313
- // right-click
314
- addElementOrCoordOpts(addTargetOpts(program
315
- .command('right-click')
316
- .description('Right-click (context menu) an element or coordinate')
317
- .option('--json', 'Emit JSON'))).action(async (opts) => {
318
- await withClient(async (client) => {
319
- const pid = await resolveTargetPid(client, opts, { verb: 'right-click' });
320
- warnIfElectronWebview(opts);
321
- const spec = buildElementOrCoords(opts);
322
- if (!spec.ok) {
323
- console.error(spec.error);
324
- process.exit(1);
325
- }
326
- await applyFocusPolicy(client, pid, opts);
327
- const res = unwrap(await client.call('right_click', { pid, ...spec.params }));
328
- emitComputerAction('right-click', pid, opts, { id: opts.id });
329
- emit(res, Boolean(opts.json), () => `right-clicked (${res.method ?? 'ok'})`);
330
- });
331
- });
332
- // type — set value on a field (--id) or paste at coords, optional commit
333
- addElementOrCoordOpts(addTargetOpts(program
334
- .command('type')
335
- .description('Set a field value (--id) or paste at a coordinate (--x --y)')
336
- .requiredOption('--text <s>', 'Text to enter')
337
- .option('--commit', 'Commit after typing (AXConfirm / Return) so the value reaches the model')
338
- .option('--allow-secure-field', 'Permit typing into a password field')
339
- .option('--json', 'Emit JSON'))).action(async (opts) => {
340
- await withClient(async (client) => {
341
- const pid = await resolveTargetPid(client, opts, { verb: 'type' });
342
- warnIfElectronWebview(opts);
343
- const spec = buildElementOrCoords(opts);
344
- if (!spec.ok) {
345
- console.error(spec.error);
346
- process.exit(1);
347
- }
348
- await applyFocusPolicy(client, pid, opts);
349
- const params = { pid, ...spec.params, text: opts.text };
350
- if (opts.commit)
351
- params.commit = true;
352
- if (opts.allowSecureField)
353
- params.allow_secure_field = true;
354
- const res = unwrap(await client.call('type', params));
355
- // textLength, never the text itself — users type passwords/secrets into fields.
356
- emitComputerAction('type', pid, opts, { id: opts.id, textLength: opts.text.length, committed: Boolean(res.committed) });
357
- emit(res, Boolean(opts.json), () => `typed ${opts.text.length} char(s)${res.committed ? ' (committed)' : ''}`);
358
- });
359
- });
360
- // type-text — stream an arbitrary unicode string into the focused field
361
- addTargetOpts(program
362
- .command('type-text')
363
- .description('Type an arbitrary unicode string into the focused field (focus first via click/focus)')
364
- .requiredOption('--text <s>', 'Text to type')
365
- .option('--commit', 'Press Return after typing')
366
- .option('--raise', 'Bring the target app to the front first (steals your foreground + keyboard focus; ignored in element mode --id)')
367
- .option('--require-frontmost', 'Fail (not warn) if the target is not the frontmost app')
368
- .option('--char-delay <ms>', 'Inter-character delay in ms (default 4; raise for lossy keyboard relays like VM guests, e.g. 25). Clamped to [1, 250].', (v) => parseInt(v, 10))
369
- .option('--json', 'Emit JSON')).action(async (opts) => {
370
- await withClient(async (client) => {
371
- const pid = await resolveTargetPid(client, opts, { verb: 'type-text' });
372
- warnIfElectronWebview(opts);
373
- await applyFocusPolicy(client, pid, opts);
374
- const params = { pid, text: opts.text };
375
- if (opts.commit)
376
- params.commit = true;
377
- if (opts.requireFrontmost)
378
- params.require_frontmost = true;
379
- const charDelay = clampCharDelay(opts.charDelay);
380
- if (charDelay !== undefined)
381
- params.char_delay_ms = charDelay;
382
- const res = unwrap(await client.call('type_text', params));
383
- warnIfNotFrontmost(res);
384
- emitComputerAction('type-text', pid, opts, { textLength: opts.text.length, committed: Boolean(opts.commit) });
385
- emit(res, Boolean(opts.json), () => `typed ${res.chars ?? opts.text.length} char(s)`);
386
- });
387
- });
388
- // key — single chord
389
- addTargetOpts(program
390
- .command('key')
391
- .description('Send a key chord, e.g. "cmd+shift+s", "enter", "esc"')
392
- .requiredOption('--keys <chord>', 'Key chord')
393
- .option('--raise', 'Bring the target app to the front first (steals your foreground + keyboard focus; ignored in element mode --id)')
394
- .option('--require-frontmost', 'Fail (not warn) if the target is not the frontmost app')
395
- .option('--json', 'Emit JSON')).action(async (opts) => {
396
- await withClient(async (client) => {
397
- const pid = await resolveTargetPid(client, opts, { verb: 'key' });
398
- warnIfElectronWebview(opts);
399
- await applyFocusPolicy(client, pid, opts);
400
- const params = { pid, keys: opts.keys };
401
- if (opts.requireFrontmost)
402
- params.require_frontmost = true;
403
- const res = unwrap(await client.call('key', params));
404
- warnIfNotFrontmost(res);
405
- emitComputerAction('key', pid, opts, { keys: opts.keys });
406
- emit(res, Boolean(opts.json), () => `sent ${opts.keys}`);
407
- });
408
- });
409
- // drag — from one point to another
410
- addTargetOpts(program
411
- .command('drag')
412
- .description('Drag from one coordinate to another')
413
- .requiredOption('--from <x,y>', 'Start coordinate "x,y"')
414
- .requiredOption('--to <x,y>', 'End coordinate "x,y"')
415
- .option('--button <left|right>', 'Mouse button', 'left')
416
- .option('--background', 'Focus-safe postToPid delivery (plain AppKit only)')
417
- .option('--raise', 'Bring the target app to the front first (steals your foreground + keyboard focus; ignored in element mode --id)')
418
- .option('--json', 'Emit JSON')).action(async (opts) => {
419
- let from;
420
- let to;
421
- try {
422
- from = parseXY(opts.from, '--from');
423
- to = parseXY(opts.to, '--to');
424
- }
425
- catch (err) {
426
- console.error(err.message);
427
- process.exit(1);
428
- }
429
- await withClient(async (client) => {
430
- const pid = await resolveTargetPid(client, opts, { verb: 'drag' });
431
- await applyFocusPolicy(client, pid, opts);
432
- const params = {
433
- pid,
434
- from: [from.x, from.y],
435
- to: [to.x, to.y],
436
- button: opts.button,
437
- };
438
- if (opts.background)
439
- params.background = true;
440
- const res = unwrap(await client.call('drag', params));
441
- emitComputerAction('drag', pid, opts, { from: opts.from, to: opts.to });
442
- emit(res, Boolean(opts.json), () => `dragged ${opts.from} -> ${opts.to} (${res.method ?? 'ok'})`);
443
- });
444
- });
445
- // scroll — by delta at an element or coordinate
446
- addElementOrCoordOpts(addTargetOpts(program
447
- .command('scroll')
448
- .description('Scroll by a pixel delta at an element or coordinate')
449
- .option('--dy <n>', 'Vertical delta (negative = down)', (v) => parseInt(v, 10))
450
- .option('--dx <n>', 'Horizontal delta', (v) => parseInt(v, 10))
451
- .option('--raise', 'Bring the target app to the front first (steals your foreground + keyboard focus; ignored in element mode --id)')
452
- .option('--json', 'Emit JSON'))).action(async (opts) => {
453
- await withClient(async (client) => {
454
- const pid = await resolveTargetPid(client, opts, { verb: 'scroll' });
455
- await applyFocusPolicy(client, pid, opts);
456
- const params = { pid };
457
- if (opts.id)
458
- params.element_id = opts.id;
459
- if (opts.x != null)
460
- params.x = opts.x;
461
- if (opts.y != null)
462
- params.y = opts.y;
463
- if (opts.dy != null)
464
- params.dy = opts.dy;
465
- if (opts.dx != null)
466
- params.dx = opts.dx;
467
- const res = unwrap(await client.call('scroll', params));
468
- emitComputerAction('scroll', pid, opts, { id: opts.id, dx: opts.dx, dy: opts.dy });
469
- emit(res, Boolean(opts.json), () => `scrolled (${res.method ?? 'ok'})`);
470
- });
471
- });
472
- // ax-action — perform any advertised AX action on an element
473
- addTargetOpts(program
474
- .command('ax-action')
475
- .description('Perform an arbitrary AX action (AXConfirm, AXCancel, AXRaise, ...) on an element')
476
- .requiredOption('--id <@eN>', 'Element id from `describe`')
477
- .requiredOption('--action <name>', 'AX action name')
478
- .option('--json', 'Emit JSON')).action(async (opts) => {
479
- await withClient(async (client) => {
480
- const pid = await resolveTargetPid(client, opts, { verb: 'ax-action' });
481
- const res = unwrap(await client.call('ax_action', { pid, element_id: opts.id, action: opts.action }));
482
- emitComputerAction('ax-action', pid, opts, { id: opts.id, action: opts.action });
483
- emit(res, Boolean(opts.json), () => `performed ${opts.action}`);
484
- });
485
- });
486
- // focus — set keyboard focus to an element
487
- addTargetOpts(program
488
- .command('focus')
489
- .description('Set keyboard focus to an element (so type-text/key land there)')
490
- .requiredOption('--id <@eN>', 'Element id from `describe`')
491
- .option('--json', 'Emit JSON')).action(async (opts) => {
492
- await withClient(async (client) => {
493
- const pid = await resolveTargetPid(client, opts, { verb: 'focus' });
494
- const res = unwrap(await client.call('set_focus', { pid, element_id: opts.id }));
495
- emitComputerAction('focus', pid, opts, { id: opts.id });
496
- emit(res, Boolean(opts.json), () => `focused ${opts.id}`);
497
- });
498
- });
499
- // raise — bring an app (or one of its windows) to the front. The window
500
- // forms (--window-id/--title) also switch macOS Spaces, which is the only
501
- // way to reach a fullscreen-Space window (VM, fullscreen editor) for
502
- // capture and HID-tap input.
503
- addTargetOpts(program
504
- .command('raise')
505
- .description('Bring an app (or a specific window) to the front — switches Spaces for fullscreen windows')
506
- .option('--window-id <n>', 'Raise a specific window by id (from `screenshot --list`)', (v) => parseInt(v, 10))
507
- .option('--title <s>', 'Raise the window whose title contains this string')
508
- .option('--json', 'Emit JSON')).action(async (opts) => {
509
- await withClient(async (client) => {
510
- const pid = await resolveTargetPid(client, opts, { verb: 'raise' });
511
- const res = unwrap(await client.call('focus_window', { pid, ...buildRaiseParams(opts) }));
512
- emitComputerAction('raise', pid, opts, { windowId: opts.windowId, title: opts.title });
513
- emit(res, Boolean(opts.json), () => {
514
- const scope = res.raised_window ? `window ${res.title ?? res.window_id ?? ''}`.trim() : 'app';
515
- return `raised ${scope} (${res.focus_elapsed_ms ?? 0}ms)`;
516
- });
517
- });
518
- });
519
- // wait — settle the UI before the next action
520
- addTargetOpts(program
521
- .command('wait')
522
- .description('Wait for a duration (--duration) or for an element (--id / --role/--label) to satisfy --until')
523
- .option('--duration <ms>', 'Unconditional sleep in ms (50-30000)', (v) => parseInt(v, 10))
524
- .option('--id <@eN>', 'Element id from `describe` to poll')
525
- .option('--until <cond>', 'Condition: exists | enabled | disappears (default: exists)')
526
- .option('--role <s>', 'Locator: AX role (e.g. AXButton)')
527
- .option('--label <s>', 'Locator: element label')
528
- .option('--identifier <s>', 'Locator: AX identifier')
529
- .option('--timeout <ms>', 'Poll timeout in ms (default 5000)', (v) => parseInt(v, 10))
530
- .option('--json', 'Emit JSON')).action(async (opts) => {
531
- const spec = buildWaitParams(opts);
532
- if (!spec.ok) {
533
- console.error(spec.error);
534
- process.exit(1);
535
- }
536
- await withClient(async (client) => {
537
- const params = { ...spec.params };
538
- // duration-only waits don't need a target pid
539
- if (params.duration_ms == null)
540
- params.pid = await resolveTargetPid(client, opts, { verb: 'wait' });
541
- const res = unwrap(await client.call('wait', params));
542
- emitComputerAction('wait', params.pid, opts, {
543
- until: opts.until, durationMs: opts.duration, satisfied: Boolean(res.satisfied),
544
- });
545
- emit(res, Boolean(opts.json), () => res.satisfied ? `satisfied (${res.waited_ms}ms)` : `timed out (${res.waited_ms}ms)`);
546
- });
547
- });
548
- // get-text — read text without OCR
549
- addTargetOpts(program
550
- .command('get-text')
551
- .description('Extract visible text from the app (or a subtree via --id)')
552
- .option('--id <@eN>', 'Element id from `describe` to scope the extraction')
553
- .option('--max-chars <n>', 'Cap the extracted text length', (v) => parseInt(v, 10))
554
- .option('--json', 'Emit JSON')).action(async (opts) => {
555
- await withClient(async (client) => {
556
- const pid = await resolveTargetPid(client, opts, { verb: 'get-text' });
557
- const params = { pid };
558
- if (opts.id)
559
- params.element_id = opts.id;
560
- if (opts.maxChars != null)
561
- params.max_chars = opts.maxChars;
562
- const res = unwrap(await client.call('get_text', params));
563
- emitComputerAction('get-text', pid, opts, { id: opts.id });
564
- emit(res, Boolean(opts.json), () => String(res.text ?? ''));
565
- });
566
- });
567
- // launch — start an app (no target resolution: it isn't running yet)
568
- program
569
- .command('launch')
570
- .description('Launch an app by bundle id, path, or name')
571
- .option('--bundle <id>', 'Bundle id (e.g. com.apple.TextEdit)')
572
- .option('--path <p>', 'Path to the .app bundle')
573
- .option('--name <s>', 'App name (resolved via /Applications and LaunchServices)')
574
- .option('--device <name>', 'Drive a remote Windows device (requires `agents computer start --device <name>` first)')
575
- .option('--json', 'Emit JSON')
576
- .action(async (opts) => {
577
- if (!opts.bundle && !opts.path && !opts.name) {
578
- console.error('pass one of --bundle, --path, --name');
579
- process.exit(1);
580
- }
581
- await withClient(async (client) => {
582
- const params = {};
583
- if (opts.bundle)
584
- params.bundle_id = opts.bundle;
585
- if (opts.path)
586
- params.path = opts.path;
587
- if (opts.name)
588
- params.name = opts.name;
589
- const res = unwrap(await client.call('launch_app', params));
590
- emitComputerAction('launch', res.pid, opts, { path: opts.path, name: opts.name });
591
- emit(res, Boolean(opts.json), () => `launched ${res.name} (pid ${res.pid})`);
592
- });
593
- });
594
- }
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
package/dist/computer.js DELETED
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Command } from 'commander';
3
- import { registerComputerSubcommands } from './commands/computer.js';
4
- const program = new Command();
5
- program.name('computer').description('Drive macOS apps via Accessibility — list, screenshot, click, type');
6
- registerComputerSubcommands(program);
7
- program.parse();
@@ -1,36 +0,0 @@
1
- import type { ComputerClient } from './computer-rpc.js';
2
- export interface AppInfo {
3
- pid: number;
4
- name: string;
5
- bundle_id: string;
6
- active: boolean;
7
- }
8
- export declare function pickTarget(list: AppInfo[], opts: {
9
- pid?: number;
10
- bundle?: string;
11
- }): {
12
- ok: true;
13
- app: AppInfo;
14
- } | {
15
- ok: false;
16
- error: string;
17
- };
18
- export declare function resolveTargetPidDecision(client: ComputerClient, opts: {
19
- pid?: number;
20
- bundle?: string;
21
- }, gate?: {
22
- verb?: string;
23
- env?: NodeJS.ProcessEnv;
24
- nowMs?: number;
25
- }): Promise<{
26
- ok: true;
27
- pid: number;
28
- source: 'pid' | 'list_apps' | 'session_admission';
29
- } | {
30
- ok: false;
31
- error: string;
32
- }>;
33
- export declare function emitComputerAction(verb: string, targetPid: number | undefined, opts: {
34
- bundle?: string;
35
- device?: string;
36
- }, extra?: Record<string, unknown>): void;