@phnx-labs/agents-cli 1.20.6 → 1.20.8
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 +13 -0
- package/dist/commands/computer-actions.d.ts +55 -0
- package/dist/commands/computer-actions.js +486 -0
- package/dist/commands/computer.js +67 -56
- package/dist/commands/inspect.d.ts +38 -7
- package/dist/commands/inspect.js +194 -24
- package/dist/commands/sessions.js +9 -12
- package/dist/commands/setup.js +2 -2
- package/dist/commands/versions.js +2 -2
- package/dist/index.js +23 -1
- package/dist/lib/computer-rpc.d.ts +2 -0
- package/dist/lib/computer-rpc.js +21 -1
- package/dist/lib/daemon.js +4 -7
- package/dist/lib/exec.d.ts +9 -0
- package/dist/lib/exec.js +61 -5
- package/dist/lib/platform/exec.d.ts +9 -0
- package/dist/lib/platform/exec.js +24 -0
- package/dist/lib/platform/index.d.ts +20 -0
- package/dist/lib/platform/index.js +20 -0
- package/dist/lib/platform/paths.d.ts +22 -0
- package/dist/lib/platform/paths.js +49 -0
- package/dist/lib/platform/process.d.ts +12 -0
- package/dist/lib/platform/process.js +22 -0
- package/dist/lib/pty-client.js +13 -5
- package/dist/lib/pty-server.d.ts +24 -1
- package/dist/lib/pty-server.js +102 -25
- package/dist/lib/refresh.js +2 -2
- package/dist/lib/session/artifacts.js +8 -2
- package/dist/lib/shims.d.ts +13 -8
- package/dist/lib/shims.js +84 -4
- package/dist/lib/teams/agents.js +5 -7
- package/package.json +1 -1
- 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)**
|
|
@@ -0,0 +1,55 @@
|
|
|
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 buildRaiseParams(opts: {
|
|
35
|
+
windowId?: number;
|
|
36
|
+
title?: string;
|
|
37
|
+
}): Record<string, unknown>;
|
|
38
|
+
export declare function buildWaitParams(opts: {
|
|
39
|
+
duration?: number;
|
|
40
|
+
id?: string;
|
|
41
|
+
until?: string;
|
|
42
|
+
role?: string;
|
|
43
|
+
label?: string;
|
|
44
|
+
identifier?: string;
|
|
45
|
+
timeout?: number;
|
|
46
|
+
}): {
|
|
47
|
+
ok: true;
|
|
48
|
+
params: Record<string, unknown>;
|
|
49
|
+
} | {
|
|
50
|
+
ok: false;
|
|
51
|
+
error: string;
|
|
52
|
+
};
|
|
53
|
+
export declare function withClient<T>(fn: (client: ComputerClient) => Promise<T>): Promise<T>;
|
|
54
|
+
export declare function unwrap(r: RPCResponse): Record<string, unknown>;
|
|
55
|
+
export declare function registerActionCommands(program: Command): void;
|
|
@@ -0,0 +1,486 @@
|
|
|
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
|
+
// Build the focus_window params for `raise`. Pure + tested: window_id and
|
|
64
|
+
// title are both optional refinements over the app-level activate.
|
|
65
|
+
export function buildRaiseParams(opts) {
|
|
66
|
+
const params = {};
|
|
67
|
+
if (opts.windowId != null)
|
|
68
|
+
params.window_id = opts.windowId;
|
|
69
|
+
if (opts.title)
|
|
70
|
+
params.title = opts.title;
|
|
71
|
+
return params;
|
|
72
|
+
}
|
|
73
|
+
// Build the wait RPC params. Pure + tested. Three modes, mirroring the
|
|
74
|
+
// daemon's Wait.run: --duration (unconditional sleep), --id + --until
|
|
75
|
+
// (cached-element poll), or --role/--label/--identifier (live locator poll).
|
|
76
|
+
export function buildWaitParams(opts) {
|
|
77
|
+
if (opts.duration != null) {
|
|
78
|
+
return { ok: true, params: { duration_ms: opts.duration } };
|
|
79
|
+
}
|
|
80
|
+
const params = {};
|
|
81
|
+
if (opts.until)
|
|
82
|
+
params.until = opts.until;
|
|
83
|
+
if (opts.timeout != null)
|
|
84
|
+
params.timeout_ms = opts.timeout;
|
|
85
|
+
if (opts.id) {
|
|
86
|
+
return { ok: true, params: { ...params, element_id: opts.id } };
|
|
87
|
+
}
|
|
88
|
+
const locator = {};
|
|
89
|
+
if (opts.role)
|
|
90
|
+
locator.role = opts.role;
|
|
91
|
+
if (opts.label)
|
|
92
|
+
locator.label = opts.label;
|
|
93
|
+
if (opts.identifier)
|
|
94
|
+
locator.identifier = opts.identifier;
|
|
95
|
+
if (Object.keys(locator).length === 0) {
|
|
96
|
+
return { ok: false, error: 'pass --duration <ms>, --id <@eN>, or a locator (--role/--label/--identifier)' };
|
|
97
|
+
}
|
|
98
|
+
return { ok: true, params: { ...params, locator } };
|
|
99
|
+
}
|
|
100
|
+
// postToPid keyboard delivery is dropped by key-window-gated apps (Parallels
|
|
101
|
+
// VMs and friends) — when the daemon reports the target was not frontmost,
|
|
102
|
+
// the keystrokes may have landed nowhere. Surface that loudly on stderr.
|
|
103
|
+
function warnIfNotFrontmost(res) {
|
|
104
|
+
if (res.frontmost === false) {
|
|
105
|
+
console.error('warning: target was not the frontmost app — keystrokes may have been dropped. Run `agents computer raise` first.');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function reportMissingHelper() {
|
|
109
|
+
console.error('helper not built. Run: ./packages/computer-helper/scripts/build.sh debug');
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
// Open a client, run fn, always close. Fails fast if no helper is present.
|
|
113
|
+
export async function withClient(fn) {
|
|
114
|
+
if (describeTransport().kind === 'none')
|
|
115
|
+
reportMissingHelper();
|
|
116
|
+
const client = openComputerClient();
|
|
117
|
+
try {
|
|
118
|
+
return await fn(client);
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
await client.close();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Unwrap an RPC response: print + exit on error, else return result.
|
|
125
|
+
export function unwrap(r) {
|
|
126
|
+
if (r.error) {
|
|
127
|
+
console.error(`error: ${r.error.code}: ${r.error.message}`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
return r.result ?? {};
|
|
131
|
+
}
|
|
132
|
+
// Resolve the target pid via list_apps + pickTarget, printing a precise error
|
|
133
|
+
// and exiting when no target matches.
|
|
134
|
+
async function resolveTargetPid(client, opts) {
|
|
135
|
+
// A directly-supplied pid skips the list_apps roundtrip — the daemon gates.
|
|
136
|
+
if (opts.pid != null)
|
|
137
|
+
return opts.pid;
|
|
138
|
+
const apps = unwrap(await client.call('list_apps'));
|
|
139
|
+
const list = apps.apps || [];
|
|
140
|
+
const picked = pickTarget(list, opts);
|
|
141
|
+
if (!picked.ok) {
|
|
142
|
+
console.error(picked.error);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
return picked.app.pid;
|
|
146
|
+
}
|
|
147
|
+
// --raise flag: app-level focus_window before the main action so coordinate
|
|
148
|
+
// clicks and keystrokes land on a visible, key window.
|
|
149
|
+
async function raiseIfRequested(client, pid, raise) {
|
|
150
|
+
if (raise)
|
|
151
|
+
unwrap(await client.call('focus_window', { pid }));
|
|
152
|
+
}
|
|
153
|
+
function emit(result, json, human) {
|
|
154
|
+
if (json) {
|
|
155
|
+
console.log(JSON.stringify(result, null, 2));
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
console.log(human());
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Add the shared --pid/--bundle target options to a verb.
|
|
162
|
+
function addTargetOpts(cmd) {
|
|
163
|
+
return cmd
|
|
164
|
+
.option('--bundle <id>', 'Bundle id of the target app (default: frontmost allow-listed app)')
|
|
165
|
+
.option('--pid <n>', 'Target pid directly (overrides --bundle)', (v) => parseInt(v, 10));
|
|
166
|
+
}
|
|
167
|
+
// Add the shared --id/--x/--y element-or-coords options to a verb.
|
|
168
|
+
function addElementOrCoordOpts(cmd) {
|
|
169
|
+
return cmd
|
|
170
|
+
.option('--id <@eN>', 'Element id from `describe`')
|
|
171
|
+
.option('--x <n>', 'X coordinate (global, points)', (v) => parseInt(v, 10))
|
|
172
|
+
.option('--y <n>', 'Y coordinate (global, points)', (v) => parseInt(v, 10));
|
|
173
|
+
}
|
|
174
|
+
export function registerActionCommands(program) {
|
|
175
|
+
// apps — list_apps
|
|
176
|
+
addTargetOpts(program
|
|
177
|
+
.command('apps')
|
|
178
|
+
.description('List apps the daemon may drive (allow-listed + running)')
|
|
179
|
+
.option('--json', 'Emit JSON')).action(async (opts) => {
|
|
180
|
+
await withClient(async (client) => {
|
|
181
|
+
const res = unwrap(await client.call('list_apps'));
|
|
182
|
+
const list = res.apps || [];
|
|
183
|
+
emit(res, Boolean(opts.json), () => list.length === 0
|
|
184
|
+
? '(no allow-listed apps running)'
|
|
185
|
+
: list
|
|
186
|
+
.map((a) => `${a.active ? '*' : ' '} ${String(a.pid).padStart(6)} ${a.bundle_id} ${a.name}`)
|
|
187
|
+
.join('\n'));
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
// describe — AX tree
|
|
191
|
+
addTargetOpts(program
|
|
192
|
+
.command('describe')
|
|
193
|
+
.description('Dump the accessibility tree (element ids feed click/type --id)')
|
|
194
|
+
.option('--depth <n>', 'Max tree depth', (v) => parseInt(v, 10))
|
|
195
|
+
.option('--json', 'Emit compact JSON (default: pretty)')).action(async (opts) => {
|
|
196
|
+
await withClient(async (client) => {
|
|
197
|
+
const pid = await resolveTargetPid(client, opts);
|
|
198
|
+
const params = { pid };
|
|
199
|
+
if (opts.depth != null)
|
|
200
|
+
params.max_depth = opts.depth;
|
|
201
|
+
const res = unwrap(await client.call('describe', params));
|
|
202
|
+
// The tree is inherently structured — always JSON, pretty unless --json.
|
|
203
|
+
console.log(JSON.stringify(opts.json ? res : res.tree ?? res, null, 2));
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
// click
|
|
207
|
+
addElementOrCoordOpts(addTargetOpts(program
|
|
208
|
+
.command('click')
|
|
209
|
+
.description('Click an element (--id) or screen coordinate (--x --y)')
|
|
210
|
+
.option('--count <n>', 'Click count (2 = double-click)', (v) => parseInt(v, 10))
|
|
211
|
+
.option('--background', 'Focus-safe postToPid delivery (plain AppKit only; skips HID tap)')
|
|
212
|
+
.option('--raise', 'Bring the target app to the front first')
|
|
213
|
+
.option('--json', 'Emit JSON'))).action(async (opts) => {
|
|
214
|
+
await withClient(async (client) => {
|
|
215
|
+
const pid = await resolveTargetPid(client, opts);
|
|
216
|
+
const spec = buildElementOrCoords(opts);
|
|
217
|
+
if (!spec.ok) {
|
|
218
|
+
console.error(spec.error);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
await raiseIfRequested(client, pid, opts.raise);
|
|
222
|
+
const params = { pid, ...spec.params };
|
|
223
|
+
if (opts.count != null)
|
|
224
|
+
params.count = opts.count;
|
|
225
|
+
if (opts.background)
|
|
226
|
+
params.background = true;
|
|
227
|
+
const res = unwrap(await client.call('click', params));
|
|
228
|
+
emit(res, Boolean(opts.json), () => `clicked (${res.action ?? 'ok'})`);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
// right-click
|
|
232
|
+
addElementOrCoordOpts(addTargetOpts(program
|
|
233
|
+
.command('right-click')
|
|
234
|
+
.description('Right-click (context menu) an element or coordinate')
|
|
235
|
+
.option('--json', 'Emit JSON'))).action(async (opts) => {
|
|
236
|
+
await withClient(async (client) => {
|
|
237
|
+
const pid = await resolveTargetPid(client, opts);
|
|
238
|
+
const spec = buildElementOrCoords(opts);
|
|
239
|
+
if (!spec.ok) {
|
|
240
|
+
console.error(spec.error);
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
const res = unwrap(await client.call('right_click', { pid, ...spec.params }));
|
|
244
|
+
emit(res, Boolean(opts.json), () => `right-clicked (${res.method ?? 'ok'})`);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
// type — set value on a field (--id) or paste at coords, optional commit
|
|
248
|
+
addElementOrCoordOpts(addTargetOpts(program
|
|
249
|
+
.command('type')
|
|
250
|
+
.description('Set a field value (--id) or paste at a coordinate (--x --y)')
|
|
251
|
+
.requiredOption('--text <s>', 'Text to enter')
|
|
252
|
+
.option('--commit', 'Commit after typing (AXConfirm / Return) so the value reaches the model')
|
|
253
|
+
.option('--allow-secure-field', 'Permit typing into a password field')
|
|
254
|
+
.option('--json', 'Emit JSON'))).action(async (opts) => {
|
|
255
|
+
await withClient(async (client) => {
|
|
256
|
+
const pid = await resolveTargetPid(client, opts);
|
|
257
|
+
const spec = buildElementOrCoords(opts);
|
|
258
|
+
if (!spec.ok) {
|
|
259
|
+
console.error(spec.error);
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
const params = { pid, ...spec.params, text: opts.text };
|
|
263
|
+
if (opts.commit)
|
|
264
|
+
params.commit = true;
|
|
265
|
+
if (opts.allowSecureField)
|
|
266
|
+
params.allow_secure_field = true;
|
|
267
|
+
const res = unwrap(await client.call('type', params));
|
|
268
|
+
emit(res, Boolean(opts.json), () => `typed ${opts.text.length} char(s)${res.committed ? ' (committed)' : ''}`);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
// type-text — stream an arbitrary unicode string into the focused field
|
|
272
|
+
addTargetOpts(program
|
|
273
|
+
.command('type-text')
|
|
274
|
+
.description('Type an arbitrary unicode string into the focused field (focus first via click/focus)')
|
|
275
|
+
.requiredOption('--text <s>', 'Text to type')
|
|
276
|
+
.option('--commit', 'Press Return after typing')
|
|
277
|
+
.option('--raise', 'Bring the target app to the front first')
|
|
278
|
+
.option('--require-frontmost', 'Fail (not warn) if the target is not the frontmost app')
|
|
279
|
+
.option('--json', 'Emit JSON')).action(async (opts) => {
|
|
280
|
+
await withClient(async (client) => {
|
|
281
|
+
const pid = await resolveTargetPid(client, opts);
|
|
282
|
+
await raiseIfRequested(client, pid, opts.raise);
|
|
283
|
+
const params = { pid, text: opts.text };
|
|
284
|
+
if (opts.commit)
|
|
285
|
+
params.commit = true;
|
|
286
|
+
if (opts.requireFrontmost)
|
|
287
|
+
params.require_frontmost = true;
|
|
288
|
+
const res = unwrap(await client.call('type_text', params));
|
|
289
|
+
warnIfNotFrontmost(res);
|
|
290
|
+
emit(res, Boolean(opts.json), () => `typed ${res.chars ?? opts.text.length} char(s)`);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
// key — single chord
|
|
294
|
+
addTargetOpts(program
|
|
295
|
+
.command('key')
|
|
296
|
+
.description('Send a key chord, e.g. "cmd+shift+s", "enter", "esc"')
|
|
297
|
+
.requiredOption('--keys <chord>', 'Key chord')
|
|
298
|
+
.option('--raise', 'Bring the target app to the front first')
|
|
299
|
+
.option('--require-frontmost', 'Fail (not warn) if the target is not the frontmost app')
|
|
300
|
+
.option('--json', 'Emit JSON')).action(async (opts) => {
|
|
301
|
+
await withClient(async (client) => {
|
|
302
|
+
const pid = await resolveTargetPid(client, opts);
|
|
303
|
+
await raiseIfRequested(client, pid, opts.raise);
|
|
304
|
+
const params = { pid, keys: opts.keys };
|
|
305
|
+
if (opts.requireFrontmost)
|
|
306
|
+
params.require_frontmost = true;
|
|
307
|
+
const res = unwrap(await client.call('key', params));
|
|
308
|
+
warnIfNotFrontmost(res);
|
|
309
|
+
emit(res, Boolean(opts.json), () => `sent ${opts.keys}`);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
// drag — from one point to another
|
|
313
|
+
addTargetOpts(program
|
|
314
|
+
.command('drag')
|
|
315
|
+
.description('Drag from one coordinate to another')
|
|
316
|
+
.requiredOption('--from <x,y>', 'Start coordinate "x,y"')
|
|
317
|
+
.requiredOption('--to <x,y>', 'End coordinate "x,y"')
|
|
318
|
+
.option('--button <left|right>', 'Mouse button', 'left')
|
|
319
|
+
.option('--background', 'Focus-safe postToPid delivery (plain AppKit only)')
|
|
320
|
+
.option('--raise', 'Bring the target app to the front first')
|
|
321
|
+
.option('--json', 'Emit JSON')).action(async (opts) => {
|
|
322
|
+
let from;
|
|
323
|
+
let to;
|
|
324
|
+
try {
|
|
325
|
+
from = parseXY(opts.from, '--from');
|
|
326
|
+
to = parseXY(opts.to, '--to');
|
|
327
|
+
}
|
|
328
|
+
catch (err) {
|
|
329
|
+
console.error(err.message);
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
await withClient(async (client) => {
|
|
333
|
+
const pid = await resolveTargetPid(client, opts);
|
|
334
|
+
await raiseIfRequested(client, pid, opts.raise);
|
|
335
|
+
const params = {
|
|
336
|
+
pid,
|
|
337
|
+
from: [from.x, from.y],
|
|
338
|
+
to: [to.x, to.y],
|
|
339
|
+
button: opts.button,
|
|
340
|
+
};
|
|
341
|
+
if (opts.background)
|
|
342
|
+
params.background = true;
|
|
343
|
+
const res = unwrap(await client.call('drag', params));
|
|
344
|
+
emit(res, Boolean(opts.json), () => `dragged ${opts.from} -> ${opts.to} (${res.method ?? 'ok'})`);
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
// scroll — by delta at an element or coordinate
|
|
348
|
+
addElementOrCoordOpts(addTargetOpts(program
|
|
349
|
+
.command('scroll')
|
|
350
|
+
.description('Scroll by a pixel delta at an element or coordinate')
|
|
351
|
+
.option('--dy <n>', 'Vertical delta (negative = down)', (v) => parseInt(v, 10))
|
|
352
|
+
.option('--dx <n>', 'Horizontal delta', (v) => parseInt(v, 10))
|
|
353
|
+
.option('--raise', 'Bring the target app to the front first')
|
|
354
|
+
.option('--json', 'Emit JSON'))).action(async (opts) => {
|
|
355
|
+
await withClient(async (client) => {
|
|
356
|
+
const pid = await resolveTargetPid(client, opts);
|
|
357
|
+
await raiseIfRequested(client, pid, opts.raise);
|
|
358
|
+
const params = { pid };
|
|
359
|
+
if (opts.id)
|
|
360
|
+
params.element_id = opts.id;
|
|
361
|
+
if (opts.x != null)
|
|
362
|
+
params.x = opts.x;
|
|
363
|
+
if (opts.y != null)
|
|
364
|
+
params.y = opts.y;
|
|
365
|
+
if (opts.dy != null)
|
|
366
|
+
params.dy = opts.dy;
|
|
367
|
+
if (opts.dx != null)
|
|
368
|
+
params.dx = opts.dx;
|
|
369
|
+
const res = unwrap(await client.call('scroll', params));
|
|
370
|
+
emit(res, Boolean(opts.json), () => `scrolled (${res.method ?? 'ok'})`);
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
// ax-action — perform any advertised AX action on an element
|
|
374
|
+
addTargetOpts(program
|
|
375
|
+
.command('ax-action')
|
|
376
|
+
.description('Perform an arbitrary AX action (AXConfirm, AXCancel, AXRaise, ...) on an element')
|
|
377
|
+
.requiredOption('--id <@eN>', 'Element id from `describe`')
|
|
378
|
+
.requiredOption('--action <name>', 'AX action name')
|
|
379
|
+
.option('--json', 'Emit JSON')).action(async (opts) => {
|
|
380
|
+
await withClient(async (client) => {
|
|
381
|
+
const pid = await resolveTargetPid(client, opts);
|
|
382
|
+
const res = unwrap(await client.call('ax_action', { pid, element_id: opts.id, action: opts.action }));
|
|
383
|
+
emit(res, Boolean(opts.json), () => `performed ${opts.action}`);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
// focus — set keyboard focus to an element
|
|
387
|
+
addTargetOpts(program
|
|
388
|
+
.command('focus')
|
|
389
|
+
.description('Set keyboard focus to an element (so type-text/key land there)')
|
|
390
|
+
.requiredOption('--id <@eN>', 'Element id from `describe`')
|
|
391
|
+
.option('--json', 'Emit JSON')).action(async (opts) => {
|
|
392
|
+
await withClient(async (client) => {
|
|
393
|
+
const pid = await resolveTargetPid(client, opts);
|
|
394
|
+
const res = unwrap(await client.call('set_focus', { pid, element_id: opts.id }));
|
|
395
|
+
emit(res, Boolean(opts.json), () => `focused ${opts.id}`);
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
// raise — bring an app (or one of its windows) to the front. The window
|
|
399
|
+
// forms (--window-id/--title) also switch macOS Spaces, which is the only
|
|
400
|
+
// way to reach a fullscreen-Space window (VM, fullscreen editor) for
|
|
401
|
+
// capture and HID-tap input.
|
|
402
|
+
addTargetOpts(program
|
|
403
|
+
.command('raise')
|
|
404
|
+
.description('Bring an app (or a specific window) to the front — switches Spaces for fullscreen windows')
|
|
405
|
+
.option('--window-id <n>', 'Raise a specific window by id (from `screenshot --list`)', (v) => parseInt(v, 10))
|
|
406
|
+
.option('--title <s>', 'Raise the window whose title contains this string')
|
|
407
|
+
.option('--json', 'Emit JSON')).action(async (opts) => {
|
|
408
|
+
await withClient(async (client) => {
|
|
409
|
+
const pid = await resolveTargetPid(client, opts);
|
|
410
|
+
const res = unwrap(await client.call('focus_window', { pid, ...buildRaiseParams(opts) }));
|
|
411
|
+
emit(res, Boolean(opts.json), () => {
|
|
412
|
+
const scope = res.raised_window ? `window ${res.title ?? res.window_id ?? ''}`.trim() : 'app';
|
|
413
|
+
return `raised ${scope} (${res.focus_elapsed_ms ?? 0}ms)`;
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
// wait — settle the UI before the next action
|
|
418
|
+
addTargetOpts(program
|
|
419
|
+
.command('wait')
|
|
420
|
+
.description('Wait for a duration (--duration) or for an element (--id / --role/--label) to satisfy --until')
|
|
421
|
+
.option('--duration <ms>', 'Unconditional sleep in ms (50-30000)', (v) => parseInt(v, 10))
|
|
422
|
+
.option('--id <@eN>', 'Element id from `describe` to poll')
|
|
423
|
+
.option('--until <cond>', 'Condition: exists | enabled | disappears (default: exists)')
|
|
424
|
+
.option('--role <s>', 'Locator: AX role (e.g. AXButton)')
|
|
425
|
+
.option('--label <s>', 'Locator: element label')
|
|
426
|
+
.option('--identifier <s>', 'Locator: AX identifier')
|
|
427
|
+
.option('--timeout <ms>', 'Poll timeout in ms (default 5000)', (v) => parseInt(v, 10))
|
|
428
|
+
.option('--json', 'Emit JSON')).action(async (opts) => {
|
|
429
|
+
const spec = buildWaitParams(opts);
|
|
430
|
+
if (!spec.ok) {
|
|
431
|
+
console.error(spec.error);
|
|
432
|
+
process.exit(1);
|
|
433
|
+
}
|
|
434
|
+
await withClient(async (client) => {
|
|
435
|
+
const params = { ...spec.params };
|
|
436
|
+
// duration-only waits don't need a target pid
|
|
437
|
+
if (params.duration_ms == null)
|
|
438
|
+
params.pid = await resolveTargetPid(client, opts);
|
|
439
|
+
const res = unwrap(await client.call('wait', params));
|
|
440
|
+
emit(res, Boolean(opts.json), () => res.satisfied ? `satisfied (${res.waited_ms}ms)` : `timed out (${res.waited_ms}ms)`);
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
// get-text — read text without OCR
|
|
444
|
+
addTargetOpts(program
|
|
445
|
+
.command('get-text')
|
|
446
|
+
.description('Extract visible text from the app (or a subtree via --id)')
|
|
447
|
+
.option('--id <@eN>', 'Element id from `describe` to scope the extraction')
|
|
448
|
+
.option('--max-chars <n>', 'Cap the extracted text length', (v) => parseInt(v, 10))
|
|
449
|
+
.option('--json', 'Emit JSON')).action(async (opts) => {
|
|
450
|
+
await withClient(async (client) => {
|
|
451
|
+
const pid = await resolveTargetPid(client, opts);
|
|
452
|
+
const params = { pid };
|
|
453
|
+
if (opts.id)
|
|
454
|
+
params.element_id = opts.id;
|
|
455
|
+
if (opts.maxChars != null)
|
|
456
|
+
params.max_chars = opts.maxChars;
|
|
457
|
+
const res = unwrap(await client.call('get_text', params));
|
|
458
|
+
emit(res, Boolean(opts.json), () => String(res.text ?? ''));
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
// launch — start an app (no target resolution: it isn't running yet)
|
|
462
|
+
program
|
|
463
|
+
.command('launch')
|
|
464
|
+
.description('Launch an app by bundle id, path, or name')
|
|
465
|
+
.option('--bundle <id>', 'Bundle id (e.g. com.apple.TextEdit)')
|
|
466
|
+
.option('--path <p>', 'Path to the .app bundle')
|
|
467
|
+
.option('--name <s>', 'App name (resolved via /Applications and LaunchServices)')
|
|
468
|
+
.option('--json', 'Emit JSON')
|
|
469
|
+
.action(async (opts) => {
|
|
470
|
+
if (!opts.bundle && !opts.path && !opts.name) {
|
|
471
|
+
console.error('pass one of --bundle, --path, --name');
|
|
472
|
+
process.exit(1);
|
|
473
|
+
}
|
|
474
|
+
await withClient(async (client) => {
|
|
475
|
+
const params = {};
|
|
476
|
+
if (opts.bundle)
|
|
477
|
+
params.bundle_id = opts.bundle;
|
|
478
|
+
if (opts.path)
|
|
479
|
+
params.path = opts.path;
|
|
480
|
+
if (opts.name)
|
|
481
|
+
params.name = opts.name;
|
|
482
|
+
const res = unwrap(await client.call('launch_app', params));
|
|
483
|
+
emit(res, Boolean(opts.json), () => `launched ${res.name} (pid ${res.pid})`);
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
}
|