@phnx-labs/agents-cli 1.20.67 → 1.20.69
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 +100 -0
- package/README.md +14 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/check.js +96 -2
- package/dist/commands/computer.d.ts +26 -0
- package/dist/commands/computer.js +173 -149
- package/dist/commands/doctor.js +37 -9
- package/dist/commands/exec.d.ts +18 -0
- package/dist/commands/exec.js +126 -6
- package/dist/commands/fork.d.ts +9 -0
- package/dist/commands/fork.js +67 -0
- package/dist/commands/plugins.js +11 -2
- package/dist/commands/repo.js +47 -2
- package/dist/commands/run-account-picker.d.ts +14 -0
- package/dist/commands/run-account-picker.js +131 -0
- package/dist/commands/setup-browser.d.ts +18 -0
- package/dist/commands/setup-browser.js +142 -0
- package/dist/commands/setup-computer.d.ts +19 -0
- package/dist/commands/setup-computer.js +133 -0
- package/dist/commands/setup-share.d.ts +17 -0
- package/dist/commands/setup-share.js +85 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +58 -1
- package/dist/commands/share.d.ts +15 -0
- package/dist/commands/share.js +10 -4
- package/dist/commands/ssh.js +267 -2
- package/dist/commands/teams.d.ts +11 -0
- package/dist/commands/teams.js +39 -1
- package/dist/commands/view.d.ts +4 -14
- package/dist/commands/view.js +39 -36
- package/dist/index.js +2 -1
- package/dist/lib/agents.d.ts +21 -5
- package/dist/lib/agents.js +59 -24
- package/dist/lib/auth-health.d.ts +103 -0
- package/dist/lib/auth-health.js +232 -0
- package/dist/lib/browser/chrome.d.ts +9 -0
- package/dist/lib/browser/chrome.js +22 -0
- package/dist/lib/browser/profiles.js +4 -25
- package/dist/lib/cli-entry.d.ts +23 -0
- package/dist/lib/cli-entry.js +116 -0
- package/dist/lib/computer/download.d.ts +51 -0
- package/dist/lib/computer/download.js +145 -0
- package/dist/lib/computer-rpc.js +9 -3
- package/dist/lib/daemon.d.ts +2 -2
- package/dist/lib/daemon.js +8 -89
- package/dist/lib/devices/connect.d.ts +15 -0
- package/dist/lib/devices/connect.js +17 -5
- package/dist/lib/devices/fleet.d.ts +23 -0
- package/dist/lib/devices/fleet.js +38 -0
- package/dist/lib/devices/health-report.d.ts +46 -0
- package/dist/lib/devices/health-report.js +159 -0
- package/dist/lib/devices/health.d.ts +14 -4
- package/dist/lib/devices/health.js +49 -8
- package/dist/lib/devices/terminfo.d.ts +44 -0
- package/dist/lib/devices/terminfo.js +167 -0
- package/dist/lib/git.d.ts +10 -0
- package/dist/lib/git.js +23 -0
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/ready.js +5 -3
- package/dist/lib/hosts/remote-cmd.d.ts +12 -0
- package/dist/lib/hosts/remote-cmd.js +17 -1
- package/dist/lib/mcp.d.ts +21 -0
- package/dist/lib/mcp.js +278 -13
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/resources/mcp.js +20 -342
- package/dist/lib/rotate.d.ts +12 -8
- package/dist/lib/rotate.js +22 -23
- package/dist/lib/routines.d.ts +13 -0
- package/dist/lib/routines.js +21 -0
- package/dist/lib/runner.js +18 -40
- package/dist/lib/secrets/agent.js +11 -15
- package/dist/lib/session/fork.d.ts +32 -0
- package/dist/lib/session/fork.js +101 -0
- package/dist/lib/share/capture.js +21 -3
- package/dist/lib/signin-badge.d.ts +41 -0
- package/dist/lib/signin-badge.js +64 -0
- package/dist/lib/ssh-exec.d.ts +5 -0
- package/dist/lib/ssh-exec.js +55 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/usage.d.ts +40 -3
- package/dist/lib/usage.js +147 -16
- package/package.json +1 -1
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { select } from '@inquirer/prompts';
|
|
2
|
+
import { agentLabel } from '../lib/agents.js';
|
|
3
|
+
import { collectRunCandidates, readinessFromCandidate, } from '../lib/rotate.js';
|
|
4
|
+
import { compareVersions, getGlobalDefault } from '../lib/versions.js';
|
|
5
|
+
import { isInteractiveTerminal, isPromptCancelled, requireInteractiveSelection } from './utils.js';
|
|
6
|
+
const CANCEL_SELECTION = '__agents_cancel_account_selection__';
|
|
7
|
+
const WINDOW_ORDER = ['session', 'week', 'sonnet_week', 'month'];
|
|
8
|
+
const WINDOW_LABELS = {
|
|
9
|
+
session: 'Session',
|
|
10
|
+
week: 'Week',
|
|
11
|
+
sonnet_week: 'Sonnet week',
|
|
12
|
+
month: 'Month',
|
|
13
|
+
};
|
|
14
|
+
function formatPercent(value) {
|
|
15
|
+
const rounded = Math.round(value * 10) / 10;
|
|
16
|
+
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
|
|
17
|
+
}
|
|
18
|
+
/** Human-readable remaining capacity for every window the provider exposes. */
|
|
19
|
+
export function formatAccountLimits(candidate) {
|
|
20
|
+
const windows = candidate.usageSnapshot?.windows;
|
|
21
|
+
if (!windows || windows.length === 0)
|
|
22
|
+
return 'limits unavailable';
|
|
23
|
+
return [...windows]
|
|
24
|
+
.sort((a, b) => WINDOW_ORDER.indexOf(a.key) - WINDOW_ORDER.indexOf(b.key))
|
|
25
|
+
.map((window) => {
|
|
26
|
+
const left = Math.max(0, 100 - window.usedPercent);
|
|
27
|
+
return left === 0
|
|
28
|
+
? `${WINDOW_LABELS[window.key]} exhausted`
|
|
29
|
+
: `${WINDOW_LABELS[window.key]} ${formatPercent(left)}% left`;
|
|
30
|
+
})
|
|
31
|
+
.join(' · ');
|
|
32
|
+
}
|
|
33
|
+
function disabledReason(candidate) {
|
|
34
|
+
const readiness = readinessFromCandidate(candidate);
|
|
35
|
+
if (readiness.ready)
|
|
36
|
+
return undefined;
|
|
37
|
+
if (readiness.reason === 'signed_out')
|
|
38
|
+
return 'logged out';
|
|
39
|
+
if (readiness.reason === 'out_of_credits')
|
|
40
|
+
return 'out of credits';
|
|
41
|
+
const windows = candidate.usageSnapshot?.windows ?? [];
|
|
42
|
+
const blocking = windows.filter((window) => window.key !== 'sonnet_week');
|
|
43
|
+
const considered = blocking.length > 0 ? blocking : windows;
|
|
44
|
+
const exhausted = considered
|
|
45
|
+
.filter((window) => window.usedPercent >= 100)
|
|
46
|
+
.map((window) => WINDOW_LABELS[window.key]);
|
|
47
|
+
return exhausted.length > 0
|
|
48
|
+
? `${exhausted.join(' and ')} ${exhausted.length === 1 ? 'limit' : 'limits'} reached`
|
|
49
|
+
: 'rate limit reached';
|
|
50
|
+
}
|
|
51
|
+
/** Build aligned picker rows with usable accounts first and unsafe rows disabled. */
|
|
52
|
+
export function buildRunAccountChoices(candidates, globalDefault) {
|
|
53
|
+
const rows = candidates.map((candidate) => {
|
|
54
|
+
const disabled = disabledReason(candidate);
|
|
55
|
+
const version = candidate.version === globalDefault
|
|
56
|
+
? `${candidate.version} (default)`
|
|
57
|
+
: candidate.version;
|
|
58
|
+
return {
|
|
59
|
+
candidate,
|
|
60
|
+
account: candidate.accountLabel || 'account unavailable',
|
|
61
|
+
version,
|
|
62
|
+
status: candidate.signedIn ? 'logged in' : 'logged out',
|
|
63
|
+
plan: candidate.usageSnapshot?.plan ?? candidate.plan ?? 'plan unavailable',
|
|
64
|
+
limits: formatAccountLimits(candidate),
|
|
65
|
+
disabled,
|
|
66
|
+
ready: disabled === undefined,
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
rows.sort((a, b) => {
|
|
70
|
+
if (a.ready !== b.ready)
|
|
71
|
+
return a.ready ? -1 : 1;
|
|
72
|
+
const aDefault = a.candidate.version === globalDefault;
|
|
73
|
+
const bDefault = b.candidate.version === globalDefault;
|
|
74
|
+
if (aDefault !== bDefault)
|
|
75
|
+
return aDefault ? -1 : 1;
|
|
76
|
+
return compareVersions(b.candidate.version, a.candidate.version);
|
|
77
|
+
});
|
|
78
|
+
const accountWidth = Math.max(0, ...rows.map((row) => row.account.length));
|
|
79
|
+
const versionWidth = Math.max(0, ...rows.map((row) => row.version.length));
|
|
80
|
+
const statusWidth = Math.max(0, ...rows.map((row) => row.status.length));
|
|
81
|
+
const planWidth = Math.max(0, ...rows.map((row) => row.plan.length));
|
|
82
|
+
return rows.map((row) => ({
|
|
83
|
+
name: [
|
|
84
|
+
row.account.padEnd(accountWidth),
|
|
85
|
+
row.version.padEnd(versionWidth),
|
|
86
|
+
row.status.padEnd(statusWidth),
|
|
87
|
+
row.plan.padEnd(planWidth),
|
|
88
|
+
row.limits,
|
|
89
|
+
].join(' '),
|
|
90
|
+
value: row.candidate.version,
|
|
91
|
+
disabled: row.disabled,
|
|
92
|
+
ready: row.ready,
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
/** Prompt for one safe installed account/version. A cancelled picker launches nothing. */
|
|
96
|
+
export async function pickRunAccountCandidate(agent) {
|
|
97
|
+
if (!isInteractiveTerminal()) {
|
|
98
|
+
requireInteractiveSelection(`Selecting a ${agentLabel(agent)} account`, [
|
|
99
|
+
`agents run ${agent}@<version>`,
|
|
100
|
+
`agents view ${agent}`,
|
|
101
|
+
]);
|
|
102
|
+
}
|
|
103
|
+
const candidates = await collectRunCandidates(agent);
|
|
104
|
+
if (candidates.length === 0) {
|
|
105
|
+
throw new Error(`No installed ${agentLabel(agent)} versions are available. Run: agents add ${agent}@latest`);
|
|
106
|
+
}
|
|
107
|
+
const choices = buildRunAccountChoices(candidates, getGlobalDefault(agent));
|
|
108
|
+
const hasReadyAccount = choices.some((choice) => choice.ready);
|
|
109
|
+
const promptChoices = choices.map(({ ready: _ready, ...choice }) => choice);
|
|
110
|
+
if (!hasReadyAccount) {
|
|
111
|
+
promptChoices.push({
|
|
112
|
+
name: 'No usable accounts — cancel',
|
|
113
|
+
value: CANCEL_SELECTION,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const version = await select({
|
|
118
|
+
message: `Select a ${agentLabel(agent)} account for this run:`,
|
|
119
|
+
choices: promptChoices,
|
|
120
|
+
loop: false,
|
|
121
|
+
});
|
|
122
|
+
if (version === CANCEL_SELECTION)
|
|
123
|
+
return null;
|
|
124
|
+
return candidates.find((candidate) => candidate.version === version) ?? null;
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
if (isPromptCancelled(err))
|
|
128
|
+
return null;
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents setup browser` — interactive wizard to get `agents browser` working on
|
|
3
|
+
* a fresh machine: detect an installed Chromium-family browser, create the
|
|
4
|
+
* `default` profile pinned to it, optionally make it this machine's default, and
|
|
5
|
+
* point the user at the one manual step we can't automate (first-run + sign-in).
|
|
6
|
+
*
|
|
7
|
+
* Idempotent: re-running shows the current default profile and offers to change
|
|
8
|
+
* the pinned browser or re-point the device default.
|
|
9
|
+
*/
|
|
10
|
+
import type { Command } from 'commander';
|
|
11
|
+
/**
|
|
12
|
+
* Interactive browser setup. Returns true if a usable default profile exists
|
|
13
|
+
* afterwards, false if the machine has no supported browser or the user backed
|
|
14
|
+
* out. Never throws on cancel — the `agents setup` hub relies on that.
|
|
15
|
+
*/
|
|
16
|
+
export declare function runBrowserWizard(): Promise<boolean>;
|
|
17
|
+
/** Register `agents setup browser` under the parent `setup` command. */
|
|
18
|
+
export declare function registerSetupBrowserCommand(setupCmd: Command): void;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents setup browser` — interactive wizard to get `agents browser` working on
|
|
3
|
+
* a fresh machine: detect an installed Chromium-family browser, create the
|
|
4
|
+
* `default` profile pinned to it, optionally make it this machine's default, and
|
|
5
|
+
* point the user at the one manual step we can't automate (first-run + sign-in).
|
|
6
|
+
*
|
|
7
|
+
* Idempotent: re-running shows the current default profile and offers to change
|
|
8
|
+
* the pinned browser or re-point the device default.
|
|
9
|
+
*/
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { updateMeta } from '../lib/state.js';
|
|
12
|
+
import { findFirstInstalledBrowser, listInstalledBrowsers } from '../lib/browser/chrome.js';
|
|
13
|
+
import { DEFAULT_BROWSER_PROFILE_NAME, createProfile, findFreeProfilePort, getConfiguredDefaultProfileName, getProfile, } from '../lib/browser/profiles.js';
|
|
14
|
+
import { DEFAULT_VIEWPORT } from '../lib/browser/devices.js';
|
|
15
|
+
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
16
|
+
const INSTALL_HINT = 'Install one of: Google Chrome, Brave, Microsoft Edge, Chromium, or Comet, then re-run `agents setup browser`.\n' +
|
|
17
|
+
'(Safari and Firefox are not supported — agents browser drives over the Chrome DevTools Protocol.)';
|
|
18
|
+
/**
|
|
19
|
+
* Interactive browser setup. Returns true if a usable default profile exists
|
|
20
|
+
* afterwards, false if the machine has no supported browser or the user backed
|
|
21
|
+
* out. Never throws on cancel — the `agents setup` hub relies on that.
|
|
22
|
+
*/
|
|
23
|
+
export async function runBrowserWizard() {
|
|
24
|
+
if (!isInteractiveTerminal()) {
|
|
25
|
+
// Non-interactive: do the safe, deterministic thing (auto-detect + pin a
|
|
26
|
+
// default) without prompting, or print the install hint and bail.
|
|
27
|
+
if (!findFirstInstalledBrowser()) {
|
|
28
|
+
console.error(chalk.red('No supported browser found.\n') + chalk.dim(INSTALL_HINT));
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
const existing = await getProfile(DEFAULT_BROWSER_PROFILE_NAME);
|
|
32
|
+
if (existing) {
|
|
33
|
+
console.log(chalk.dim(`Browser profile "${DEFAULT_BROWSER_PROFILE_NAME}" already exists.`));
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
const created = await createAutoDefault();
|
|
37
|
+
console.log(chalk.green(`Created browser profile "${created.name}" → ${created.browser}.`));
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
const installed = listInstalledBrowsers();
|
|
41
|
+
if (installed.length === 0) {
|
|
42
|
+
console.error(chalk.red('No supported browser found on this machine.'));
|
|
43
|
+
console.log(chalk.dim(INSTALL_HINT));
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
const { confirm, select } = await import('@inquirer/prompts');
|
|
47
|
+
// If a default profile already exists, this is a reconfigure.
|
|
48
|
+
const existing = await getProfile(DEFAULT_BROWSER_PROFILE_NAME);
|
|
49
|
+
if (existing) {
|
|
50
|
+
console.log(chalk.dim(`Browser profile "${existing.name}" already exists (${existing.browser}${existing.binary ? ` · ${existing.binary}` : ''}).`));
|
|
51
|
+
const change = await confirm({ message: 'Re-create it (e.g. to pin a different browser)?', default: false });
|
|
52
|
+
if (!change) {
|
|
53
|
+
await maybeSetDeviceDefault(existing.name, confirm);
|
|
54
|
+
printOnboardingNextStep(existing.name);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
// Re-create: drop the old one so createProfile doesn't collide.
|
|
58
|
+
const { deleteProfile } = await import('../lib/browser/profiles.js');
|
|
59
|
+
await deleteProfile(existing.name);
|
|
60
|
+
}
|
|
61
|
+
// Pick which installed browser to pin (auto-select if only one).
|
|
62
|
+
let chosen = installed[0];
|
|
63
|
+
if (installed.length > 1) {
|
|
64
|
+
const value = await select({
|
|
65
|
+
message: 'Which browser should the default profile use?',
|
|
66
|
+
choices: installed.map((b) => ({ name: `${b.browserType} ${chalk.dim(b.binary)}`, value: b.browserType })),
|
|
67
|
+
});
|
|
68
|
+
chosen = installed.find((b) => b.browserType === value) ?? installed[0];
|
|
69
|
+
}
|
|
70
|
+
const freePort = await findFreeProfilePort();
|
|
71
|
+
const profile = {
|
|
72
|
+
name: DEFAULT_BROWSER_PROFILE_NAME,
|
|
73
|
+
description: `${chosen.browserType} profile (agents setup browser)`,
|
|
74
|
+
browser: chosen.browserType,
|
|
75
|
+
binary: chosen.binary,
|
|
76
|
+
endpoints: [`cdp://127.0.0.1:${freePort}`],
|
|
77
|
+
viewport: { width: DEFAULT_VIEWPORT.width, height: DEFAULT_VIEWPORT.height },
|
|
78
|
+
};
|
|
79
|
+
await createProfile(profile);
|
|
80
|
+
console.log(chalk.green(`\nCreated browser profile "${profile.name}" → ${chosen.browserType} (CDP 127.0.0.1:${freePort}).`));
|
|
81
|
+
await maybeSetDeviceDefault(profile.name, confirm);
|
|
82
|
+
printOnboardingNextStep(profile.name);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
/** Build + persist a `default` profile pinned to the first installed browser. */
|
|
86
|
+
async function createAutoDefault() {
|
|
87
|
+
const detected = findFirstInstalledBrowser();
|
|
88
|
+
if (!detected)
|
|
89
|
+
throw new Error('No supported browser found.');
|
|
90
|
+
const freePort = await findFreeProfilePort();
|
|
91
|
+
const profile = {
|
|
92
|
+
name: DEFAULT_BROWSER_PROFILE_NAME,
|
|
93
|
+
description: `Auto-detected ${detected.browserType} profile`,
|
|
94
|
+
browser: detected.browserType,
|
|
95
|
+
binary: detected.binary,
|
|
96
|
+
endpoints: [`cdp://127.0.0.1:${freePort}`],
|
|
97
|
+
viewport: { width: DEFAULT_VIEWPORT.width, height: DEFAULT_VIEWPORT.height },
|
|
98
|
+
};
|
|
99
|
+
await createProfile(profile);
|
|
100
|
+
return profile;
|
|
101
|
+
}
|
|
102
|
+
/** Offer to make `name` this machine's default browser profile (device-local). */
|
|
103
|
+
async function maybeSetDeviceDefault(name, confirm) {
|
|
104
|
+
const current = getConfiguredDefaultProfileName();
|
|
105
|
+
if (current === name)
|
|
106
|
+
return; // already the device default
|
|
107
|
+
const set = await confirm({
|
|
108
|
+
message: `Make "${name}" this machine's default browser profile?`,
|
|
109
|
+
default: true,
|
|
110
|
+
});
|
|
111
|
+
if (set) {
|
|
112
|
+
updateMeta((m) => ({ ...m, defaultBrowserProfile: name }));
|
|
113
|
+
console.log(chalk.dim(`Bare \`agents browser start\` will now use "${name}" on this machine.`));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** The one step we can't automate: Chrome's first-run + your own sign-in. */
|
|
117
|
+
function printOnboardingNextStep(name) {
|
|
118
|
+
console.log(chalk.bold('\nOne manual step left:'));
|
|
119
|
+
console.log(' ' +
|
|
120
|
+
chalk.cyan(`agents browser start --profile ${name}`) +
|
|
121
|
+
chalk.dim(' # finish Chrome first-run + sign in to any sites you want automated'));
|
|
122
|
+
console.log(chalk.dim(` Then check it's ready: agents browser profiles doctor ${name}`));
|
|
123
|
+
}
|
|
124
|
+
/** Register `agents setup browser` under the parent `setup` command. */
|
|
125
|
+
export function registerSetupBrowserCommand(setupCmd) {
|
|
126
|
+
setupCmd
|
|
127
|
+
.command('browser')
|
|
128
|
+
.description('Set up `agents browser` — detect an installed browser and create the default profile.')
|
|
129
|
+
.action(async () => {
|
|
130
|
+
try {
|
|
131
|
+
await runBrowserWizard();
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
if (isPromptCancelled(err)) {
|
|
135
|
+
console.log(chalk.yellow('\nCancelled'));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
console.error(chalk.red(err.message));
|
|
139
|
+
process.exitCode = 1;
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents setup computer` — guided setup for `agents computer` on macOS: fetch +
|
|
3
|
+
* verify the signed helper, install it, then walk the user through the two TCC
|
|
4
|
+
* permission grants (Accessibility + Screen Recording) by opening the exact
|
|
5
|
+
* System Settings panes and polling until the grant lands.
|
|
6
|
+
*
|
|
7
|
+
* The plain `agents computer setup` / `start` commands remain for scripted use;
|
|
8
|
+
* this wizard chains them with the permission hand-holding a fresh machine needs.
|
|
9
|
+
* Idempotent: re-running re-installs the current helper and re-checks trust.
|
|
10
|
+
*/
|
|
11
|
+
import type { Command } from 'commander';
|
|
12
|
+
/**
|
|
13
|
+
* Interactive computer setup. Returns true if the helper is installed and trust
|
|
14
|
+
* is granted (or the user chose to finish later), false if unsupported/aborted.
|
|
15
|
+
* Never throws on cancel — the `agents setup` hub relies on that.
|
|
16
|
+
*/
|
|
17
|
+
export declare function runComputerWizard(): Promise<boolean>;
|
|
18
|
+
/** Register `agents setup computer` under the parent `setup` command. */
|
|
19
|
+
export declare function registerSetupComputerCommand(setupCmd: Command): void;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents setup computer` — guided setup for `agents computer` on macOS: fetch +
|
|
3
|
+
* verify the signed helper, install it, then walk the user through the two TCC
|
|
4
|
+
* permission grants (Accessibility + Screen Recording) by opening the exact
|
|
5
|
+
* System Settings panes and polling until the grant lands.
|
|
6
|
+
*
|
|
7
|
+
* The plain `agents computer setup` / `start` commands remain for scripted use;
|
|
8
|
+
* this wizard chains them with the permission hand-holding a fresh machine needs.
|
|
9
|
+
* Idempotent: re-running re-installs the current helper and re-checks trust.
|
|
10
|
+
*/
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import { execFileSync } from 'node:child_process';
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
import { installComputerHelperMacLocal, activateComputerHelperMacLocal, probeComputerTrust, } from './computer.js';
|
|
15
|
+
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
16
|
+
const ACCESSIBILITY_PANE = 'x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility';
|
|
17
|
+
const SCREEN_PANE = 'x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture';
|
|
18
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
19
|
+
/** Open a System Settings Privacy pane (best-effort — never throws). */
|
|
20
|
+
function openPane(pane) {
|
|
21
|
+
try {
|
|
22
|
+
execFileSync('/usr/bin/open', [pane], { stdio: 'ignore' });
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// ignore — we print the manual path as a fallback
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Interactive computer setup. Returns true if the helper is installed and trust
|
|
30
|
+
* is granted (or the user chose to finish later), false if unsupported/aborted.
|
|
31
|
+
* Never throws on cancel — the `agents setup` hub relies on that.
|
|
32
|
+
*/
|
|
33
|
+
export async function runComputerWizard() {
|
|
34
|
+
if (os.platform() !== 'darwin') {
|
|
35
|
+
console.log(chalk.yellow('`agents setup computer` configures the macOS helper (local control).'));
|
|
36
|
+
console.log(chalk.dim('On Windows, provision a remote host from a Mac/Linux box instead: `agents computer setup --host <device>`.'));
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
// 1. Download + verify + install the signed, notarized helper.
|
|
40
|
+
console.log(chalk.bold('Installing the Computer Helper...'));
|
|
41
|
+
try {
|
|
42
|
+
await installComputerHelperMacLocal();
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
console.error(chalk.red(`Install failed: ${err.message}`));
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
// 2. Activate the daemon so macOS can attribute the TCC grants to it.
|
|
49
|
+
console.log(chalk.bold('\nStarting the helper...'));
|
|
50
|
+
let trusted = false;
|
|
51
|
+
try {
|
|
52
|
+
({ trusted } = await activateComputerHelperMacLocal());
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
console.error(chalk.red(`Could not start the helper: ${err.message}`));
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
// 3. Guide the two permission grants if not already trusted.
|
|
59
|
+
if (!trusted) {
|
|
60
|
+
console.log(chalk.bold('\nGrant two permissions to "Computer Helper" (one-time):'));
|
|
61
|
+
console.log(' 1. ' + chalk.cyan('Accessibility') + chalk.dim(' — lets it click/type'));
|
|
62
|
+
console.log(' 2. ' + chalk.cyan('Screen Recording') + chalk.dim(' — lets it screenshot windows'));
|
|
63
|
+
console.log(chalk.dim('\nOpening System Settings > Privacy & Security ...'));
|
|
64
|
+
openPane(ACCESSIBILITY_PANE);
|
|
65
|
+
if (isInteractiveTerminal()) {
|
|
66
|
+
trusted = await pollForTrust();
|
|
67
|
+
}
|
|
68
|
+
// Also nudge the Screen Recording pane so both are visible.
|
|
69
|
+
openPane(SCREEN_PANE);
|
|
70
|
+
if (!trusted) {
|
|
71
|
+
console.log(chalk.yellow('\nAccessibility not granted yet.'));
|
|
72
|
+
console.log(chalk.dim('Finish both grants in System Settings, then run: ') + chalk.cyan('agents computer start'));
|
|
73
|
+
console.log(chalk.dim(' Accessibility: System Settings > Privacy & Security > Accessibility'));
|
|
74
|
+
console.log(chalk.dim(' Screen Recording: System Settings > Privacy & Security > Screen Recording'));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (trusted) {
|
|
78
|
+
console.log(chalk.green('\nComputer control is ready.'));
|
|
79
|
+
}
|
|
80
|
+
// 4. App allow-list guidance (deny-by-default) — always shown; it's the gate
|
|
81
|
+
// between "trusted" and "can actually drive an app".
|
|
82
|
+
console.log(chalk.bold('\nWhitelist the apps the helper may drive (default is deny-all):'));
|
|
83
|
+
console.log(chalk.dim(' Add a YAML under ~/.agents/permissions/groups/, e.g. computer.yaml:'));
|
|
84
|
+
console.log(chalk.dim(' name: computer'));
|
|
85
|
+
console.log(chalk.dim(' allow:'));
|
|
86
|
+
console.log(chalk.dim(' - "Computer(com.apple.finder)"'));
|
|
87
|
+
console.log(chalk.dim(' - "Computer(com.apple.notes)"'));
|
|
88
|
+
console.log(chalk.dim(' Then reload: ') + chalk.cyan('agents computer reload'));
|
|
89
|
+
return trusted;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Poll the daemon's trust status while the user toggles the Accessibility
|
|
93
|
+
* checkbox in System Settings. Bounded to ~2 minutes; the user can Ctrl+C.
|
|
94
|
+
*/
|
|
95
|
+
async function pollForTrust() {
|
|
96
|
+
const { default: ora } = await import('ora');
|
|
97
|
+
const spinner = ora('Waiting for Accessibility to be granted (toggle the checkbox in System Settings)...').start();
|
|
98
|
+
const deadline = Date.now() + 120_000;
|
|
99
|
+
try {
|
|
100
|
+
while (Date.now() < deadline) {
|
|
101
|
+
if (await probeComputerTrust()) {
|
|
102
|
+
spinner.succeed('Accessibility granted.');
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
await sleep(2000);
|
|
106
|
+
}
|
|
107
|
+
spinner.stop();
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
spinner.stop();
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Register `agents setup computer` under the parent `setup` command. */
|
|
116
|
+
export function registerSetupComputerCommand(setupCmd) {
|
|
117
|
+
setupCmd
|
|
118
|
+
.command('computer')
|
|
119
|
+
.description('Set up `agents computer` (macOS) — install the signed helper and grant control permissions.')
|
|
120
|
+
.action(async () => {
|
|
121
|
+
try {
|
|
122
|
+
await runComputerWizard();
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
if (isPromptCancelled(err)) {
|
|
126
|
+
console.log(chalk.yellow('\nCancelled'));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
console.error(chalk.red(err.message));
|
|
130
|
+
process.exitCode = 1;
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents setup share` — interactive wizard to configure the `agents share`
|
|
3
|
+
* endpoint (Cloudflare R2 + Worker). A friendly front door over the existing
|
|
4
|
+
* `agents share setup` (provision) and `agents share join` flows, reusing their
|
|
5
|
+
* exact logic so there is a single source of truth for provisioning.
|
|
6
|
+
*
|
|
7
|
+
* Idempotent: re-running shows the current endpoint and offers to reconfigure.
|
|
8
|
+
*/
|
|
9
|
+
import type { Command } from 'commander';
|
|
10
|
+
/**
|
|
11
|
+
* Interactive share setup. Returns true if the user configured (or already had)
|
|
12
|
+
* an endpoint, false if they skipped. Never throws on user cancel — callers
|
|
13
|
+
* (the `agents setup` hub) rely on that to keep the fresh-machine flow going.
|
|
14
|
+
*/
|
|
15
|
+
export declare function runShareWizard(): Promise<boolean>;
|
|
16
|
+
/** Register `agents setup share` under the parent `setup` command. */
|
|
17
|
+
export declare function registerSetupShareCommand(setupCmd: Command): void;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents setup share` — interactive wizard to configure the `agents share`
|
|
3
|
+
* endpoint (Cloudflare R2 + Worker). A friendly front door over the existing
|
|
4
|
+
* `agents share setup` (provision) and `agents share join` flows, reusing their
|
|
5
|
+
* exact logic so there is a single source of truth for provisioning.
|
|
6
|
+
*
|
|
7
|
+
* Idempotent: re-running shows the current endpoint and offers to reconfigure.
|
|
8
|
+
*/
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import { DEFAULT_BUCKET_NAME, DEFAULT_CF_BUNDLE, DEFAULT_WORKER_NAME, readShareConfig, } from '../lib/share/config.js';
|
|
11
|
+
import { runShareProvision, runShareJoin } from './share.js';
|
|
12
|
+
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
13
|
+
/**
|
|
14
|
+
* Interactive share setup. Returns true if the user configured (or already had)
|
|
15
|
+
* an endpoint, false if they skipped. Never throws on user cancel — callers
|
|
16
|
+
* (the `agents setup` hub) rely on that to keep the fresh-machine flow going.
|
|
17
|
+
*/
|
|
18
|
+
export async function runShareWizard() {
|
|
19
|
+
if (!isInteractiveTerminal()) {
|
|
20
|
+
console.error(chalk.red('agents setup share needs an interactive terminal. ' +
|
|
21
|
+
'Non-interactively, use `agents share setup` (provision) or `agents share join <url>`.'));
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const existing = readShareConfig();
|
|
25
|
+
if (existing) {
|
|
26
|
+
console.log(chalk.dim(`Share is already configured → ${chalk.green(existing.baseUrl)}`));
|
|
27
|
+
console.log(chalk.dim(` worker ${existing.workerName} · bucket ${existing.bucketName} · account ${existing.accountId}`));
|
|
28
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
29
|
+
const reconfigure = await confirm({ message: 'Reconfigure the share endpoint?', default: false });
|
|
30
|
+
if (!reconfigure) {
|
|
31
|
+
console.log(chalk.dim('Keeping the current endpoint.'));
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const { select } = await import('@inquirer/prompts');
|
|
36
|
+
const mode = await select({
|
|
37
|
+
message: 'How do you want to publish shared links?',
|
|
38
|
+
choices: [
|
|
39
|
+
{
|
|
40
|
+
name: 'Provision my own (Cloudflare R2 + Worker)',
|
|
41
|
+
value: 'provision',
|
|
42
|
+
description: 'One-time: creates a bucket + Worker on your Cloudflare account (~$0). Needs a Cloudflare API token.',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'Join an existing endpoint (a teammate already provisioned one)',
|
|
46
|
+
value: 'join',
|
|
47
|
+
description: 'Paste the base URL + write token from whoever owns the endpoint.',
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
});
|
|
51
|
+
if (mode === 'provision') {
|
|
52
|
+
await runShareProvision({
|
|
53
|
+
bundle: DEFAULT_CF_BUNDLE,
|
|
54
|
+
worker: DEFAULT_WORKER_NAME,
|
|
55
|
+
bucket: DEFAULT_BUCKET_NAME,
|
|
56
|
+
});
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
const { input } = await import('@inquirer/prompts');
|
|
60
|
+
const baseUrl = await input({
|
|
61
|
+
message: 'Endpoint base URL (e.g. https://share.agents-cli.sh)',
|
|
62
|
+
validate: (v) => (v.trim().startsWith('http') ? true : 'Enter the full https:// URL of the endpoint.'),
|
|
63
|
+
});
|
|
64
|
+
await runShareJoin(baseUrl.trim());
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
/** Register `agents setup share` under the parent `setup` command. */
|
|
68
|
+
export function registerSetupShareCommand(setupCmd) {
|
|
69
|
+
setupCmd
|
|
70
|
+
.command('share')
|
|
71
|
+
.description('Configure the `agents share` endpoint (Cloudflare R2 + Worker) — provision your own or join one.')
|
|
72
|
+
.action(async () => {
|
|
73
|
+
try {
|
|
74
|
+
await runShareWizard();
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
if (isPromptCancelled(err)) {
|
|
78
|
+
console.log(chalk.yellow('\nCancelled'));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
console.error(chalk.red(err.message));
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
package/dist/commands/setup.d.ts
CHANGED
|
@@ -18,5 +18,5 @@ export declare function runSetup(program: Command, options?: {
|
|
|
18
18
|
* error and exit.
|
|
19
19
|
*/
|
|
20
20
|
export declare function ensureInitialized(program: Command): Promise<void>;
|
|
21
|
-
/** Register the `agents setup` command. */
|
|
21
|
+
/** Register the `agents setup` command and its capability subcommands. */
|
|
22
22
|
export declare function registerSetupCommand(program: Command): void;
|
package/dist/commands/setup.js
CHANGED
|
@@ -18,6 +18,9 @@ import { AGENTS, agentConfigDirName, getUnmanagedAgentInstalls, countSessionFile
|
|
|
18
18
|
import { setGlobalDefault } from '../lib/versions.js';
|
|
19
19
|
import { ensureShimCurrent, switchHomeFileSymlinks, isShimsInPath, addShimsToPath, getPathSetupInstructions } from '../lib/shims.js';
|
|
20
20
|
import { setHelpSections } from '../lib/help.js';
|
|
21
|
+
import { registerSetupBrowserCommand, runBrowserWizard } from './setup-browser.js';
|
|
22
|
+
import { registerSetupComputerCommand, runComputerWizard } from './setup-computer.js';
|
|
23
|
+
import { registerSetupShareCommand, runShareWizard } from './setup-share.js';
|
|
21
24
|
const HOME = os.homedir();
|
|
22
25
|
/**
|
|
23
26
|
* Import an existing unmanaged agent installation into agents-cli.
|
|
@@ -186,6 +189,10 @@ export async function runSetup(program, options = {}) {
|
|
|
186
189
|
}
|
|
187
190
|
if (options.suppressFooter)
|
|
188
191
|
return;
|
|
192
|
+
// Fresh-machine hub: offer to set up the optional capabilities that need their
|
|
193
|
+
// own guided flow. TTY-only and fully opt-in — a non-interactive `agents setup`
|
|
194
|
+
// stops at the system-repo bootstrap above, unchanged.
|
|
195
|
+
await runSetupHub();
|
|
189
196
|
console.log(chalk.bold('\nSetup complete. Try:'));
|
|
190
197
|
console.log(chalk.cyan(' agents view ') + chalk.gray(' # see what\'s installed'));
|
|
191
198
|
console.log(chalk.cyan(' agents run <agent> "hello" ') + chalk.gray(' # run an agent'));
|
|
@@ -217,13 +224,52 @@ export async function ensureInitialized(program) {
|
|
|
217
224
|
}
|
|
218
225
|
await runSetup(program, { suppressFooter: true });
|
|
219
226
|
}
|
|
220
|
-
/**
|
|
227
|
+
/**
|
|
228
|
+
* Interactive "what else do you want to set up?" menu shown after the bare
|
|
229
|
+
* `agents setup` finishes on a TTY. Each pick runs that capability's guided
|
|
230
|
+
* wizard. Never throws — a cancel or an optional wizard's error just skips the
|
|
231
|
+
* rest and lets core setup complete.
|
|
232
|
+
*/
|
|
233
|
+
async function runSetupHub() {
|
|
234
|
+
if (!isInteractiveTerminal())
|
|
235
|
+
return;
|
|
236
|
+
try {
|
|
237
|
+
const { checkbox } = await import('@inquirer/prompts');
|
|
238
|
+
const picks = await checkbox({
|
|
239
|
+
message: 'Set up optional capabilities now? (space to select, enter to confirm)',
|
|
240
|
+
choices: [
|
|
241
|
+
{ name: 'browser — drive a real Chrome/Brave/Edge for web automation', value: 'browser' },
|
|
242
|
+
{ name: 'computer — control native macOS apps (screenshot, click, type)', value: 'computer' },
|
|
243
|
+
{ name: 'share — publish shareable links (Cloudflare R2 + Worker)', value: 'share' },
|
|
244
|
+
],
|
|
245
|
+
});
|
|
246
|
+
for (const pick of picks) {
|
|
247
|
+
console.log();
|
|
248
|
+
if (pick === 'browser')
|
|
249
|
+
await runBrowserWizard();
|
|
250
|
+
else if (pick === 'computer')
|
|
251
|
+
await runComputerWizard();
|
|
252
|
+
else if (pick === 'share')
|
|
253
|
+
await runShareWizard();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
if (isPromptCancelled(err))
|
|
258
|
+
return;
|
|
259
|
+
console.log(chalk.yellow(`Optional setup skipped: ${err.message}`));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/** Register the `agents setup` command and its capability subcommands. */
|
|
221
263
|
export function registerSetupCommand(program) {
|
|
222
264
|
const setupCmd = program
|
|
223
265
|
.command('setup')
|
|
224
266
|
.description('First-time setup. Clones a config repo and installs agent CLIs.')
|
|
225
267
|
.option('-f, --force', 'Re-run setup even if ~/.agents/.system/ already exists (use with caution)')
|
|
226
268
|
.option('--no-system-repo', 'Skip cloning the system repo (you must populate ~/.agents/.system/ yourself)');
|
|
269
|
+
// Capability subcommands: `agents setup browser|computer|share`.
|
|
270
|
+
registerSetupBrowserCommand(setupCmd);
|
|
271
|
+
registerSetupComputerCommand(setupCmd);
|
|
272
|
+
registerSetupShareCommand(setupCmd);
|
|
227
273
|
setHelpSections(setupCmd, {
|
|
228
274
|
examples: `
|
|
229
275
|
# First-time setup (clones the system repo into ~/.agents/.system/)
|
|
@@ -231,11 +277,22 @@ export function registerSetupCommand(program) {
|
|
|
231
277
|
|
|
232
278
|
# Re-run after corruption or to repair ~/.agents/.system/
|
|
233
279
|
agents setup --force
|
|
280
|
+
|
|
281
|
+
# Set up a specific capability on its own
|
|
282
|
+
agents setup browser
|
|
283
|
+
agents setup computer
|
|
284
|
+
agents setup share
|
|
234
285
|
`,
|
|
235
286
|
notes: `
|
|
236
287
|
What it does:
|
|
237
288
|
1. Clones the system repo into ~/.agents/.system/
|
|
238
289
|
2. Imports any unmanaged agent installations it finds
|
|
290
|
+
3. On a TTY, offers to set up optional capabilities (browser/computer/share)
|
|
291
|
+
|
|
292
|
+
Capability setup can also be run any time on its own:
|
|
293
|
+
agents setup browser # detect a browser + create the default profile
|
|
294
|
+
agents setup computer # install the signed macOS helper + grant permissions
|
|
295
|
+
agents setup share # provision or join a Cloudflare share endpoint
|
|
239
296
|
|
|
240
297
|
To install CLIs from agents.yaml and sync resources into version homes:
|
|
241
298
|
agents repo refresh -y
|