@phnx-labs/agents-cli 1.20.68 → 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.
Files changed (48) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +11 -1
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/computer.d.ts +26 -0
  5. package/dist/commands/computer.js +173 -149
  6. package/dist/commands/exec.d.ts +18 -0
  7. package/dist/commands/exec.js +88 -6
  8. package/dist/commands/fork.d.ts +9 -0
  9. package/dist/commands/fork.js +67 -0
  10. package/dist/commands/run-account-picker.d.ts +14 -0
  11. package/dist/commands/run-account-picker.js +131 -0
  12. package/dist/commands/setup-browser.d.ts +18 -0
  13. package/dist/commands/setup-browser.js +142 -0
  14. package/dist/commands/setup-computer.d.ts +19 -0
  15. package/dist/commands/setup-computer.js +133 -0
  16. package/dist/commands/setup-share.d.ts +17 -0
  17. package/dist/commands/setup-share.js +85 -0
  18. package/dist/commands/setup.d.ts +1 -1
  19. package/dist/commands/setup.js +58 -1
  20. package/dist/commands/share.d.ts +15 -0
  21. package/dist/commands/share.js +10 -4
  22. package/dist/commands/ssh.js +164 -0
  23. package/dist/commands/view.d.ts +3 -14
  24. package/dist/commands/view.js +27 -28
  25. package/dist/index.js +2 -1
  26. package/dist/lib/agents.d.ts +10 -0
  27. package/dist/lib/agents.js +32 -0
  28. package/dist/lib/auth-health.d.ts +103 -0
  29. package/dist/lib/auth-health.js +232 -0
  30. package/dist/lib/browser/chrome.d.ts +9 -0
  31. package/dist/lib/browser/chrome.js +22 -0
  32. package/dist/lib/computer/download.d.ts +51 -0
  33. package/dist/lib/computer/download.js +145 -0
  34. package/dist/lib/computer-rpc.js +9 -3
  35. package/dist/lib/devices/connect.d.ts +15 -0
  36. package/dist/lib/devices/connect.js +17 -5
  37. package/dist/lib/devices/terminfo.d.ts +44 -0
  38. package/dist/lib/devices/terminfo.js +167 -0
  39. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  40. package/dist/lib/rotate.d.ts +12 -8
  41. package/dist/lib/rotate.js +22 -23
  42. package/dist/lib/session/fork.d.ts +32 -0
  43. package/dist/lib/session/fork.js +101 -0
  44. package/dist/lib/startup/command-registry.d.ts +1 -0
  45. package/dist/lib/startup/command-registry.js +2 -0
  46. package/dist/lib/usage.d.ts +23 -0
  47. package/dist/lib/usage.js +84 -0
  48. package/package.json +1 -1
@@ -18,6 +18,31 @@ import * as fs from 'fs';
18
18
  import * as path from 'path';
19
19
  import * as os from 'os';
20
20
  import { randomUUID } from 'crypto';
21
+ /** Distinguish a terminal account-picker marker from an explicit @version pin. */
22
+ export function parseRunAccountPickerRequest(agentSpec) {
23
+ const requested = agentSpec.endsWith('@');
24
+ const normalizedAgentSpec = requested ? agentSpec.slice(0, -1) : agentSpec;
25
+ return {
26
+ requested,
27
+ normalizedAgentSpec,
28
+ valid: !requested || (!!normalizedAgentSpec && !normalizedAgentSpec.includes('@')),
29
+ };
30
+ }
31
+ /** Return every option whose routing semantics conflict with a local account choice. */
32
+ export function runAccountPickerConflicts(options) {
33
+ const conflicts = [];
34
+ if (options.resume !== undefined)
35
+ conflicts.push('--resume');
36
+ if (options.strategy !== undefined)
37
+ conflicts.push('--strategy');
38
+ if (options.balanced)
39
+ conflicts.push('--balanced');
40
+ if (options.lease)
41
+ conflicts.push('--lease');
42
+ if (options.host || options.device || options.on || options.computer)
43
+ conflicts.push('--host/--device');
44
+ return conflicts;
45
+ }
21
46
  /** Type guard that narrows a string to a known AgentId. */
22
47
  function isValidAgent(agent) {
23
48
  return agent in AGENTS;
@@ -308,6 +333,9 @@ export function registerRunCommand(program) {
308
333
  # Interactive (TUI) with the pinned default version
309
334
  agents run claude
310
335
 
336
+ # Pick a signed-in account/version for only this run
337
+ agents run claude@
338
+
311
339
  # Pipe JSON events to a parser (--quiet drops the preamble)
312
340
  agents run claude "..." --json --quiet | jq
313
341
 
@@ -342,6 +370,10 @@ export function registerRunCommand(program) {
342
370
  A version/account is skipped when it is rate-limited right now — any usage window (incl. the 5-hour session window) at 100%, matching the 'agents view' badge.
343
371
  --balanced is shorthand for --strategy balanced. Ignored when @version is pinned, when a profile is used, or with --fallback.
344
372
 
373
+ Account picker: append @ with no version (agents run claude@) to choose one
374
+ installed account for this run. Rows show identity, login state, plan,
375
+ and available limits; unsafe accounts stay visible but disabled.
376
+
345
377
  Fallback: --fallback codex,gemini retries on rate-limit failure via /continue handoff. Each entry accepts @version.
346
378
 
347
379
  Resume: --resume <id> continues a prior conversation (full or partial id; omit to pick interactively). claude/codex resume natively; others replay via a /continue first message. Add a prompt to continue headlessly.
@@ -370,6 +402,24 @@ export function registerRunCommand(program) {
370
402
  // a native flag, not a prompt. Run interactively.
371
403
  prompt = undefined;
372
404
  }
405
+ // A trailing @ is an explicit request to choose one installed account.
406
+ // Strip only that terminal marker; concrete agent@version pins retain
407
+ // their existing meaning in every dispatch path below.
408
+ const accountPicker = parseRunAccountPickerRequest(agentSpec);
409
+ const accountPickerRequested = accountPicker.requested;
410
+ const normalizedAgentSpec = accountPicker.normalizedAgentSpec;
411
+ if (!accountPicker.valid) {
412
+ console.error(chalk.red(`Invalid account picker target: ${agentSpec}. Use agents run <agent>@.`));
413
+ process.exit(1);
414
+ }
415
+ if (accountPickerRequested) {
416
+ const conflicts = runAccountPickerConflicts(options);
417
+ if (conflicts.length > 0) {
418
+ console.error(chalk.red(`Account selection with ${agentSpec} cannot be combined with ${conflicts.join(', ')}. ` +
419
+ 'Pick the account locally, or use an explicit agent@version target.'));
420
+ process.exit(1);
421
+ }
422
+ }
373
423
  // --lease: invent a disposable cloud box for this run (via crabbox), run
374
424
  // the agent there, then tear it down. Unlike --host, nothing is registered.
375
425
  if (options.lease) {
@@ -393,7 +443,7 @@ export function registerRunCommand(program) {
393
443
  const { leaseAndRun } = await import('../lib/crabbox/lease.js');
394
444
  const { getConfiguredRunStrategy, resolveRunVersion } = await import('../lib/rotate.js');
395
445
  const detected = await detectSignedInRuntimes();
396
- const agentName = agentSpec.split('@')[0];
446
+ const agentName = normalizedAgentSpec.split('@')[0];
397
447
  const leaseCwd = options.cwd ?? process.cwd();
398
448
  // `--lease` requires a prompt (guarded above), so it is headless by
399
449
  // contract — never block on an interactive picker. Provision exactly the
@@ -596,7 +646,7 @@ export function registerRunCommand(program) {
596
646
  throw e;
597
647
  }
598
648
  try {
599
- const [runAgent, rawRunVersion] = agentSpec.split('@');
649
+ const [runAgent, rawRunVersion] = normalizedAgentSpec.split('@');
600
650
  // Forward the explicit @version pin verbatim. Resolving aliases like
601
651
  // @latest locally would check local installs, but the remote host may
602
652
  // have versions the laptop does not. The remote agents CLI resolves
@@ -894,7 +944,7 @@ export function registerRunCommand(program) {
894
944
  await warnUnpushedWork(resumeExec.cwd ?? process.cwd());
895
945
  process.exit(resumeExit);
896
946
  }
897
- const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, headlessPlanStallCommand, nativeResume, resolveInteractive }, { ALL_AGENT_IDS }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle, assertRemoteBundleFlagsUnsupported, isHeadlessSecretsContext }, { splitBundleRef, resolveSshTarget, remoteResolveEnv }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, rotationFailoverChain, shouldArmRotationFailover, RUN_STRATEGIES }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias, ensureAgentRunnable }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents, pruneStaleWorkflowSubagents, ensureSubagentDispatchTool }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports },] = await Promise.all([
947
+ const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, headlessPlanStallCommand, nativeResume, resolveInteractive }, { ALL_AGENT_IDS, ACCOUNT_INSPECTION_AGENT_IDS, agentLabel, supportsAccountInspection }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle, assertRemoteBundleFlagsUnsupported, isHeadlessSecretsContext }, { splitBundleRef, resolveSshTarget, remoteResolveEnv }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, rotationFailoverChain, shouldArmRotationFailover, RUN_STRATEGIES }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias, ensureAgentRunnable }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents, pruneStaleWorkflowSubagents, ensureSubagentDispatchTool }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports },] = await Promise.all([
898
948
  import('../lib/exec.js'),
899
949
  import('../lib/agents.js'),
900
950
  import('../lib/profiles.js'),
@@ -910,7 +960,7 @@ export function registerRunCommand(program) {
910
960
  ]);
911
961
  const isValidAgent = (agent) => ALL_AGENT_IDS.includes(agent);
912
962
  // Parse agent@version
913
- const [rawAgent, rawVersion] = agentSpec.split('@');
963
+ const [rawAgent, rawVersion] = normalizedAgentSpec.split('@');
914
964
  let agent;
915
965
  let version = rawVersion || undefined;
916
966
  let profileEnv;
@@ -938,6 +988,16 @@ export function registerRunCommand(program) {
938
988
  // degenerates to a no-op ("I'll wait for the completion notification").
939
989
  let workflowHasSubagents = false;
940
990
  const cwd = options.cwd ?? process.cwd();
991
+ if (accountPickerRequested && !isValidAgent(rawAgent)) {
992
+ if (profileExists(rawAgent)) {
993
+ console.error(chalk.red(`Account selection is not available for profile '${rawAgent}'. Run its concrete host agent with @ instead.`));
994
+ process.exit(1);
995
+ }
996
+ if (resolveWorkflowRef(rawAgent, cwd)) {
997
+ console.error(chalk.red(`Account selection is not available for workflow '${rawAgent}'. Run a concrete agent with @ instead.`));
998
+ process.exit(1);
999
+ }
1000
+ }
941
1001
  if (isValidAgent(rawAgent)) {
942
1002
  agent = rawAgent;
943
1003
  }
@@ -1161,6 +1221,28 @@ export function registerRunCommand(program) {
1161
1221
  process.exit(1);
1162
1222
  }
1163
1223
  }
1224
+ if (accountPickerRequested) {
1225
+ if (!supportsAccountInspection(agent)) {
1226
+ console.error(chalk.red(`${agentLabel(agent)} does not expose local account state, so agents-cli cannot safely select an account.`));
1227
+ console.error(chalk.gray(`Supported account pickers: ${ACCOUNT_INSPECTION_AGENT_IDS.join(', ')}`));
1228
+ process.exit(1);
1229
+ }
1230
+ try {
1231
+ const { pickRunAccountCandidate } = await import('./run-account-picker.js');
1232
+ const selected = await pickRunAccountCandidate(agent);
1233
+ if (!selected)
1234
+ return;
1235
+ version = selected.version;
1236
+ if (!options.quiet) {
1237
+ const identity = selected.accountLabel || 'signed-in account';
1238
+ process.stderr.write(chalk.gray(`[agents] selected ${identity} · ${agent}@${selected.version} for this run\n`));
1239
+ }
1240
+ }
1241
+ catch (err) {
1242
+ console.error(chalk.red(err.message));
1243
+ process.exit(1);
1244
+ }
1245
+ }
1164
1246
  version = resolveVersionAlias(agent, version);
1165
1247
  // --resume: resolve a prior conversation and rewrite the run target to
1166
1248
  // continue it. `version` here is already the alias-resolved candidate-version
@@ -1298,7 +1380,7 @@ export function registerRunCommand(program) {
1298
1380
  // the bare primary still resolves through the strategy — otherwise every
1299
1381
  // `agents run claude --fallback codex` run lands on the pinned default
1300
1382
  // account and account rotation silently stops (the gh-monitor heal bug).
1301
- if (strategy !== 'pinned' || options.balanced || explicitStrategy) {
1383
+ if (!accountPickerRequested && (strategy !== 'pinned' || options.balanced || explicitStrategy)) {
1302
1384
  if (version) {
1303
1385
  process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} ignored: version ${version} is pinned\n`));
1304
1386
  }
@@ -1373,7 +1455,7 @@ export function registerRunCommand(program) {
1373
1455
  json: options.json,
1374
1456
  quiet: options.quiet,
1375
1457
  authCheckDisabled: options.authCheck === false || process.env.AGENTS_NO_AUTH_CHECK === '1',
1376
- rotated: !!rotationResult,
1458
+ rotated: !!rotationResult || accountPickerRequested,
1377
1459
  });
1378
1460
  if (preflight) {
1379
1461
  try {
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `agents fork <session>` — branch an existing conversation into a new,
3
+ * independent session you can continue separately. The original is untouched.
4
+ *
5
+ * Thin command layer; the copy/register logic lives in `lib/session/fork.ts`.
6
+ */
7
+ import type { Command } from 'commander';
8
+ /** Register the top-level `agents fork` command. */
9
+ export declare function registerForkCommand(program: Command): void;
@@ -0,0 +1,67 @@
1
+ import chalk from 'chalk';
2
+ import { setHelpSections } from '../lib/help.js';
3
+ import { findSessionsById } from '../lib/session/db.js';
4
+ import { discoverSessions } from '../lib/session/discover.js';
5
+ import { forkSession, isForkableAgent, FORKABLE_AGENTS } from '../lib/session/fork.js';
6
+ /** Register the top-level `agents fork` command. */
7
+ export function registerForkCommand(program) {
8
+ const cmd = program
9
+ .command('fork <session>')
10
+ .description('Branch a session into a new, independent copy you can continue separately. The original is untouched.')
11
+ .option('--name <label>', 'Label for the fork (default: "fork of <original>")');
12
+ setHelpSections(cmd, {
13
+ examples: `
14
+ # Fork a session by (partial) id, then continue the fork
15
+ agents fork 4f3a9c21
16
+ agents resume <new-id>
17
+
18
+ # Give the fork a name
19
+ agents fork 4f3a9c21 --name "try redis instead"
20
+ `,
21
+ notes: `
22
+ - 'resume' continues the SAME conversation; 'fork' copies it under a new id so the two diverge.
23
+ - The fork is a full copy of the conversation so far; continuing it never touches the original.
24
+ - Resolve the session the same way as resume: an exact or prefix id fragment.
25
+ - Currently supports: ${FORKABLE_AGENTS.join(', ')}. Other agents are a planned follow-up.
26
+ `,
27
+ });
28
+ cmd.action(async (sessionArg, options) => {
29
+ // Resolve the source. Try the index first; only pay for a rescan if the id
30
+ // isn't found yet (mirrors the resume path's freshen-then-lookup).
31
+ let matches = findSessionsById(sessionArg, {});
32
+ if (matches.length === 0) {
33
+ await discoverSessions({});
34
+ matches = findSessionsById(sessionArg, {});
35
+ }
36
+ if (matches.length === 0) {
37
+ console.log(chalk.red(`No session matching "${sessionArg}".`));
38
+ console.log(chalk.gray('List candidates with: agents sessions'));
39
+ return;
40
+ }
41
+ if (matches.length > 1) {
42
+ console.log(chalk.yellow(`"${sessionArg}" is ambiguous — ${matches.length} sessions match. Use a longer id:`));
43
+ for (const m of matches.slice(0, 8)) {
44
+ console.log(chalk.gray(` ${m.shortId} ${m.agent} ${m.label || m.topic || ''}`));
45
+ }
46
+ return;
47
+ }
48
+ const source = matches[0];
49
+ if (!isForkableAgent(source.agent)) {
50
+ console.log(chalk.yellow(`fork does not support ${source.agent} sessions yet.`));
51
+ console.log(chalk.gray(` Supported: ${FORKABLE_AGENTS.join(', ')}.`));
52
+ return;
53
+ }
54
+ let result;
55
+ try {
56
+ result = forkSession(source, { name: options.name });
57
+ }
58
+ catch (err) {
59
+ console.log(chalk.red(`Could not fork ${source.shortId}: ${err.message}`));
60
+ return;
61
+ }
62
+ console.log(chalk.green(`Forked ${source.shortId} -> ${result.shortId}`));
63
+ console.log(chalk.gray(` Label: ${result.label}`));
64
+ console.log(chalk.gray(` Continue: agents resume ${result.shortId}`));
65
+ console.log(chalk.gray(` Original ${source.shortId} is untouched.`));
66
+ });
67
+ }
@@ -0,0 +1,14 @@
1
+ import type { AgentId } from '../lib/types.js';
2
+ import { type RotateCandidate } from '../lib/rotate.js';
3
+ export interface RunAccountChoice {
4
+ name: string;
5
+ value: string;
6
+ disabled?: string;
7
+ ready: boolean;
8
+ }
9
+ /** Human-readable remaining capacity for every window the provider exposes. */
10
+ export declare function formatAccountLimits(candidate: RotateCandidate): string;
11
+ /** Build aligned picker rows with usable accounts first and unsafe rows disabled. */
12
+ export declare function buildRunAccountChoices(candidates: RotateCandidate[], globalDefault: string | null): RunAccountChoice[];
13
+ /** Prompt for one safe installed account/version. A cancelled picker launches nothing. */
14
+ export declare function pickRunAccountCandidate(agent: AgentId): Promise<RotateCandidate | null>;
@@ -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;