@phnx-labs/agents-cli 1.20.5 → 1.20.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 (70) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +1 -1
  3. package/dist/commands/browser.js +31 -4
  4. package/dist/commands/computer-actions.d.ts +36 -0
  5. package/dist/commands/computer-actions.js +328 -0
  6. package/dist/commands/computer.js +74 -55
  7. package/dist/commands/defaults.d.ts +7 -0
  8. package/dist/commands/defaults.js +89 -0
  9. package/dist/commands/exec.js +24 -6
  10. package/dist/commands/inspect.d.ts +38 -7
  11. package/dist/commands/inspect.js +194 -24
  12. package/dist/commands/rules.js +3 -3
  13. package/dist/commands/secrets.js +46 -9
  14. package/dist/commands/sessions.js +9 -12
  15. package/dist/commands/setup.js +2 -2
  16. package/dist/commands/teams.js +108 -11
  17. package/dist/commands/view.d.ts +12 -1
  18. package/dist/commands/view.js +121 -38
  19. package/dist/index.js +61 -22
  20. package/dist/lib/agents.d.ts +10 -6
  21. package/dist/lib/agents.js +23 -14
  22. package/dist/lib/browser/chrome.d.ts +10 -0
  23. package/dist/lib/browser/chrome.js +84 -3
  24. package/dist/lib/daemon.js +4 -7
  25. package/dist/lib/exec.d.ts +9 -0
  26. package/dist/lib/exec.js +85 -9
  27. package/dist/lib/migrate.js +6 -4
  28. package/dist/lib/permissions.d.ts +23 -0
  29. package/dist/lib/permissions.js +89 -7
  30. package/dist/lib/platform/exec.d.ts +9 -0
  31. package/dist/lib/platform/exec.js +24 -0
  32. package/dist/lib/platform/index.d.ts +20 -0
  33. package/dist/lib/platform/index.js +20 -0
  34. package/dist/lib/platform/paths.d.ts +22 -0
  35. package/dist/lib/platform/paths.js +49 -0
  36. package/dist/lib/platform/process.d.ts +12 -0
  37. package/dist/lib/platform/process.js +22 -0
  38. package/dist/lib/plugin-marketplace.js +1 -1
  39. package/dist/lib/project-launch.d.ts +5 -0
  40. package/dist/lib/project-launch.js +37 -0
  41. package/dist/lib/pty-client.js +13 -5
  42. package/dist/lib/pty-server.d.ts +24 -1
  43. package/dist/lib/pty-server.js +109 -29
  44. package/dist/lib/resources/rules.js +1 -1
  45. package/dist/lib/resources/skills.js +1 -1
  46. package/dist/lib/resources.d.ts +2 -0
  47. package/dist/lib/resources.js +2 -1
  48. package/dist/lib/rotate.js +6 -18
  49. package/dist/lib/run-config.d.ts +9 -0
  50. package/dist/lib/run-config.js +35 -0
  51. package/dist/lib/run-defaults.d.ts +42 -0
  52. package/dist/lib/run-defaults.js +180 -0
  53. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  54. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  55. package/dist/lib/secrets/install-helper.d.ts +11 -3
  56. package/dist/lib/secrets/install-helper.js +48 -6
  57. package/dist/lib/secrets/linux.d.ts +12 -0
  58. package/dist/lib/secrets/linux.js +30 -16
  59. package/dist/lib/session/artifacts.js +8 -2
  60. package/dist/lib/shims.d.ts +9 -1
  61. package/dist/lib/shims.js +80 -3
  62. package/dist/lib/staleness/detectors/hooks.js +1 -1
  63. package/dist/lib/staleness/writers/hooks.js +1 -1
  64. package/dist/lib/teams/agents.js +5 -7
  65. package/dist/lib/teams/api.d.ts +67 -0
  66. package/dist/lib/teams/api.js +78 -0
  67. package/dist/lib/types.d.ts +15 -6
  68. package/dist/lib/versions.js +4 -4
  69. package/package.json +5 -2
  70. package/scripts/postinstall.js +18 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 1.20.7
6
+
7
+ **`agents inspect` — DotAgents repo targets (#256)**
8
+
9
+ - `agents inspect` now accepts a DotAgents repo as the target, not just an installed agent: `user` (~/.agents/), `system` (~/.agents/.system/), `project` (nearest `.agents/` from cwd), any extra-repo alias registered via `agents repo add`, or a filesystem path. Paths accept either a repo containing a `.agents/` dir or a DotAgents root directly.
10
+ - Repo summary shows the root (OSC-8 linked), git branch / dirty count / origin URL, manifest files (`agents.yaml`, `hooks.yaml`), and per-kind resource counts. All existing drill-down flags (`--commands`, `--skills`, `--plugins`, ... with fuzzy queries and `--json`) work against the single repo root — what is physically in that repo, with no layered resolution or same-name overrides.
11
+ - Resolution precedence: a directory that is itself a DotAgents root wins over its nested `.agents/`, so extra repos that keep resources at the top level and use `.agents/` only for worktrees resolve to their real resources.
12
+ - Unknown targets now error with both halves of the namespace: the known agent ids and the available repo targets (built-in layers plus registered aliases).
13
+
14
+ **`scripts/install.sh` — bash 3.2 fix (#256)**
15
+
16
+ - `set -u` plus `"${BUILD_ARGS[@]}"` on an empty array aborted the dev install with `BUILD_ARGS[@]: unbound variable` under macOS system bash; the expansion is now guarded with `${BUILD_ARGS[@]+...}`.
17
+
5
18
  ## 1.20.5
6
19
 
7
20
  **`agents inspect` — per-agent+version detail view with drill-down (#217)**
package/README.md CHANGED
@@ -243,7 +243,7 @@ agents teams status auth-feature # Who's working, what they changed, what the
243
243
 
244
244
  Teammates run detached -- close your terminal, they keep working. Check in with `teams status`, read full output with `teams logs <name>`, clean up with `teams disband`.
245
245
 
246
- Team state is observable via `agents teams list --json` / `agents teams status --json`. External tools join it with `sessions --json` (teammates get `isTeamOrigin: true`) and `cloud list --json` (for `--cloud` teammates) to build a unified fleet view. See [docs/06-observability.md](docs/06-observability.md).
246
+ Team state is observable via `agents teams list --json` / `agents teams status --json` (compact by default; add `--verbose` for the full per-teammate shape). External tools join it with `sessions --json` (teammates get `isTeamOrigin: true`) and `cloud list --json` (for `--cloud` teammates) to build a unified fleet view. See [docs/06-observability.md](docs/06-observability.md).
247
247
 
248
248
  ---
249
249
 
@@ -1,7 +1,7 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { listProfiles, getProfile, createProfile, deleteProfile, ensureDefaultBrowserProfile, getProfileRuntimeDir, extractConfiguredPort, findFreeProfilePort, getEndpointPresets, } from '../lib/browser/profiles.js';
4
- import { findBrowserPath, getPortOccupant } from '../lib/browser/chrome.js';
4
+ import { findBrowserPath, getPortOccupant, isLauncherScript } from '../lib/browser/chrome.js';
5
5
  import { listProfileCacheDirs, removeProfileCache, listAllProfileSnapshots, } from '../lib/browser/runtime-state.js';
6
6
  import { DEFAULT_VIEWPORT } from '../lib/browser/devices.js';
7
7
  import { discoverBrowserWsUrl, verifyBrowserIdentity } from '../lib/browser/cdp.js';
@@ -311,10 +311,28 @@ function registerProfilesCommands(browser) {
311
311
  process.exit(1);
312
312
  }
313
313
  const checks = [];
314
- // 1. Binary exists for declared browser type
314
+ // 1. Binary exists for declared browser type, and is a real executable we
315
+ // can drive — not a distro launcher script. findBrowserPath already
316
+ // unwraps the known Chromium wrappers to their ELF; if it still hands
317
+ // back a shebang script we couldn't resolve, `start` would fail with
318
+ // `CDP connection closed` (the wrapper re-execs the browser as a child,
319
+ // breaking the --remote-debugging-pipe transport — issue #229). Flag it
320
+ // here instead of letting launch fail opaquely.
315
321
  try {
316
322
  const binPath = findBrowserPath(profile.browser, profile.binary);
317
- checks.push({ label: 'binary', ok: true, detail: binPath });
323
+ if (isLauncherScript(binPath)) {
324
+ checks.push({
325
+ label: 'binary',
326
+ ok: false,
327
+ detail: `${binPath} is a launcher script, not the browser executable — ` +
328
+ `agents browser drives the browser over --remote-debugging-pipe and ` +
329
+ `can't attach to a wrapper that re-execs it. Point the profile at the ` +
330
+ `real binary (\`--binary /path/to/browser\`) or reinstall the standard package.`,
331
+ });
332
+ }
333
+ else {
334
+ checks.push({ label: 'binary', ok: true, detail: binPath });
335
+ }
318
336
  }
319
337
  catch (err) {
320
338
  checks.push({
@@ -351,7 +369,16 @@ function registerProfilesCommands(browser) {
351
369
  else {
352
370
  const occupant = getPortOccupant(port);
353
371
  if (!occupant) {
354
- checks.push({ label: 'port', ok: true, detail: `${port} is free` });
372
+ // A free port doesn't mean "ready to launch here": for a local
373
+ // profile we self-launch over an internal --remote-debugging-pipe and
374
+ // never bind this port. The port is consulted only to attach to a
375
+ // browser someone already started on it. Say so, so a green doctor
376
+ // can't be read as "the port is what launch depends on" (#229).
377
+ checks.push({
378
+ label: 'port',
379
+ ok: true,
380
+ detail: `${port} free — will self-launch over an internal pipe (port used only to attach to an already-running browser)`,
381
+ });
355
382
  }
356
383
  else {
357
384
  try {
@@ -0,0 +1,36 @@
1
+ import { Command } from 'commander';
2
+ import { type ComputerClient, type RPCResponse } from '../lib/computer-rpc.js';
3
+ export interface AppInfo {
4
+ pid: number;
5
+ name: string;
6
+ bundle_id: string;
7
+ active: boolean;
8
+ }
9
+ export declare function pickTarget(list: AppInfo[], opts: {
10
+ pid?: number;
11
+ bundle?: string;
12
+ }): {
13
+ ok: true;
14
+ app: AppInfo;
15
+ } | {
16
+ ok: false;
17
+ error: string;
18
+ };
19
+ export declare function parseXY(s: string, flag: string): {
20
+ x: number;
21
+ y: number;
22
+ };
23
+ export declare function buildElementOrCoords(opts: {
24
+ id?: string;
25
+ x?: number;
26
+ y?: number;
27
+ }): {
28
+ ok: true;
29
+ params: Record<string, unknown>;
30
+ } | {
31
+ ok: false;
32
+ error: string;
33
+ };
34
+ export declare function withClient<T>(fn: (client: ComputerClient) => Promise<T>): Promise<T>;
35
+ export declare function unwrap(r: RPCResponse): Record<string, unknown>;
36
+ export declare function registerActionCommands(program: Command): void;
@@ -0,0 +1,328 @@
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 { openComputerClient, describeTransport, } from '../lib/computer-rpc.js';
10
+ // Pure target picker — exercised by unit tests. Precedence: explicit --pid,
11
+ // then --bundle, then the frontmost active allow-listed app (the same default
12
+ // `screenshot` uses). Kept side-effect-free so the resolution rules are
13
+ // testable without a live daemon.
14
+ export function pickTarget(list, opts) {
15
+ if (opts.pid != null) {
16
+ const app = list.find((a) => a.pid === opts.pid);
17
+ // A --pid the daemon doesn't list (not allow-listed / not running) is
18
+ // still passed through: the daemon is the authority and will return a
19
+ // precise permission_denied / app_not_found. We don't second-guess it.
20
+ return { ok: true, app: app ?? { pid: opts.pid, name: '', bundle_id: '', active: false } };
21
+ }
22
+ if (opts.bundle) {
23
+ const app = list.find((a) => a.bundle_id === opts.bundle);
24
+ if (!app) {
25
+ return {
26
+ ok: false,
27
+ error: `bundle not in allow list (or not running): ${opts.bundle}\nadd Computer(${opts.bundle}) to a permissions group, then \`agents computer reload\``,
28
+ };
29
+ }
30
+ return { ok: true, app };
31
+ }
32
+ const active = list.find((a) => a.active);
33
+ if (!active) {
34
+ return {
35
+ ok: false,
36
+ error: 'no active app found in allow list\nadd Computer(<bundle-id>) to a permissions group, then `agents computer reload`',
37
+ };
38
+ }
39
+ return { ok: true, app: active };
40
+ }
41
+ // Parse an "x,y" coordinate pair. Pure + tested.
42
+ export function parseXY(s, flag) {
43
+ const parts = s.split(',').map((v) => v.trim());
44
+ if (parts.length !== 2) {
45
+ throw new Error(`${flag} must be "x,y" (got: ${s})`);
46
+ }
47
+ const x = Number(parts[0]);
48
+ const y = Number(parts[1]);
49
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
50
+ throw new Error(`${flag} must be two numbers "x,y" (got: ${s})`);
51
+ }
52
+ return { x, y };
53
+ }
54
+ // Build the element-or-coords target spec shared by click/type/scroll/etc.
55
+ // Pure + tested: returns the params fragment or an error string.
56
+ export function buildElementOrCoords(opts) {
57
+ if (opts.id)
58
+ return { ok: true, params: { element_id: opts.id } };
59
+ if (opts.x != null && opts.y != null)
60
+ return { ok: true, params: { x: opts.x, y: opts.y } };
61
+ return { ok: false, error: 'pass --id <@eN> (from `describe`) or --x <n> --y <n>' };
62
+ }
63
+ function reportMissingHelper() {
64
+ console.error('helper not built. Run: ./packages/computer-helper/scripts/build.sh debug');
65
+ process.exit(1);
66
+ }
67
+ // Open a client, run fn, always close. Fails fast if no helper is present.
68
+ export async function withClient(fn) {
69
+ if (describeTransport().kind === 'none')
70
+ reportMissingHelper();
71
+ const client = openComputerClient();
72
+ try {
73
+ return await fn(client);
74
+ }
75
+ finally {
76
+ await client.close();
77
+ }
78
+ }
79
+ // Unwrap an RPC response: print + exit on error, else return result.
80
+ export function unwrap(r) {
81
+ if (r.error) {
82
+ console.error(`error: ${r.error.code}: ${r.error.message}`);
83
+ process.exit(1);
84
+ }
85
+ return r.result ?? {};
86
+ }
87
+ // Resolve the target pid via list_apps + pickTarget, printing a precise error
88
+ // and exiting when no target matches.
89
+ async function resolveTargetPid(client, opts) {
90
+ // A directly-supplied pid skips the list_apps roundtrip — the daemon gates.
91
+ if (opts.pid != null)
92
+ return opts.pid;
93
+ const apps = unwrap(await client.call('list_apps'));
94
+ const list = apps.apps || [];
95
+ const picked = pickTarget(list, opts);
96
+ if (!picked.ok) {
97
+ console.error(picked.error);
98
+ process.exit(1);
99
+ }
100
+ return picked.app.pid;
101
+ }
102
+ function emit(result, json, human) {
103
+ if (json) {
104
+ console.log(JSON.stringify(result, null, 2));
105
+ }
106
+ else {
107
+ console.log(human());
108
+ }
109
+ }
110
+ // Add the shared --pid/--bundle target options to a verb.
111
+ function addTargetOpts(cmd) {
112
+ return cmd
113
+ .option('--bundle <id>', 'Bundle id of the target app (default: frontmost allow-listed app)')
114
+ .option('--pid <n>', 'Target pid directly (overrides --bundle)', (v) => parseInt(v, 10));
115
+ }
116
+ // Add the shared --id/--x/--y element-or-coords options to a verb.
117
+ function addElementOrCoordOpts(cmd) {
118
+ return cmd
119
+ .option('--id <@eN>', 'Element id from `describe`')
120
+ .option('--x <n>', 'X coordinate (global, points)', (v) => parseInt(v, 10))
121
+ .option('--y <n>', 'Y coordinate (global, points)', (v) => parseInt(v, 10));
122
+ }
123
+ export function registerActionCommands(program) {
124
+ // apps — list_apps
125
+ addTargetOpts(program
126
+ .command('apps')
127
+ .description('List apps the daemon may drive (allow-listed + running)')
128
+ .option('--json', 'Emit JSON')).action(async (opts) => {
129
+ await withClient(async (client) => {
130
+ const res = unwrap(await client.call('list_apps'));
131
+ const list = res.apps || [];
132
+ emit(res, Boolean(opts.json), () => list.length === 0
133
+ ? '(no allow-listed apps running)'
134
+ : list
135
+ .map((a) => `${a.active ? '*' : ' '} ${String(a.pid).padStart(6)} ${a.bundle_id} ${a.name}`)
136
+ .join('\n'));
137
+ });
138
+ });
139
+ // describe — AX tree
140
+ addTargetOpts(program
141
+ .command('describe')
142
+ .description('Dump the accessibility tree (element ids feed click/type --id)')
143
+ .option('--depth <n>', 'Max tree depth', (v) => parseInt(v, 10))
144
+ .option('--json', 'Emit compact JSON (default: pretty)')).action(async (opts) => {
145
+ await withClient(async (client) => {
146
+ const pid = await resolveTargetPid(client, opts);
147
+ const params = { pid };
148
+ if (opts.depth != null)
149
+ params.max_depth = opts.depth;
150
+ const res = unwrap(await client.call('describe', params));
151
+ // The tree is inherently structured — always JSON, pretty unless --json.
152
+ console.log(JSON.stringify(opts.json ? res : res.tree ?? res, null, 2));
153
+ });
154
+ });
155
+ // click
156
+ addElementOrCoordOpts(addTargetOpts(program
157
+ .command('click')
158
+ .description('Click an element (--id) or screen coordinate (--x --y)')
159
+ .option('--count <n>', 'Click count (2 = double-click)', (v) => parseInt(v, 10))
160
+ .option('--background', 'Focus-safe postToPid delivery (plain AppKit only; skips HID tap)')
161
+ .option('--json', 'Emit JSON'))).action(async (opts) => {
162
+ await withClient(async (client) => {
163
+ const pid = await resolveTargetPid(client, opts);
164
+ const spec = buildElementOrCoords(opts);
165
+ if (!spec.ok) {
166
+ console.error(spec.error);
167
+ process.exit(1);
168
+ }
169
+ const params = { pid, ...spec.params };
170
+ if (opts.count != null)
171
+ params.count = opts.count;
172
+ if (opts.background)
173
+ params.background = true;
174
+ const res = unwrap(await client.call('click', params));
175
+ emit(res, Boolean(opts.json), () => `clicked (${res.action ?? 'ok'})`);
176
+ });
177
+ });
178
+ // right-click
179
+ addElementOrCoordOpts(addTargetOpts(program
180
+ .command('right-click')
181
+ .description('Right-click (context menu) an element or coordinate')
182
+ .option('--json', 'Emit JSON'))).action(async (opts) => {
183
+ await withClient(async (client) => {
184
+ const pid = await resolveTargetPid(client, opts);
185
+ const spec = buildElementOrCoords(opts);
186
+ if (!spec.ok) {
187
+ console.error(spec.error);
188
+ process.exit(1);
189
+ }
190
+ const res = unwrap(await client.call('right_click', { pid, ...spec.params }));
191
+ emit(res, Boolean(opts.json), () => `right-clicked (${res.method ?? 'ok'})`);
192
+ });
193
+ });
194
+ // type — set value on a field (--id) or paste at coords, optional commit
195
+ addElementOrCoordOpts(addTargetOpts(program
196
+ .command('type')
197
+ .description('Set a field value (--id) or paste at a coordinate (--x --y)')
198
+ .requiredOption('--text <s>', 'Text to enter')
199
+ .option('--commit', 'Commit after typing (AXConfirm / Return) so the value reaches the model')
200
+ .option('--allow-secure-field', 'Permit typing into a password field')
201
+ .option('--json', 'Emit JSON'))).action(async (opts) => {
202
+ await withClient(async (client) => {
203
+ const pid = await resolveTargetPid(client, opts);
204
+ const spec = buildElementOrCoords(opts);
205
+ if (!spec.ok) {
206
+ console.error(spec.error);
207
+ process.exit(1);
208
+ }
209
+ const params = { pid, ...spec.params, text: opts.text };
210
+ if (opts.commit)
211
+ params.commit = true;
212
+ if (opts.allowSecureField)
213
+ params.allow_secure_field = true;
214
+ const res = unwrap(await client.call('type', params));
215
+ emit(res, Boolean(opts.json), () => `typed ${opts.text.length} char(s)${res.committed ? ' (committed)' : ''}`);
216
+ });
217
+ });
218
+ // type-text — stream an arbitrary unicode string into the focused field
219
+ addTargetOpts(program
220
+ .command('type-text')
221
+ .description('Type an arbitrary unicode string into the focused field (focus first via click/focus)')
222
+ .requiredOption('--text <s>', 'Text to type')
223
+ .option('--commit', 'Press Return after typing')
224
+ .option('--json', 'Emit JSON')).action(async (opts) => {
225
+ await withClient(async (client) => {
226
+ const pid = await resolveTargetPid(client, opts);
227
+ const params = { pid, text: opts.text };
228
+ if (opts.commit)
229
+ params.commit = true;
230
+ const res = unwrap(await client.call('type_text', params));
231
+ emit(res, Boolean(opts.json), () => `typed ${res.chars ?? opts.text.length} char(s)`);
232
+ });
233
+ });
234
+ // key — single chord
235
+ addTargetOpts(program
236
+ .command('key')
237
+ .description('Send a key chord, e.g. "cmd+shift+s", "enter", "esc"')
238
+ .requiredOption('--keys <chord>', 'Key chord')
239
+ .option('--json', 'Emit JSON')).action(async (opts) => {
240
+ await withClient(async (client) => {
241
+ const pid = await resolveTargetPid(client, opts);
242
+ const res = unwrap(await client.call('key', { pid, keys: opts.keys }));
243
+ emit(res, Boolean(opts.json), () => `sent ${opts.keys}`);
244
+ });
245
+ });
246
+ // drag — from one point to another
247
+ addTargetOpts(program
248
+ .command('drag')
249
+ .description('Drag from one coordinate to another')
250
+ .requiredOption('--from <x,y>', 'Start coordinate "x,y"')
251
+ .requiredOption('--to <x,y>', 'End coordinate "x,y"')
252
+ .option('--button <left|right>', 'Mouse button', 'left')
253
+ .option('--background', 'Focus-safe postToPid delivery (plain AppKit only)')
254
+ .option('--json', 'Emit JSON')).action(async (opts) => {
255
+ let from;
256
+ let to;
257
+ try {
258
+ from = parseXY(opts.from, '--from');
259
+ to = parseXY(opts.to, '--to');
260
+ }
261
+ catch (err) {
262
+ console.error(err.message);
263
+ process.exit(1);
264
+ }
265
+ await withClient(async (client) => {
266
+ const pid = await resolveTargetPid(client, opts);
267
+ const params = {
268
+ pid,
269
+ from: [from.x, from.y],
270
+ to: [to.x, to.y],
271
+ button: opts.button,
272
+ };
273
+ if (opts.background)
274
+ params.background = true;
275
+ const res = unwrap(await client.call('drag', params));
276
+ emit(res, Boolean(opts.json), () => `dragged ${opts.from} -> ${opts.to} (${res.method ?? 'ok'})`);
277
+ });
278
+ });
279
+ // scroll — by delta at an element or coordinate
280
+ addElementOrCoordOpts(addTargetOpts(program
281
+ .command('scroll')
282
+ .description('Scroll by a pixel delta at an element or coordinate')
283
+ .option('--dy <n>', 'Vertical delta (negative = down)', (v) => parseInt(v, 10))
284
+ .option('--dx <n>', 'Horizontal delta', (v) => parseInt(v, 10))
285
+ .option('--json', 'Emit JSON'))).action(async (opts) => {
286
+ await withClient(async (client) => {
287
+ const pid = await resolveTargetPid(client, opts);
288
+ const params = { pid };
289
+ if (opts.id)
290
+ params.element_id = opts.id;
291
+ if (opts.x != null)
292
+ params.x = opts.x;
293
+ if (opts.y != null)
294
+ params.y = opts.y;
295
+ if (opts.dy != null)
296
+ params.dy = opts.dy;
297
+ if (opts.dx != null)
298
+ params.dx = opts.dx;
299
+ const res = unwrap(await client.call('scroll', params));
300
+ emit(res, Boolean(opts.json), () => `scrolled (${res.method ?? 'ok'})`);
301
+ });
302
+ });
303
+ // ax-action — perform any advertised AX action on an element
304
+ addTargetOpts(program
305
+ .command('ax-action')
306
+ .description('Perform an arbitrary AX action (AXConfirm, AXCancel, AXRaise, ...) on an element')
307
+ .requiredOption('--id <@eN>', 'Element id from `describe`')
308
+ .requiredOption('--action <name>', 'AX action name')
309
+ .option('--json', 'Emit JSON')).action(async (opts) => {
310
+ await withClient(async (client) => {
311
+ const pid = await resolveTargetPid(client, opts);
312
+ const res = unwrap(await client.call('ax_action', { pid, element_id: opts.id, action: opts.action }));
313
+ emit(res, Boolean(opts.json), () => `performed ${opts.action}`);
314
+ });
315
+ });
316
+ // focus — set keyboard focus to an element
317
+ addTargetOpts(program
318
+ .command('focus')
319
+ .description('Set keyboard focus to an element (so type-text/key land there)')
320
+ .requiredOption('--id <@eN>', 'Element id from `describe`')
321
+ .option('--json', 'Emit JSON')).action(async (opts) => {
322
+ await withClient(async (client) => {
323
+ const pid = await resolveTargetPid(client, opts);
324
+ const res = unwrap(await client.call('set_focus', { pid, element_id: opts.id }));
325
+ emit(res, Boolean(opts.json), () => `focused ${opts.id}`);
326
+ });
327
+ });
328
+ }
@@ -3,33 +3,40 @@ import * as fs from 'fs';
3
3
  import * as os from 'os';
4
4
  import * as path from 'path';
5
5
  import { registerCommandGroups } from '../lib/help.js';
6
- import { openComputerClient, resolveHelperApp, resolveHelperExec, resolveSocketPath, resolveLogPath, resolvePolicyPath, resolvePeersPath, describeTransport, loadComputerAllowList, loadDefaultPeers, writeComputerPolicy, writeComputerPeers, } from '../lib/computer-rpc.js';
6
+ import { openComputerClient, resolveHelperApp, resolveHelperExec, resolveSocketPath, resolveLogPath, resolvePolicyPath, resolvePeersPath, loadComputerAllowList, loadDefaultPeers, writeComputerPolicy, writeComputerPeers, } from '../lib/computer-rpc.js';
7
+ import { registerActionCommands, withClient, unwrap, pickTarget } from './computer-actions.js';
7
8
  // Help groups — mirror `agents browser` so the mental model carries over.
8
9
  const COMPUTER_HELP_GROUPS = [
9
- { title: 'Installation', names: ['install-helper'] },
10
+ { title: 'Installation', names: ['setup'] },
10
11
  { title: 'Daemon lifecycle', names: ['start', 'stop', 'reload', 'status'] },
11
- { title: 'Capture evidence', names: ['screenshot'] },
12
+ { title: 'Observe', names: ['apps', 'describe', 'screenshot'] },
13
+ { title: 'Interact', names: ['click', 'right-click', 'type', 'type-text', 'key', 'drag', 'scroll', 'ax-action', 'focus'] },
12
14
  ];
13
15
  export function registerComputerCommand(program) {
14
16
  const computer = program
15
17
  .command('computer')
16
- .description('Drive macOS apps via Accessibility — list, screenshot, click, type');
18
+ .description('Drive macOS apps via Accessibility — list, screenshot, click, type (macOS only)')
19
+ // The whole subsystem is macOS Accessibility / TCC. Fail fast with a clear
20
+ // message on other platforms instead of a downstream ENOENT / launchctl error.
21
+ .hook('preAction', () => {
22
+ if (process.platform !== 'darwin') {
23
+ console.error('agents computer: macOS only — it drives apps via the macOS Accessibility API.');
24
+ process.exit(1);
25
+ }
26
+ });
17
27
  registerComputerSubcommands(computer);
18
28
  registerCommandGroups(computer, COMPUTER_HELP_GROUPS);
19
29
  }
20
30
  export function registerComputerSubcommands(program) {
21
- registerInstallHelperCommand(program);
31
+ registerSetupCommand(program);
22
32
  registerStartCommand(program);
23
33
  registerStopCommand(program);
24
34
  registerReloadCommand(program);
25
35
  registerStatusCommand(program);
26
36
  registerScreenshotCommand(program);
37
+ registerActionCommands(program);
27
38
  registerCommandGroups(program, COMPUTER_HELP_GROUPS);
28
39
  }
29
- function reportMissingHelper() {
30
- console.error('helper not built. Run: ./packages/computer-helper/scripts/build.sh debug');
31
- process.exit(1);
32
- }
33
40
  function registerStatusCommand(program) {
34
41
  program
35
42
  .command('status')
@@ -50,7 +57,7 @@ function registerStatusCommand(program) {
50
57
  console.log(`peers: ${callers.length} caller${callers.length === 1 ? '' : 's'} (peer-auth on socket)`);
51
58
  if (!installed) {
52
59
  console.log('');
53
- console.log('Run: agents computer install-helper');
60
+ console.log('Run: agents computer setup');
54
61
  return;
55
62
  }
56
63
  if (!socketUp) {
@@ -84,50 +91,55 @@ function registerStatusCommand(program) {
84
91
  function registerScreenshotCommand(program) {
85
92
  program
86
93
  .command('screenshot')
87
- .description('Capture a JPEG of the frontmost window of a bundle id (default: frontmost app)')
88
- .option('--bundle <id>', 'Bundle id to capture (default: bundle id of frontmost app)')
94
+ .description('Capture a window (default: largest), enumerate windows (--list), or the whole display (--display)')
95
+ .option('--bundle <id>', 'Bundle id to capture (default: frontmost allow-listed app)')
96
+ .option('--pid <n>', 'Target pid directly (overrides --bundle)', (v) => parseInt(v, 10))
97
+ .option('--list', 'List the app\'s windows (id/title/layer/bounds) instead of capturing — reveals modals/popups')
98
+ .option('--window-id <n>', 'Capture a specific window by id (from --list)', (v) => parseInt(v, 10))
99
+ .option('--display', 'Capture the whole display the app is on (composites stacked modals)')
89
100
  .option('--out <path>', 'Output JPEG path', './computer-screenshot.jpg')
90
101
  .option('--quality <n>', 'JPEG quality 1-100', (v) => parseInt(v, 10), 85)
102
+ .option('--json', 'Emit JSON (metadata for captures; window list for --list)')
91
103
  .action(async (opts) => {
92
- const transport = describeTransport();
93
- if (transport.kind === 'none')
94
- reportMissingHelper();
95
104
  const quality = Math.max(1, Math.min(100, opts.quality || 85));
96
- const client = openComputerClient();
97
- try {
98
- // Step 1: list_apps to get the candidate set.
99
- const apps = await client.call('list_apps');
100
- if (apps.error) {
101
- console.error(`error: ${apps.error.code}: ${apps.error.message}`);
102
- process.exit(1);
103
- }
104
- const list = apps.result?.apps || [];
105
- let target;
106
- if (opts.bundle) {
107
- target = list.find((a) => a.bundle_id === opts.bundle);
108
- if (!target) {
109
- console.error(`bundle not in allow list (or not running): ${opts.bundle}`);
110
- console.error(`add Computer(${opts.bundle}) to a permissions group, then \`agents computer reload\``);
105
+ await withClient(async (client) => {
106
+ // Resolve the target pid (explicit --pid, else --bundle, else frontmost).
107
+ let pid = opts.pid;
108
+ if (pid == null) {
109
+ const list = unwrap(await client.call('list_apps')).apps || [];
110
+ const picked = pickTarget(list, { bundle: opts.bundle });
111
+ if (!picked.ok) {
112
+ console.error(picked.error);
111
113
  process.exit(1);
112
114
  }
115
+ pid = picked.app.pid;
113
116
  }
114
- else {
115
- target = list.find((a) => a.active);
116
- if (!target) {
117
- console.error('no active app found in allow list');
118
- console.error('add Computer(<bundle-id>) to a permissions group, then `agents computer reload`');
119
- process.exit(1);
117
+ // --list: enumerate windows, no image.
118
+ if (opts.list) {
119
+ const res = unwrap(await client.call('screenshot', { pid, list: true }));
120
+ const windows = res.windows || [];
121
+ if (opts.json) {
122
+ console.log(JSON.stringify(res, null, 2));
120
123
  }
124
+ else if (windows.length === 0) {
125
+ console.log('(no windows)');
126
+ }
127
+ else {
128
+ for (const w of windows) {
129
+ const b = w.bounds || [];
130
+ console.log(`${String(w.window_id).padStart(8)} layer ${w.layer} [${b.join(',')}] ${w.title || '(untitled)'}`);
131
+ }
132
+ }
133
+ return;
121
134
  }
122
- // Step 2: screenshot.
123
- const shot = await client.call('screenshot', { pid: target.pid, quality });
124
- if (shot.error) {
125
- console.error(`error: ${shot.error.code}: ${shot.error.message}`);
126
- process.exit(1);
127
- }
128
- const b64 = shot.result?.image_data;
129
- const width = shot.result?.width;
130
- const height = shot.result?.height;
135
+ // Capture: window (default / --window-id) or full display.
136
+ const params = { pid, quality };
137
+ if (opts.display)
138
+ params.display = true;
139
+ else if (opts.windowId != null)
140
+ params.window_id = opts.windowId;
141
+ const res = unwrap(await client.call('screenshot', params));
142
+ const b64 = res.image_data;
131
143
  if (!b64) {
132
144
  console.error('helper returned no image_data');
133
145
  process.exit(1);
@@ -135,14 +147,20 @@ function registerScreenshotCommand(program) {
135
147
  const buf = Buffer.from(b64, 'base64');
136
148
  const outPath = path.resolve(opts.out);
137
149
  fs.writeFileSync(outPath, buf);
138
- console.log(`saved: ${outPath} (${width ?? '?'}x${height ?? '?'}, ${buf.byteLength} bytes)`);
139
- }
140
- finally {
141
- await client.close();
142
- }
150
+ if (opts.json) {
151
+ // Drop the heavy base64 from the metadata echo; report where it went.
152
+ const meta = { ...res, image_data: `<saved to ${outPath}>` };
153
+ console.log(JSON.stringify(meta, null, 2));
154
+ }
155
+ else {
156
+ const origin = res.origin || [];
157
+ const originStr = origin.length === 2 ? `, origin [${origin.join(',')}], scale ${res.scale ?? '?'}` : '';
158
+ console.log(`saved: ${outPath} (${res.width ?? '?'}x${res.height ?? '?'}, ${buf.byteLength} bytes${originStr})`);
159
+ }
160
+ });
143
161
  });
144
162
  }
145
- // install-helper:
163
+ // setup (alias: install-helper):
146
164
  // 1. resolve dist .app
147
165
  // 2. copy to /Applications/Computer Helper.app
148
166
  // 3. codesign --verify the destination
@@ -159,9 +177,10 @@ const HELPER_BUNDLE_ID = 'com.phnx-labs.computer-helper';
159
177
  const HELPER_APP_NAME = 'Computer Helper.app';
160
178
  const HELPER_APP_DEST = `/Applications/${HELPER_APP_NAME}`;
161
179
  const HELPER_LABEL = HELPER_BUNDLE_ID;
162
- function registerInstallHelperCommand(program) {
180
+ function registerSetupCommand(program) {
163
181
  program
164
- .command('install-helper')
182
+ .command('setup')
183
+ .alias('install-helper')
165
184
  .description('Install ComputerHelper.app to /Applications/ (does NOT activate the daemon — run `start` to enable)')
166
185
  .action(async () => {
167
186
  const srcApp = resolveHelperApp();
@@ -255,12 +274,12 @@ function registerStartCommand(program) {
255
274
  const logPath = resolveLogPath();
256
275
  if (!fs.existsSync(plistPath)) {
257
276
  console.error(`plist not found at ${plistPath}`);
258
- console.error('run: agents computer install-helper');
277
+ console.error('run: agents computer setup');
259
278
  process.exit(1);
260
279
  }
261
280
  if (!fs.existsSync(HELPER_APP_DEST)) {
262
281
  console.error(`helper app not found at ${HELPER_APP_DEST}`);
263
- console.error('run: agents computer install-helper');
282
+ console.error('run: agents computer setup');
264
283
  process.exit(1);
265
284
  }
266
285
  const uid = process.getuid?.();