@phnx-labs/agents-cli 1.20.6 → 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.
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)**
@@ -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,22 +3,24 @@ 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
- .command('computers')
17
+ .command('computer')
16
18
  .description('Drive macOS apps via Accessibility — list, screenshot, click, type (macOS only)')
17
19
  // The whole subsystem is macOS Accessibility / TCC. Fail fast with a clear
18
20
  // message on other platforms instead of a downstream ENOENT / launchctl error.
19
21
  .hook('preAction', () => {
20
22
  if (process.platform !== 'darwin') {
21
- console.error('agents computers: macOS only — it drives apps via the macOS Accessibility API.');
23
+ console.error('agents computer: macOS only — it drives apps via the macOS Accessibility API.');
22
24
  process.exit(1);
23
25
  }
24
26
  });
@@ -26,18 +28,15 @@ export function registerComputerCommand(program) {
26
28
  registerCommandGroups(computer, COMPUTER_HELP_GROUPS);
27
29
  }
28
30
  export function registerComputerSubcommands(program) {
29
- registerInstallHelperCommand(program);
31
+ registerSetupCommand(program);
30
32
  registerStartCommand(program);
31
33
  registerStopCommand(program);
32
34
  registerReloadCommand(program);
33
35
  registerStatusCommand(program);
34
36
  registerScreenshotCommand(program);
37
+ registerActionCommands(program);
35
38
  registerCommandGroups(program, COMPUTER_HELP_GROUPS);
36
39
  }
37
- function reportMissingHelper() {
38
- console.error('helper not built. Run: ./packages/computer-helper/scripts/build.sh debug');
39
- process.exit(1);
40
- }
41
40
  function registerStatusCommand(program) {
42
41
  program
43
42
  .command('status')
@@ -58,7 +57,7 @@ function registerStatusCommand(program) {
58
57
  console.log(`peers: ${callers.length} caller${callers.length === 1 ? '' : 's'} (peer-auth on socket)`);
59
58
  if (!installed) {
60
59
  console.log('');
61
- console.log('Run: agents computer install-helper');
60
+ console.log('Run: agents computer setup');
62
61
  return;
63
62
  }
64
63
  if (!socketUp) {
@@ -92,50 +91,55 @@ function registerStatusCommand(program) {
92
91
  function registerScreenshotCommand(program) {
93
92
  program
94
93
  .command('screenshot')
95
- .description('Capture a JPEG of the frontmost window of a bundle id (default: frontmost app)')
96
- .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)')
97
100
  .option('--out <path>', 'Output JPEG path', './computer-screenshot.jpg')
98
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)')
99
103
  .action(async (opts) => {
100
- const transport = describeTransport();
101
- if (transport.kind === 'none')
102
- reportMissingHelper();
103
104
  const quality = Math.max(1, Math.min(100, opts.quality || 85));
104
- const client = openComputerClient();
105
- try {
106
- // Step 1: list_apps to get the candidate set.
107
- const apps = await client.call('list_apps');
108
- if (apps.error) {
109
- console.error(`error: ${apps.error.code}: ${apps.error.message}`);
110
- process.exit(1);
111
- }
112
- const list = apps.result?.apps || [];
113
- let target;
114
- if (opts.bundle) {
115
- target = list.find((a) => a.bundle_id === opts.bundle);
116
- if (!target) {
117
- console.error(`bundle not in allow list (or not running): ${opts.bundle}`);
118
- 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);
119
113
  process.exit(1);
120
114
  }
115
+ pid = picked.app.pid;
121
116
  }
122
- else {
123
- target = list.find((a) => a.active);
124
- if (!target) {
125
- console.error('no active app found in allow list');
126
- console.error('add Computer(<bundle-id>) to a permissions group, then `agents computer reload`');
127
- 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));
128
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;
129
134
  }
130
- // Step 2: screenshot.
131
- const shot = await client.call('screenshot', { pid: target.pid, quality });
132
- if (shot.error) {
133
- console.error(`error: ${shot.error.code}: ${shot.error.message}`);
134
- process.exit(1);
135
- }
136
- const b64 = shot.result?.image_data;
137
- const width = shot.result?.width;
138
- 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;
139
143
  if (!b64) {
140
144
  console.error('helper returned no image_data');
141
145
  process.exit(1);
@@ -143,14 +147,20 @@ function registerScreenshotCommand(program) {
143
147
  const buf = Buffer.from(b64, 'base64');
144
148
  const outPath = path.resolve(opts.out);
145
149
  fs.writeFileSync(outPath, buf);
146
- console.log(`saved: ${outPath} (${width ?? '?'}x${height ?? '?'}, ${buf.byteLength} bytes)`);
147
- }
148
- finally {
149
- await client.close();
150
- }
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
+ });
151
161
  });
152
162
  }
153
- // install-helper:
163
+ // setup (alias: install-helper):
154
164
  // 1. resolve dist .app
155
165
  // 2. copy to /Applications/Computer Helper.app
156
166
  // 3. codesign --verify the destination
@@ -167,9 +177,10 @@ const HELPER_BUNDLE_ID = 'com.phnx-labs.computer-helper';
167
177
  const HELPER_APP_NAME = 'Computer Helper.app';
168
178
  const HELPER_APP_DEST = `/Applications/${HELPER_APP_NAME}`;
169
179
  const HELPER_LABEL = HELPER_BUNDLE_ID;
170
- function registerInstallHelperCommand(program) {
180
+ function registerSetupCommand(program) {
171
181
  program
172
- .command('install-helper')
182
+ .command('setup')
183
+ .alias('install-helper')
173
184
  .description('Install ComputerHelper.app to /Applications/ (does NOT activate the daemon — run `start` to enable)')
174
185
  .action(async () => {
175
186
  const srcApp = resolveHelperApp();
@@ -263,12 +274,12 @@ function registerStartCommand(program) {
263
274
  const logPath = resolveLogPath();
264
275
  if (!fs.existsSync(plistPath)) {
265
276
  console.error(`plist not found at ${plistPath}`);
266
- console.error('run: agents computer install-helper');
277
+ console.error('run: agents computer setup');
267
278
  process.exit(1);
268
279
  }
269
280
  if (!fs.existsSync(HELPER_APP_DEST)) {
270
281
  console.error(`helper app not found at ${HELPER_APP_DEST}`);
271
- console.error('run: agents computer install-helper');
282
+ console.error('run: agents computer setup');
272
283
  process.exit(1);
273
284
  }
274
285
  const uid = process.getuid?.();
@@ -1,14 +1,30 @@
1
1
  /**
2
- * `agents inspect <agent>[@version]`single-agent, single-version detail view.
2
+ * `agents inspect <target>`detail view for one agent+version or one DotAgents repo.
3
3
  *
4
- * Summary mode shows the per-version header (paths, shim, capabilities, resource
5
- * counts, sessions). Drill-down flags (`--skills`, `--hooks`, `--mcp`, ...) list
6
- * one resource kind; passing a positional query to the same flag fuzzy-searches
7
- * for a single resource and prints its detail. Resource names render as OSC-8
8
- * hyperlinks to the marker file (SKILL.md / WORKFLOW.md / AGENT.md / the file
9
- * itself) so users can click straight to the source.
4
+ * Agent targets (`claude`, `claude@2.1.170`) show the per-version header (paths,
5
+ * shim, capabilities, resource counts, sessions). Repo targets (`user`, `system`,
6
+ * `project`, a registered extra-repo alias, or a filesystem path to a repo with a
7
+ * `.agents/` dir or to a DotAgents root itself) show the repo root, git state, and
8
+ * per-kind resource counts. Drill-down flags (`--skills`, `--hooks`, `--mcp`, ...)
9
+ * list one resource kind for either target form; passing a positional query to the
10
+ * same flag fuzzy-searches for a single resource and prints its detail. Resource
11
+ * names render as OSC-8 hyperlinks to the marker file (SKILL.md / WORKFLOW.md /
12
+ * AGENT.md / the file itself) so users can click straight to the source.
10
13
  */
11
14
  import { Command } from 'commander';
15
+ /** Resource kinds the inspect command can drill into. */
16
+ declare const DRILLABLE_KINDS: readonly ["commands", "skills", "hooks", "mcp", "rules", "plugins", "workflows", "subagents"];
17
+ type DrillableKind = typeof DRILLABLE_KINDS[number];
18
+ interface ResourceItem {
19
+ name: string;
20
+ source: string;
21
+ /** Absolute path to the resource entry (file or directory). */
22
+ path: string;
23
+ /** Path the OSC-8 link should point at — marker file inside bundles, else `path`. */
24
+ linkTarget: string;
25
+ /** One-line description (frontmatter `description:` or first non-frontmatter line). */
26
+ description: string;
27
+ }
12
28
  interface InspectOptions {
13
29
  brief?: boolean;
14
30
  json?: boolean;
@@ -23,4 +39,19 @@ interface InspectOptions {
23
39
  }
24
40
  export declare function registerInspectCommand(program: Command): void;
25
41
  export declare function inspectAction(target: string, options: InspectOptions): Promise<void>;
42
+ export interface RepoTarget {
43
+ /** Display label: 'user' | 'system' | 'project', an extra-repo alias, or a path-derived name. */
44
+ label: string;
45
+ /** Absolute path to the DotAgents root (the dir holding commands/, skills/, ...). */
46
+ root: string;
47
+ }
48
+ /**
49
+ * Resolve a non-agent target as a DotAgents repo: the built-in layer names,
50
+ * a registered extra-repo alias, or a filesystem path. Paths accept either a
51
+ * DotAgents root itself or a repo whose `.agents/` dir should be inspected.
52
+ * Returns null when the target is none of these.
53
+ */
54
+ export declare function resolveRepoTarget(target: string, cwd?: string): RepoTarget | null;
55
+ /** List one resource kind from a single repo root — no layering, no overrides. */
56
+ export declare function collectRepoKind(repo: RepoTarget, kind: DrillableKind): ResourceItem[];
26
57
  export {};