@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.
- package/CHANGELOG.md +13 -0
- package/README.md +1 -1
- package/dist/commands/browser.js +31 -4
- package/dist/commands/computer-actions.d.ts +36 -0
- package/dist/commands/computer-actions.js +328 -0
- package/dist/commands/computer.js +74 -55
- package/dist/commands/defaults.d.ts +7 -0
- package/dist/commands/defaults.js +89 -0
- package/dist/commands/exec.js +24 -6
- package/dist/commands/inspect.d.ts +38 -7
- package/dist/commands/inspect.js +194 -24
- package/dist/commands/rules.js +3 -3
- package/dist/commands/secrets.js +46 -9
- package/dist/commands/sessions.js +9 -12
- package/dist/commands/setup.js +2 -2
- package/dist/commands/teams.js +108 -11
- package/dist/commands/view.d.ts +12 -1
- package/dist/commands/view.js +121 -38
- package/dist/index.js +61 -22
- package/dist/lib/agents.d.ts +10 -6
- package/dist/lib/agents.js +23 -14
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +84 -3
- package/dist/lib/daemon.js +4 -7
- package/dist/lib/exec.d.ts +9 -0
- package/dist/lib/exec.js +85 -9
- package/dist/lib/migrate.js +6 -4
- package/dist/lib/permissions.d.ts +23 -0
- package/dist/lib/permissions.js +89 -7
- 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/plugin-marketplace.js +1 -1
- package/dist/lib/project-launch.d.ts +5 -0
- package/dist/lib/project-launch.js +37 -0
- package/dist/lib/pty-client.js +13 -5
- package/dist/lib/pty-server.d.ts +24 -1
- package/dist/lib/pty-server.js +109 -29
- package/dist/lib/resources/rules.js +1 -1
- package/dist/lib/resources/skills.js +1 -1
- package/dist/lib/resources.d.ts +2 -0
- package/dist/lib/resources.js +2 -1
- package/dist/lib/rotate.js +6 -18
- package/dist/lib/run-config.d.ts +9 -0
- package/dist/lib/run-config.js +35 -0
- package/dist/lib/run-defaults.d.ts +42 -0
- package/dist/lib/run-defaults.js +180 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/install-helper.d.ts +11 -3
- package/dist/lib/secrets/install-helper.js +48 -6
- package/dist/lib/secrets/linux.d.ts +12 -0
- package/dist/lib/secrets/linux.js +30 -16
- package/dist/lib/session/artifacts.js +8 -2
- package/dist/lib/shims.d.ts +9 -1
- package/dist/lib/shims.js +80 -3
- package/dist/lib/staleness/detectors/hooks.js +1 -1
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/teams/agents.js +5 -7
- package/dist/lib/teams/api.d.ts +67 -0
- package/dist/lib/teams/api.js +78 -0
- package/dist/lib/types.d.ts +15 -6
- package/dist/lib/versions.js +4 -4
- package/package.json +5 -2
- 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
|
|
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
|
|
package/dist/commands/browser.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|
|
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: ['
|
|
10
|
+
{ title: 'Installation', names: ['setup'] },
|
|
10
11
|
{ title: 'Daemon lifecycle', names: ['start', 'stop', 'reload', 'status'] },
|
|
11
|
-
{ title: '
|
|
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
|
-
|
|
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
|
|
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
|
|
88
|
-
.option('--bundle <id>', 'Bundle id to capture (default:
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
//
|
|
123
|
-
const
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
const
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
180
|
+
function registerSetupCommand(program) {
|
|
163
181
|
program
|
|
164
|
-
.command('
|
|
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
|
|
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
|
|
282
|
+
console.error('run: agents computer setup');
|
|
264
283
|
process.exit(1);
|
|
265
284
|
}
|
|
266
285
|
const uid = process.getuid?.();
|