@phnx-labs/agents-cli 1.18.5 → 1.19.0
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 -2
- package/README.md +22 -20
- package/dist/commands/browser.js +25 -2
- package/dist/commands/cloud.js +3 -3
- package/dist/commands/computer.d.ts +6 -0
- package/dist/commands/computer.js +477 -0
- package/dist/commands/doctor.js +19 -17
- package/dist/commands/exec.js +37 -59
- package/dist/commands/factory.js +12 -5
- package/dist/commands/import.js +6 -1
- package/dist/commands/mcp.js +9 -4
- package/dist/commands/packages.d.ts +3 -0
- package/dist/commands/packages.js +20 -12
- package/dist/commands/permissions.d.ts +2 -0
- package/dist/commands/permissions.js +20 -1
- package/dist/commands/plugins.d.ts +2 -0
- package/dist/commands/plugins.js +23 -4
- package/dist/commands/profiles.js +1 -1
- package/dist/commands/pty.js +126 -112
- package/dist/commands/pull.js +29 -25
- package/dist/commands/repo.js +24 -26
- package/dist/commands/routines.js +29 -26
- package/dist/commands/secrets.js +66 -73
- package/dist/commands/sessions-tail.js +21 -22
- package/dist/commands/sessions.js +36 -68
- package/dist/commands/setup.js +20 -24
- package/dist/commands/teams.js +30 -39
- package/dist/commands/versions.js +60 -68
- package/dist/commands/worktree.d.ts +20 -0
- package/dist/commands/worktree.js +242 -0
- package/dist/computer.d.ts +2 -0
- package/dist/computer.js +7 -0
- package/dist/index.js +70 -26
- package/dist/lib/agents.d.ts +4 -1
- package/dist/lib/agents.js +23 -5
- package/dist/lib/browser/cdp.d.ts +15 -1
- package/dist/lib/browser/cdp.js +77 -8
- package/dist/lib/browser/chrome.js +17 -24
- package/dist/lib/browser/drivers/ssh.d.ts +1 -0
- package/dist/lib/browser/drivers/ssh.js +20 -8
- package/dist/lib/browser/ipc.js +38 -5
- package/dist/lib/browser/profiles.js +34 -2
- package/dist/lib/browser/runtime-state.d.ts +1 -2
- package/dist/lib/browser/runtime-state.js +11 -3
- package/dist/lib/browser/service.d.ts +5 -0
- package/dist/lib/browser/service.js +32 -4
- package/dist/lib/browser/types.d.ts +1 -1
- package/dist/lib/browser/upload.d.ts +2 -0
- package/dist/lib/browser/upload.js +34 -0
- package/dist/lib/cloud/rush.d.ts +2 -1
- package/dist/lib/cloud/rush.js +28 -9
- package/dist/lib/computer-rpc.d.ts +24 -0
- package/dist/lib/computer-rpc.js +263 -0
- package/dist/lib/daemon.js +7 -7
- package/dist/lib/exec.d.ts +2 -1
- package/dist/lib/exec.js +3 -2
- package/dist/lib/fs-atomic.d.ts +18 -0
- package/dist/lib/fs-atomic.js +76 -0
- package/dist/lib/git.js +2 -4
- package/dist/lib/help.d.ts +15 -0
- package/dist/lib/help.js +41 -0
- package/dist/lib/hooks/match.d.ts +1 -0
- package/dist/lib/hooks/match.js +57 -12
- package/dist/lib/hooks.d.ts +1 -0
- package/dist/lib/hooks.js +27 -10
- package/dist/lib/import.d.ts +1 -0
- package/dist/lib/import.js +7 -0
- package/dist/lib/manifest.js +27 -1
- package/dist/lib/mcp.d.ts +14 -0
- package/dist/lib/mcp.js +79 -14
- package/dist/lib/migrate.js +3 -3
- package/dist/lib/models.js +3 -1
- package/dist/lib/permissions.d.ts +5 -0
- package/dist/lib/permissions.js +35 -0
- package/dist/lib/plugin-marketplace.d.ts +3 -1
- package/dist/lib/plugin-marketplace.js +36 -1
- package/dist/lib/plugins.d.ts +19 -1
- package/dist/lib/plugins.js +99 -8
- package/dist/lib/redact.d.ts +4 -0
- package/dist/lib/redact.js +18 -0
- package/dist/lib/registry.d.ts +2 -0
- package/dist/lib/registry.js +15 -0
- package/dist/lib/sandbox.js +15 -5
- package/dist/lib/secrets/bundles.d.ts +7 -12
- package/dist/lib/secrets/bundles.js +45 -29
- package/dist/lib/secrets/index.js +4 -4
- package/dist/lib/session/cloud.d.ts +2 -0
- package/dist/lib/session/cloud.js +34 -6
- package/dist/lib/session/parse.js +7 -2
- package/dist/lib/session/render.d.ts +4 -1
- package/dist/lib/session/render.js +81 -35
- package/dist/lib/shims.d.ts +5 -2
- package/dist/lib/shims.js +29 -7
- package/dist/lib/state.d.ts +5 -5
- package/dist/lib/state.js +43 -13
- package/dist/lib/teams/agents.d.ts +1 -1
- package/dist/lib/teams/agents.js +2 -2
- package/dist/lib/types.d.ts +4 -3
- package/dist/lib/types.js +0 -2
- package/dist/lib/versions.js +65 -40
- package/dist/lib/workflows.d.ts +7 -0
- package/dist/lib/workflows.js +42 -1
- package/npm-shrinkwrap.json +3256 -0
- package/package.json +32 -26
- package/scripts/postinstall.js +8 -2
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { registerCommandGroups } from '../lib/help.js';
|
|
6
|
+
import { openComputerClient, resolveHelperApp, resolveHelperExec, resolveSocketPath, resolveLogPath, resolvePolicyPath, describeTransport, loadComputerAllowList, writeComputerPolicy, } from '../lib/computer-rpc.js';
|
|
7
|
+
// Help groups — mirror `agents browser` so the mental model carries over.
|
|
8
|
+
const COMPUTER_HELP_GROUPS = [
|
|
9
|
+
{ title: 'Installation', names: ['install-helper'] },
|
|
10
|
+
{ title: 'Daemon lifecycle', names: ['start', 'stop', 'reload', 'status'] },
|
|
11
|
+
{ title: 'Capture evidence', names: ['screenshot'] },
|
|
12
|
+
];
|
|
13
|
+
export function registerComputerCommand(program) {
|
|
14
|
+
const computer = program
|
|
15
|
+
.command('computer')
|
|
16
|
+
.description('Drive macOS apps via Accessibility — list, screenshot, click, type');
|
|
17
|
+
registerComputerSubcommands(computer);
|
|
18
|
+
registerCommandGroups(computer, COMPUTER_HELP_GROUPS);
|
|
19
|
+
}
|
|
20
|
+
export function registerComputerSubcommands(program) {
|
|
21
|
+
registerInstallHelperCommand(program);
|
|
22
|
+
registerStartCommand(program);
|
|
23
|
+
registerStopCommand(program);
|
|
24
|
+
registerReloadCommand(program);
|
|
25
|
+
registerStatusCommand(program);
|
|
26
|
+
registerScreenshotCommand(program);
|
|
27
|
+
registerCommandGroups(program, COMPUTER_HELP_GROUPS);
|
|
28
|
+
}
|
|
29
|
+
function reportMissingHelper() {
|
|
30
|
+
console.error('helper not built. Run: ./packages/computer-helper/scripts/build.sh debug');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
function registerStatusCommand(program) {
|
|
34
|
+
program
|
|
35
|
+
.command('status')
|
|
36
|
+
.description('Report install state, daemon state, and Accessibility trust')
|
|
37
|
+
.action(async () => {
|
|
38
|
+
const socketPath = resolveSocketPath();
|
|
39
|
+
const installed = fs.existsSync(HELPER_APP_DEST);
|
|
40
|
+
const socketUp = fs.existsSync(socketPath);
|
|
41
|
+
console.log(`installed: ${installed ? 'yes' : 'no'} (${HELPER_APP_DEST})`);
|
|
42
|
+
console.log(`daemon: ${socketUp ? 'running' : 'stopped'}`);
|
|
43
|
+
// Show the current allow list — what the user has actually authorized
|
|
44
|
+
// via Computer(...) patterns in their permission groups.
|
|
45
|
+
const allowed = loadComputerAllowList();
|
|
46
|
+
const previewParts = allowed.slice(0, 5);
|
|
47
|
+
const previewSuffix = allowed.length > 5 ? ` (+${allowed.length - 5} more)` : '';
|
|
48
|
+
console.log(`policy: ${allowed.length} app${allowed.length === 1 ? '' : 's'} allowed${allowed.length > 0 ? `: ${previewParts.join(', ')}${previewSuffix}` : ''}`);
|
|
49
|
+
if (!installed) {
|
|
50
|
+
console.log('');
|
|
51
|
+
console.log('Run: agents computer install-helper');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (!socketUp) {
|
|
55
|
+
console.log('');
|
|
56
|
+
console.log('Run: agents computer start');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Daemon is up — probe trust state.
|
|
60
|
+
const client = openComputerClient();
|
|
61
|
+
try {
|
|
62
|
+
const r = await client.call('trust_status');
|
|
63
|
+
if (r.error) {
|
|
64
|
+
console.error(`error: ${r.error.code}: ${r.error.message}`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
const trusted = Boolean(r.result?.trusted);
|
|
68
|
+
const helperPid = r.result?.pid;
|
|
69
|
+
console.log(`trust: ${trusted ? 'granted' : 'denied'}`);
|
|
70
|
+
if (typeof helperPid === 'number')
|
|
71
|
+
console.log(`pid: ${helperPid}`);
|
|
72
|
+
if (!trusted) {
|
|
73
|
+
console.log('');
|
|
74
|
+
console.log('Grant Accessibility + Screen Recording in System Settings, then `agents computer start` again.');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
await client.close();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function registerScreenshotCommand(program) {
|
|
83
|
+
program
|
|
84
|
+
.command('screenshot')
|
|
85
|
+
.description('Capture a JPEG of the frontmost window of a bundle id (default: frontmost app)')
|
|
86
|
+
.option('--bundle <id>', 'Bundle id to capture (default: bundle id of frontmost app)')
|
|
87
|
+
.option('--out <path>', 'Output JPEG path', './computer-screenshot.jpg')
|
|
88
|
+
.option('--quality <n>', 'JPEG quality 1-100', (v) => parseInt(v, 10), 85)
|
|
89
|
+
.action(async (opts) => {
|
|
90
|
+
const transport = describeTransport();
|
|
91
|
+
if (transport.kind === 'none')
|
|
92
|
+
reportMissingHelper();
|
|
93
|
+
const quality = Math.max(1, Math.min(100, opts.quality || 85));
|
|
94
|
+
const client = openComputerClient();
|
|
95
|
+
try {
|
|
96
|
+
// Step 1: list_apps to get the candidate set.
|
|
97
|
+
const apps = await client.call('list_apps');
|
|
98
|
+
if (apps.error) {
|
|
99
|
+
console.error(`error: ${apps.error.code}: ${apps.error.message}`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
const list = apps.result?.apps || [];
|
|
103
|
+
let target;
|
|
104
|
+
if (opts.bundle) {
|
|
105
|
+
target = list.find((a) => a.bundle_id === opts.bundle);
|
|
106
|
+
if (!target) {
|
|
107
|
+
console.error(`bundle not in allow list (or not running): ${opts.bundle}`);
|
|
108
|
+
console.error(`add Computer(${opts.bundle}) to a permissions group, then \`agents computer reload\``);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
target = list.find((a) => a.active);
|
|
114
|
+
if (!target) {
|
|
115
|
+
console.error('no active app found in allow list');
|
|
116
|
+
console.error('add Computer(<bundle-id>) to a permissions group, then `agents computer reload`');
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Step 2: screenshot.
|
|
121
|
+
const shot = await client.call('screenshot', { pid: target.pid, quality });
|
|
122
|
+
if (shot.error) {
|
|
123
|
+
console.error(`error: ${shot.error.code}: ${shot.error.message}`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
const b64 = shot.result?.image_data;
|
|
127
|
+
const width = shot.result?.width;
|
|
128
|
+
const height = shot.result?.height;
|
|
129
|
+
if (!b64) {
|
|
130
|
+
console.error('helper returned no image_data');
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
const buf = Buffer.from(b64, 'base64');
|
|
134
|
+
const outPath = path.resolve(opts.out);
|
|
135
|
+
fs.writeFileSync(outPath, buf);
|
|
136
|
+
console.log(`saved: ${outPath} (${width ?? '?'}x${height ?? '?'}, ${buf.byteLength} bytes)`);
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
await client.close();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
// install-helper:
|
|
144
|
+
// 1. resolve dist .app
|
|
145
|
+
// 2. copy to /Applications/Computer Helper.app
|
|
146
|
+
// 3. codesign --verify the destination
|
|
147
|
+
// 4. write LaunchAgent plist with absolute HOME paths
|
|
148
|
+
// 5. launchctl bootout (ignore failure) -> bootstrap -> kickstart -k
|
|
149
|
+
// 6. wait for socket to appear
|
|
150
|
+
// 7. probe trust_status, print grant instructions if needed
|
|
151
|
+
//
|
|
152
|
+
// macOS TCC is keyed by signed-bundle identity + bundle id. Putting the
|
|
153
|
+
// .app at a stable absolute path under /Applications/ means the AX grant
|
|
154
|
+
// survives across npm updates. The CLI itself is unsigned but doesn't
|
|
155
|
+
// need AX — it sends JSON-RPC to the daemon, which has AX.
|
|
156
|
+
const HELPER_BUNDLE_ID = 'com.phnx-labs.computer-helper';
|
|
157
|
+
const HELPER_APP_NAME = 'Computer Helper.app';
|
|
158
|
+
const HELPER_APP_DEST = `/Applications/${HELPER_APP_NAME}`;
|
|
159
|
+
const HELPER_LABEL = HELPER_BUNDLE_ID;
|
|
160
|
+
function registerInstallHelperCommand(program) {
|
|
161
|
+
program
|
|
162
|
+
.command('install-helper')
|
|
163
|
+
.description('Install ComputerHelper.app to /Applications/ (does NOT activate the daemon — run `start` to enable)')
|
|
164
|
+
.action(async () => {
|
|
165
|
+
const srcApp = resolveHelperApp();
|
|
166
|
+
if (!srcApp || !fs.existsSync(srcApp)) {
|
|
167
|
+
console.error('helper not built. Run: ./packages/computer-helper/scripts/build.sh debug');
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
const home = os.homedir();
|
|
171
|
+
const socketPath = resolveSocketPath();
|
|
172
|
+
const logPath = resolveLogPath();
|
|
173
|
+
const plistPath = path.join(home, 'Library', 'LaunchAgents', `${HELPER_LABEL}.plist`);
|
|
174
|
+
console.log(`source: ${srcApp}`);
|
|
175
|
+
console.log(`dest: ${HELPER_APP_DEST}`);
|
|
176
|
+
// 1. Copy to /Applications/. Use ditto to preserve xattrs (Gatekeeper
|
|
177
|
+
// provenance + codesign metadata). Wipe any prior install first.
|
|
178
|
+
if (fs.existsSync(HELPER_APP_DEST)) {
|
|
179
|
+
try {
|
|
180
|
+
fs.rmSync(HELPER_APP_DEST, { recursive: true, force: true });
|
|
181
|
+
console.log(`removed prior install`);
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
console.error(`failed to remove prior install at ${HELPER_APP_DEST}: ${err.message}`);
|
|
185
|
+
console.error('try: sudo rm -rf "' + HELPER_APP_DEST + '"');
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
execFileSync('/usr/bin/ditto', [srcApp, HELPER_APP_DEST], { stdio: 'inherit' });
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
console.error(`ditto copy failed: ${err.message}`);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
console.log(`copied to ${HELPER_APP_DEST}`);
|
|
197
|
+
// 2. Verify codesign on the destination. Fail loud if the copy
|
|
198
|
+
// somehow stripped the signature — TCC needs a valid signature.
|
|
199
|
+
try {
|
|
200
|
+
execFileSync('/usr/bin/codesign', ['--verify', '--deep', '--strict', HELPER_APP_DEST], { stdio: 'inherit' });
|
|
201
|
+
console.log('codesign verify: OK');
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
console.error('codesign verify FAILED. The destination .app is unsigned or its signature was stripped.');
|
|
205
|
+
console.error('rebuild the helper with a Developer ID cert: ./packages/computer-helper/scripts/build.sh release');
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
// 3. Ensure socket + log parent dirs exist.
|
|
209
|
+
fs.mkdirSync(path.dirname(socketPath), { recursive: true });
|
|
210
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
211
|
+
// 4. Write the LaunchAgent plist but DO NOT bootstrap it. The user
|
|
212
|
+
// explicitly opts into running the daemon via `agents computer start`.
|
|
213
|
+
// Screen Recording + Accessibility are scary permissions; we don't
|
|
214
|
+
// want an always-on listener that can drive any app the user could.
|
|
215
|
+
const execInsideApp = path.join(HELPER_APP_DEST, 'Contents', 'MacOS', 'ComputerHelper');
|
|
216
|
+
const plistContent = renderLaunchAgentPlist({
|
|
217
|
+
label: HELPER_LABEL,
|
|
218
|
+
exec: execInsideApp,
|
|
219
|
+
socketPath,
|
|
220
|
+
logPath,
|
|
221
|
+
});
|
|
222
|
+
fs.mkdirSync(path.dirname(plistPath), { recursive: true });
|
|
223
|
+
fs.writeFileSync(plistPath, plistContent);
|
|
224
|
+
console.log(`wrote plist: ${plistPath} (NOT activated)`);
|
|
225
|
+
console.log('');
|
|
226
|
+
console.log('Helper installed (inactive).');
|
|
227
|
+
console.log('');
|
|
228
|
+
console.log(` app: ${HELPER_APP_DEST}`);
|
|
229
|
+
console.log(` plist: ${plistPath}`);
|
|
230
|
+
console.log('');
|
|
231
|
+
console.log('Next steps:');
|
|
232
|
+
console.log(' 1. Grant TCC permissions (one-time):');
|
|
233
|
+
console.log(' System Settings > Privacy & Security > Accessibility — add Computer Helper.app');
|
|
234
|
+
console.log(' System Settings > Privacy & Security > Screen Recording — add Computer Helper.app');
|
|
235
|
+
console.log(' 2. Whitelist the apps the daemon may drive. Add a YAML under ~/.agents/permissions/groups/:');
|
|
236
|
+
console.log(' name: computer');
|
|
237
|
+
console.log(' allow:');
|
|
238
|
+
console.log(' - "Computer(com.apple.mail)"');
|
|
239
|
+
console.log(' - "Computer(com.apple.notes)"');
|
|
240
|
+
console.log(' Default policy is deny-all.');
|
|
241
|
+
console.log(' 3. When you want to use it: agents computer start');
|
|
242
|
+
console.log(' 4. When you are done: agents computer stop');
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
function registerStartCommand(program) {
|
|
246
|
+
program
|
|
247
|
+
.command('start')
|
|
248
|
+
.description('Activate the helper daemon (loads launchd, opens socket)')
|
|
249
|
+
.action(async () => {
|
|
250
|
+
const home = os.homedir();
|
|
251
|
+
const plistPath = path.join(home, 'Library', 'LaunchAgents', `${HELPER_LABEL}.plist`);
|
|
252
|
+
const socketPath = resolveSocketPath();
|
|
253
|
+
const logPath = resolveLogPath();
|
|
254
|
+
if (!fs.existsSync(plistPath)) {
|
|
255
|
+
console.error(`plist not found at ${plistPath}`);
|
|
256
|
+
console.error('run: agents computer install-helper');
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
if (!fs.existsSync(HELPER_APP_DEST)) {
|
|
260
|
+
console.error(`helper app not found at ${HELPER_APP_DEST}`);
|
|
261
|
+
console.error('run: agents computer install-helper');
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
const uid = process.getuid?.();
|
|
265
|
+
if (typeof uid !== 'number') {
|
|
266
|
+
console.error('cannot resolve uid');
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
const domain = `gui/${uid}`;
|
|
270
|
+
// Render the policy file BEFORE launchctl bootstrap so the daemon
|
|
271
|
+
// reads a fresh allow list at startup. The helper falls back to an
|
|
272
|
+
// empty allow list (everything denied) if this file is missing or
|
|
273
|
+
// unparseable — fail-safe.
|
|
274
|
+
const allowed = loadComputerAllowList();
|
|
275
|
+
writeComputerPolicy(allowed);
|
|
276
|
+
console.log(`policy: ${allowed.length} app${allowed.length === 1 ? '' : 's'} allowed (${resolvePolicyPath()})`);
|
|
277
|
+
if (allowed.length > 0) {
|
|
278
|
+
const preview = allowed.slice(0, 5).join(', ');
|
|
279
|
+
const more = allowed.length > 5 ? ` (+${allowed.length - 5} more)` : '';
|
|
280
|
+
console.log(` ${preview}${more}`);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
console.log(` (no Computer(...) patterns found — everything will be denied)`);
|
|
284
|
+
console.log(` add to ~/.agents/permissions/groups/<name>.yaml under allow:`);
|
|
285
|
+
console.log(` - "Computer(com.apple.finder)"`);
|
|
286
|
+
}
|
|
287
|
+
// Bootout first to clear any prior registration. Best-effort.
|
|
288
|
+
try {
|
|
289
|
+
execFileSync('/bin/launchctl', ['bootout', domain, plistPath], { stdio: 'pipe' });
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
// expected when not previously loaded
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
execFileSync('/bin/launchctl', ['bootstrap', domain, plistPath], { stdio: 'pipe' });
|
|
296
|
+
}
|
|
297
|
+
catch (err) {
|
|
298
|
+
console.error(`launchctl bootstrap failed: ${err.message}`);
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
// Force restart so we pick up the latest binary.
|
|
302
|
+
try {
|
|
303
|
+
execFileSync('/bin/launchctl', ['kickstart', '-k', `${domain}/${HELPER_LABEL}`], { stdio: 'pipe' });
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
console.error(`launchctl kickstart failed: ${err.message}`);
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
// Wait up to 5s for the socket.
|
|
310
|
+
const deadline = Date.now() + 5000;
|
|
311
|
+
while (Date.now() < deadline) {
|
|
312
|
+
if (fs.existsSync(socketPath))
|
|
313
|
+
break;
|
|
314
|
+
await sleep(100);
|
|
315
|
+
}
|
|
316
|
+
if (!fs.existsSync(socketPath)) {
|
|
317
|
+
console.error(`socket did not appear at ${socketPath} within 5s`);
|
|
318
|
+
console.error(`check ${logPath} for helper startup errors`);
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
// Probe trust through the socket.
|
|
322
|
+
let trustStr = 'unknown';
|
|
323
|
+
try {
|
|
324
|
+
const client = openComputerClient();
|
|
325
|
+
try {
|
|
326
|
+
const r = await client.call('trust_status');
|
|
327
|
+
trustStr = r.error ? `error (${r.error.code})` : (r.result?.trusted ? 'granted' : 'denied');
|
|
328
|
+
}
|
|
329
|
+
finally {
|
|
330
|
+
await client.close();
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
trustStr = `error (${err.message})`;
|
|
335
|
+
}
|
|
336
|
+
console.log(`daemon: running`);
|
|
337
|
+
console.log(`socket: ${socketPath}`);
|
|
338
|
+
console.log(`trust: ${trustStr}`);
|
|
339
|
+
if (trustStr === 'denied') {
|
|
340
|
+
console.log('');
|
|
341
|
+
console.log('Grant Accessibility + Screen Recording to Computer Helper.app, then run `agents computer start` again.');
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
function registerReloadCommand(program) {
|
|
346
|
+
program
|
|
347
|
+
.command('reload')
|
|
348
|
+
.description('Reload the allow-list policy from ~/.agents/permissions/groups/ (SIGHUP the daemon)')
|
|
349
|
+
.action(async () => {
|
|
350
|
+
const socketPath = resolveSocketPath();
|
|
351
|
+
if (!fs.existsSync(socketPath)) {
|
|
352
|
+
console.error(`daemon not running (no socket at ${socketPath})`);
|
|
353
|
+
console.error('run: agents computer start');
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
const allowed = loadComputerAllowList();
|
|
357
|
+
writeComputerPolicy(allowed);
|
|
358
|
+
console.log(`policy: ${allowed.length} app${allowed.length === 1 ? '' : 's'} allowed (${resolvePolicyPath()})`);
|
|
359
|
+
// Resolve the daemon's pid via `launchctl list <label>`. The plist
|
|
360
|
+
// output includes a "PID" key when the service is running.
|
|
361
|
+
const uid = process.getuid?.();
|
|
362
|
+
if (typeof uid !== 'number') {
|
|
363
|
+
console.error('cannot resolve uid');
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
const domain = `gui/${uid}`;
|
|
367
|
+
let pid = null;
|
|
368
|
+
try {
|
|
369
|
+
const out = execFileSync('/bin/launchctl', ['print', `${domain}/${HELPER_LABEL}`], { encoding: 'utf-8' });
|
|
370
|
+
const m = out.match(/\bpid\s*=\s*(\d+)/);
|
|
371
|
+
if (m)
|
|
372
|
+
pid = parseInt(m[1], 10);
|
|
373
|
+
}
|
|
374
|
+
catch (err) {
|
|
375
|
+
console.error(`launchctl print failed: ${err.message}`);
|
|
376
|
+
process.exit(1);
|
|
377
|
+
}
|
|
378
|
+
if (pid === null || !Number.isFinite(pid) || pid <= 0) {
|
|
379
|
+
console.error('could not resolve daemon pid from launchctl print output');
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
try {
|
|
383
|
+
process.kill(pid, 'SIGHUP');
|
|
384
|
+
}
|
|
385
|
+
catch (err) {
|
|
386
|
+
console.error(`kill -HUP ${pid} failed: ${err.message}`);
|
|
387
|
+
process.exit(1);
|
|
388
|
+
}
|
|
389
|
+
// Brief socket-up check so the user knows the daemon survived the
|
|
390
|
+
// signal (it should — SIGHUP just triggers a re-read).
|
|
391
|
+
await sleep(150);
|
|
392
|
+
if (!fs.existsSync(socketPath)) {
|
|
393
|
+
console.error(`socket disappeared after SIGHUP — check ${resolveLogPath()}`);
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
console.log(`reloaded: daemon pid ${pid}`);
|
|
397
|
+
if (allowed.length > 0) {
|
|
398
|
+
const preview = allowed.slice(0, 5).join(', ');
|
|
399
|
+
const more = allowed.length > 5 ? ` (+${allowed.length - 5} more)` : '';
|
|
400
|
+
console.log(` ${preview}${more}`);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
function registerStopCommand(program) {
|
|
405
|
+
program
|
|
406
|
+
.command('stop')
|
|
407
|
+
.description('Deactivate the helper daemon (bootout, removes socket)')
|
|
408
|
+
.action(async () => {
|
|
409
|
+
const home = os.homedir();
|
|
410
|
+
const plistPath = path.join(home, 'Library', 'LaunchAgents', `${HELPER_LABEL}.plist`);
|
|
411
|
+
const socketPath = resolveSocketPath();
|
|
412
|
+
const uid = process.getuid?.();
|
|
413
|
+
if (typeof uid !== 'number') {
|
|
414
|
+
console.error('cannot resolve uid');
|
|
415
|
+
process.exit(1);
|
|
416
|
+
}
|
|
417
|
+
const domain = `gui/${uid}`;
|
|
418
|
+
try {
|
|
419
|
+
execFileSync('/bin/launchctl', ['bootout', domain, plistPath], { stdio: 'pipe' });
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
// already gone — fine
|
|
423
|
+
}
|
|
424
|
+
// launchd unlinks the socket when the daemon exits; helper also has an
|
|
425
|
+
// atexit unlink. Best-effort cleanup if either path didn't fire.
|
|
426
|
+
try {
|
|
427
|
+
fs.unlinkSync(socketPath);
|
|
428
|
+
}
|
|
429
|
+
catch { }
|
|
430
|
+
console.log('daemon: stopped');
|
|
431
|
+
if (fs.existsSync(socketPath)) {
|
|
432
|
+
console.warn(`(socket still present at ${socketPath} — may belong to a different process)`);
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
function renderLaunchAgentPlist(opts) {
|
|
437
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
438
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
439
|
+
<plist version="1.0">
|
|
440
|
+
<dict>
|
|
441
|
+
<key>Label</key>
|
|
442
|
+
<string>${escapeXml(opts.label)}</string>
|
|
443
|
+
<key>ProgramArguments</key>
|
|
444
|
+
<array>
|
|
445
|
+
<string>${escapeXml(opts.exec)}</string>
|
|
446
|
+
<string>--socket</string>
|
|
447
|
+
<string>${escapeXml(opts.socketPath)}</string>
|
|
448
|
+
</array>
|
|
449
|
+
<key>RunAtLoad</key>
|
|
450
|
+
<true/>
|
|
451
|
+
<key>KeepAlive</key>
|
|
452
|
+
<true/>
|
|
453
|
+
<key>ProcessType</key>
|
|
454
|
+
<string>Background</string>
|
|
455
|
+
<key>StandardErrorPath</key>
|
|
456
|
+
<string>${escapeXml(opts.logPath)}</string>
|
|
457
|
+
<key>StandardOutPath</key>
|
|
458
|
+
<string>${escapeXml(opts.logPath)}</string>
|
|
459
|
+
</dict>
|
|
460
|
+
</plist>
|
|
461
|
+
`;
|
|
462
|
+
}
|
|
463
|
+
function escapeXml(s) {
|
|
464
|
+
return s
|
|
465
|
+
.replace(/&/g, '&')
|
|
466
|
+
.replace(/</g, '<')
|
|
467
|
+
.replace(/>/g, '>')
|
|
468
|
+
.replace(/"/g, '"')
|
|
469
|
+
.replace(/'/g, ''');
|
|
470
|
+
}
|
|
471
|
+
function sleep(ms) {
|
|
472
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
473
|
+
}
|
|
474
|
+
// Backwards-compat: a few external callers may still import these.
|
|
475
|
+
// Re-export from the shared lib so existing imports keep working.
|
|
476
|
+
export { resolveHelperExec as resolveHelperPath };
|
|
477
|
+
export { resolveSocketPath };
|
package/dist/commands/doctor.js
CHANGED
|
@@ -8,6 +8,7 @@ import { diffVersionSkills, iterSkillsCapableVersions } from '../lib/skills.js';
|
|
|
8
8
|
import { diffVersionHooks, iterHooksCapableVersions } from '../lib/hooks.js';
|
|
9
9
|
import { diffVersionResources, DOCTOR_ALL_KINDS, } from '../lib/doctor-diff.js';
|
|
10
10
|
import { unifiedDiff, colorizeUnifiedDiff } from '../lib/diff-text.js';
|
|
11
|
+
import { setHelpSections } from '../lib/help.js';
|
|
11
12
|
import * as fs from 'fs';
|
|
12
13
|
const AGENT_NAMES = Object.fromEntries(ALL_AGENT_IDS.map((id) => [id, AGENTS[id].name]));
|
|
13
14
|
// ─── overview mode (no target) ────────────────────────────────────────────────
|
|
@@ -314,31 +315,32 @@ function renderTargetText(report, options) {
|
|
|
314
315
|
}
|
|
315
316
|
// ─── command registration ────────────────────────────────────────────────────
|
|
316
317
|
export function registerDoctorCommand(program) {
|
|
317
|
-
program
|
|
318
|
+
const doctorCmd = program
|
|
318
319
|
.command('doctor [target]')
|
|
319
|
-
.description('Diagnose CLI availability, sync status, and resource divergence (optionally for a specific agent[@version])')
|
|
320
|
+
.description('Diagnose CLI availability, sync status, and resource divergence (optionally for a specific agent[@version]).')
|
|
320
321
|
.option('--json', 'Output machine-readable JSON')
|
|
321
322
|
.option('--diff', 'In target mode, include unified diffs for divergent files')
|
|
322
323
|
.option('--kind <kinds>', 'Restrict to comma-separated resource kinds (commands,skills,hooks,rules,mcp,permissions,subagents,plugins,promptcuts)')
|
|
323
|
-
.option('--cwd <path>', 'Resolution cwd for project layer detection (default: process.cwd())')
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
324
|
+
.option('--cwd <path>', 'Resolution cwd for project layer detection (default: process.cwd())');
|
|
325
|
+
setHelpSections(doctorCmd, {
|
|
326
|
+
examples: `
|
|
327
|
+
# Overview: CLI availability + sync status + orphans across all defaults
|
|
328
|
+
agents doctor
|
|
328
329
|
|
|
329
|
-
|
|
330
|
-
|
|
330
|
+
# Full per-resource report for the active default
|
|
331
|
+
agents doctor claude@default
|
|
331
332
|
|
|
332
|
-
|
|
333
|
-
|
|
333
|
+
# All installed versions of one agent
|
|
334
|
+
agents doctor gemini
|
|
334
335
|
|
|
335
|
-
|
|
336
|
-
|
|
336
|
+
# Pin to a specific installed version
|
|
337
|
+
agents doctor codex@0.117.0
|
|
337
338
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
339
|
+
# Inspect only rules and hooks, with full diffs
|
|
340
|
+
agents doctor claude@default --kind rules,hooks --diff
|
|
341
|
+
`,
|
|
342
|
+
});
|
|
343
|
+
doctorCmd.action((target, opts) => {
|
|
342
344
|
const cwd = opts.cwd ? opts.cwd : process.cwd();
|
|
343
345
|
if (!target) {
|
|
344
346
|
const clis = checkAllClis();
|