@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
package/dist/commands/doctor.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { addHostOption } from '../lib/hosts/option.js';
|
|
2
2
|
import { buildRemoteAgentsInvocation } from '../lib/hosts/remote-cmd.js';
|
|
3
3
|
import { loadDevices, isControlDevice } from '../lib/devices/registry.js';
|
|
4
|
+
import { fanOutDevices } from '../lib/devices/fleet.js';
|
|
4
5
|
import { resolveHost } from '../lib/hosts/registry.js';
|
|
5
|
-
import {
|
|
6
|
+
import { sshExecAsync } from '../lib/ssh-exec.js';
|
|
6
7
|
import { sshTargetFor } from '../lib/hosts/types.js';
|
|
7
8
|
import { machineId } from '../lib/session/sync/config.js';
|
|
8
9
|
import chalk from 'chalk';
|
|
9
10
|
import { checkAllClis, collectTeamsDoctorData } from '../lib/teams/agents.js';
|
|
10
|
-
import { AGENTS, ALL_AGENT_IDS, resolveAgentName, formatAgentError } from '../lib/agents.js';
|
|
11
|
+
import { AGENTS, ALL_AGENT_IDS, resolveAgentName, formatAgentError, getAccountInfo } from '../lib/agents.js';
|
|
12
|
+
import { formatSignInBadge } from '../lib/signin-badge.js';
|
|
11
13
|
import { getGlobalDefault, getVersionHomePath, isVersionInstalled, listInstalledVersions, parseAgentSpec, } from '../lib/versions.js';
|
|
12
14
|
import { loadManifest, isStale } from '../lib/staleness/index.js';
|
|
13
15
|
import { diffVersionResources, DOCTOR_ALL_KINDS, } from '../lib/doctor-diff.js';
|
|
@@ -55,7 +57,7 @@ function printWrappedLine(prefix, text) {
|
|
|
55
57
|
for (const line of wrapLine(prefix, text))
|
|
56
58
|
console.log(chalk.gray(line));
|
|
57
59
|
}
|
|
58
|
-
function renderOverviewText(clis, syncRows, orphanRows, hostClis) {
|
|
60
|
+
function renderOverviewText(clis, syncRows, orphanRows, hostClis, signIn) {
|
|
59
61
|
console.log(chalk.bold('Agent CLIs'));
|
|
60
62
|
// Show the fleet you actually run — agents that are ready in PATH, plus any
|
|
61
63
|
// you MANAGE (have installed versions) whose binary isn't resolving (a real
|
|
@@ -72,7 +74,9 @@ function renderOverviewText(clis, syncRows, orphanRows, hostClis) {
|
|
|
72
74
|
for (const [name, entry] of shown) {
|
|
73
75
|
const pretty = (AGENT_NAMES[name] || name).padEnd(11);
|
|
74
76
|
if (entry.installed) {
|
|
75
|
-
|
|
77
|
+
const badge = formatSignInBadge(signIn[name]);
|
|
78
|
+
const pathHint = entry.path ? chalk.gray(` ${entry.path}`) : '';
|
|
79
|
+
console.log(` ${chalk.green('ready')} ${pretty} ${badge}${pathHint}`);
|
|
76
80
|
}
|
|
77
81
|
else {
|
|
78
82
|
console.log(` ${chalk.red('no ')} ${pretty} ${chalk.gray(entry.error || 'not installed')}`);
|
|
@@ -266,7 +270,7 @@ async function probeFleetTarget(target) {
|
|
|
266
270
|
// has it via the install profile, and our single-quote escaping would
|
|
267
271
|
// prevent $HOME expansion there, so skip the bootstrap on Windows.
|
|
268
272
|
isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' });
|
|
269
|
-
const res =
|
|
273
|
+
const res = await sshExecAsync(target.sshTarget, remoteCmd, { timeoutMs: 30000, multiplex: true });
|
|
270
274
|
if (res.code !== 0) {
|
|
271
275
|
return {
|
|
272
276
|
name: target.name,
|
|
@@ -298,9 +302,18 @@ async function runDevicesDoctor(opts) {
|
|
|
298
302
|
if (!singleName) {
|
|
299
303
|
results.push({ name: localName, online: true, agents: await collectTeamsDoctorData() });
|
|
300
304
|
}
|
|
301
|
-
// Remote targets in parallel.
|
|
302
|
-
const remoteResults = await
|
|
303
|
-
results.push(...remoteResults)
|
|
305
|
+
// Remote targets in parallel, through the shared fleet fan-out helper.
|
|
306
|
+
const remoteResults = await fanOutDevices(targets, probeFleetTarget);
|
|
307
|
+
results.push(...remoteResults.map((r) => {
|
|
308
|
+
if (r.status === 'ok' && r.value)
|
|
309
|
+
return r.value;
|
|
310
|
+
return {
|
|
311
|
+
name: r.name,
|
|
312
|
+
online: false,
|
|
313
|
+
error: r.error ?? String(r.reason ?? 'skipped'),
|
|
314
|
+
agents: {},
|
|
315
|
+
};
|
|
316
|
+
}));
|
|
304
317
|
if (opts.json) {
|
|
305
318
|
console.log(JSON.stringify({ devices: results }, null, 2));
|
|
306
319
|
return;
|
|
@@ -710,9 +723,24 @@ export function registerDoctorCommand(program) {
|
|
|
710
723
|
const syncRows = checkSyncStatus(cwd);
|
|
711
724
|
const orphanRows = countOrphans();
|
|
712
725
|
const hostClis = listCliStatus(cwd);
|
|
726
|
+
// Advisory login state per installed agent (file-based getAccountInfo,
|
|
727
|
+
// no home → the account-global/active credential). Best-effort: a probe
|
|
728
|
+
// failure just leaves that agent's badge as "logged out".
|
|
729
|
+
const signIn = {};
|
|
730
|
+
await Promise.all(Object.entries(clis)
|
|
731
|
+
.filter(([, e]) => e.installed)
|
|
732
|
+
.map(async ([name]) => {
|
|
733
|
+
try {
|
|
734
|
+
signIn[name] = await getAccountInfo(name);
|
|
735
|
+
}
|
|
736
|
+
catch {
|
|
737
|
+
/* advisory only */
|
|
738
|
+
}
|
|
739
|
+
}));
|
|
713
740
|
if (opts.json) {
|
|
714
741
|
console.log(JSON.stringify({
|
|
715
742
|
clis,
|
|
743
|
+
signIn,
|
|
716
744
|
sync: syncRows,
|
|
717
745
|
orphans: orphanRows,
|
|
718
746
|
hostClis: {
|
|
@@ -727,7 +755,7 @@ export function registerDoctorCommand(program) {
|
|
|
727
755
|
}, null, 2));
|
|
728
756
|
return;
|
|
729
757
|
}
|
|
730
|
-
renderOverviewText(clis, syncRows, orphanRows, hostClis);
|
|
758
|
+
renderOverviewText(clis, syncRows, orphanRows, hostClis, signIn);
|
|
731
759
|
// Point at the interactive reconcile when anything is out of sync — the
|
|
732
760
|
// report shouldn't be a dead end. `agents status` runs the unified
|
|
733
761
|
// home-reading engine and offers to sync (opt-in, never auto-fires here).
|
package/dist/commands/exec.d.ts
CHANGED
|
@@ -8,6 +8,24 @@
|
|
|
8
8
|
import { type Command } from 'commander';
|
|
9
9
|
import type { ExecEffort } from '../lib/exec.js';
|
|
10
10
|
import { type SshGResult } from '../lib/hosts/ssh-config.js';
|
|
11
|
+
export interface RunAccountPickerRequest {
|
|
12
|
+
requested: boolean;
|
|
13
|
+
normalizedAgentSpec: string;
|
|
14
|
+
valid: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** Distinguish a terminal account-picker marker from an explicit @version pin. */
|
|
17
|
+
export declare function parseRunAccountPickerRequest(agentSpec: string): RunAccountPickerRequest;
|
|
18
|
+
/** Return every option whose routing semantics conflict with a local account choice. */
|
|
19
|
+
export declare function runAccountPickerConflicts(options: {
|
|
20
|
+
resume?: string | boolean;
|
|
21
|
+
strategy?: string;
|
|
22
|
+
balanced?: boolean;
|
|
23
|
+
lease?: string | boolean;
|
|
24
|
+
host?: string;
|
|
25
|
+
device?: string;
|
|
26
|
+
on?: string;
|
|
27
|
+
computer?: string;
|
|
28
|
+
}): string[];
|
|
11
29
|
/** The host descriptor fields the `--copy-creds` security gate reads. */
|
|
12
30
|
export interface CopyCredsGateHost {
|
|
13
31
|
name: string;
|
package/dist/commands/exec.js
CHANGED
|
@@ -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;
|
|
@@ -259,6 +284,7 @@ export function registerRunCommand(program) {
|
|
|
259
284
|
.option('--json', 'Stream events as JSON lines (for parsing by other tools)')
|
|
260
285
|
.option('--quiet', 'Suppress preamble (rotation banner, "Running:" line). Useful when piping JSON events to a parser.', false)
|
|
261
286
|
.option('--headless', 'Force headless mode. Auto-enabled when a prompt is provided; pass explicitly to stay headless with no prompt (reads the prompt from stdin).', false)
|
|
287
|
+
.option('--no-auth-check', 'Skip the pre-launch "looks logged out" warning on an interactive run (advisory; never blocks anyway). Also silenced by AGENTS_NO_AUTH_CHECK=1.')
|
|
262
288
|
.option('-i, --interactive', 'Force interactive mode even when a prompt is provided. Mutually exclusive with --headless.')
|
|
263
289
|
.option('--resume [id]', 'Resume a previous conversation. Accepts a full or partial session id (prefix-matched against the index); omit the id to pick from recent sessions interactively. Resumes under the version that started the session. claude/codex resume natively; other agents replay via a /continue first message. Pair with a prompt to continue headlessly.')
|
|
264
290
|
.option('--session-id <id>', 'Force a NEW conversation to use this exact session UUID (Claude only). This CREATES a session — to resume an existing one, use --resume.')
|
|
@@ -281,7 +307,7 @@ export function registerRunCommand(program) {
|
|
|
281
307
|
.option('--interval <dur>', 'Loop delay between iterations ("0" back-to-back, "30m" paces). Loop only.')
|
|
282
308
|
.option('--host <name>', 'Offload this run onto another machine over SSH instead of running locally — a device, a registered agent host, or user@host. See `agents devices` / `agents hosts`.')
|
|
283
309
|
.option('--device <name>', 'Alias of --host: offload this run onto a registered device (from `agents devices`).')
|
|
284
|
-
.option('--remote-cwd <dir>',
|
|
310
|
+
.option('--remote-cwd <dir>', "Explicit host working directory for --host runs, used VERBATIM (overrides --cwd; usually --cwd suffices — it re-roots a local-home path onto the remote home). Pass a single-quoted '$HOME/…' or a valid remote absolute path; a local ~ expands here and won't exist there (/Users/you vs /home/you).")
|
|
285
311
|
.option('--no-follow', 'With --host, dispatch detached and return immediately (track via `agents hosts ps/logs`).')
|
|
286
312
|
.option('--any', 'With --host <cap> (a capability tag), pick any matching host instead of erroring when several match.')
|
|
287
313
|
.option('--copy-creds', 'With --host, copy the picked runtime credentials (and Claude OAuth token) to the host, then shred them after the run. Opt-in per run.')
|
|
@@ -307,6 +333,9 @@ export function registerRunCommand(program) {
|
|
|
307
333
|
# Interactive (TUI) with the pinned default version
|
|
308
334
|
agents run claude
|
|
309
335
|
|
|
336
|
+
# Pick a signed-in account/version for only this run
|
|
337
|
+
agents run claude@
|
|
338
|
+
|
|
310
339
|
# Pipe JSON events to a parser (--quiet drops the preamble)
|
|
311
340
|
agents run claude "..." --json --quiet | jq
|
|
312
341
|
|
|
@@ -341,6 +370,10 @@ export function registerRunCommand(program) {
|
|
|
341
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.
|
|
342
371
|
--balanced is shorthand for --strategy balanced. Ignored when @version is pinned, when a profile is used, or with --fallback.
|
|
343
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
|
+
|
|
344
377
|
Fallback: --fallback codex,gemini retries on rate-limit failure via /continue handoff. Each entry accepts @version.
|
|
345
378
|
|
|
346
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.
|
|
@@ -369,6 +402,24 @@ export function registerRunCommand(program) {
|
|
|
369
402
|
// a native flag, not a prompt. Run interactively.
|
|
370
403
|
prompt = undefined;
|
|
371
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
|
+
}
|
|
372
423
|
// --lease: invent a disposable cloud box for this run (via crabbox), run
|
|
373
424
|
// the agent there, then tear it down. Unlike --host, nothing is registered.
|
|
374
425
|
if (options.lease) {
|
|
@@ -392,7 +443,7 @@ export function registerRunCommand(program) {
|
|
|
392
443
|
const { leaseAndRun } = await import('../lib/crabbox/lease.js');
|
|
393
444
|
const { getConfiguredRunStrategy, resolveRunVersion } = await import('../lib/rotate.js');
|
|
394
445
|
const detected = await detectSignedInRuntimes();
|
|
395
|
-
const agentName =
|
|
446
|
+
const agentName = normalizedAgentSpec.split('@')[0];
|
|
396
447
|
const leaseCwd = options.cwd ?? process.cwd();
|
|
397
448
|
// `--lease` requires a prompt (guarded above), so it is headless by
|
|
398
449
|
// contract — never block on an interactive picker. Provision exactly the
|
|
@@ -595,7 +646,7 @@ export function registerRunCommand(program) {
|
|
|
595
646
|
throw e;
|
|
596
647
|
}
|
|
597
648
|
try {
|
|
598
|
-
const [runAgent, rawRunVersion] =
|
|
649
|
+
const [runAgent, rawRunVersion] = normalizedAgentSpec.split('@');
|
|
599
650
|
// Forward the explicit @version pin verbatim. Resolving aliases like
|
|
600
651
|
// @latest locally would check local installs, but the remote host may
|
|
601
652
|
// have versions the laptop does not. The remote agents CLI resolves
|
|
@@ -893,7 +944,7 @@ export function registerRunCommand(program) {
|
|
|
893
944
|
await warnUnpushedWork(resumeExec.cwd ?? process.cwd());
|
|
894
945
|
process.exit(resumeExit);
|
|
895
946
|
}
|
|
896
|
-
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([
|
|
897
948
|
import('../lib/exec.js'),
|
|
898
949
|
import('../lib/agents.js'),
|
|
899
950
|
import('../lib/profiles.js'),
|
|
@@ -909,7 +960,7 @@ export function registerRunCommand(program) {
|
|
|
909
960
|
]);
|
|
910
961
|
const isValidAgent = (agent) => ALL_AGENT_IDS.includes(agent);
|
|
911
962
|
// Parse agent@version
|
|
912
|
-
const [rawAgent, rawVersion] =
|
|
963
|
+
const [rawAgent, rawVersion] = normalizedAgentSpec.split('@');
|
|
913
964
|
let agent;
|
|
914
965
|
let version = rawVersion || undefined;
|
|
915
966
|
let profileEnv;
|
|
@@ -937,6 +988,16 @@ export function registerRunCommand(program) {
|
|
|
937
988
|
// degenerates to a no-op ("I'll wait for the completion notification").
|
|
938
989
|
let workflowHasSubagents = false;
|
|
939
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
|
+
}
|
|
940
1001
|
if (isValidAgent(rawAgent)) {
|
|
941
1002
|
agent = rawAgent;
|
|
942
1003
|
}
|
|
@@ -1160,6 +1221,28 @@ export function registerRunCommand(program) {
|
|
|
1160
1221
|
process.exit(1);
|
|
1161
1222
|
}
|
|
1162
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
|
+
}
|
|
1163
1246
|
version = resolveVersionAlias(agent, version);
|
|
1164
1247
|
// --resume: resolve a prior conversation and rewrite the run target to
|
|
1165
1248
|
// continue it. `version` here is already the alias-resolved candidate-version
|
|
@@ -1297,7 +1380,7 @@ export function registerRunCommand(program) {
|
|
|
1297
1380
|
// the bare primary still resolves through the strategy — otherwise every
|
|
1298
1381
|
// `agents run claude --fallback codex` run lands on the pinned default
|
|
1299
1382
|
// account and account rotation silently stops (the gh-monitor heal bug).
|
|
1300
|
-
if (strategy !== 'pinned' || options.balanced || explicitStrategy) {
|
|
1383
|
+
if (!accountPickerRequested && (strategy !== 'pinned' || options.balanced || explicitStrategy)) {
|
|
1301
1384
|
if (version) {
|
|
1302
1385
|
process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} ignored: version ${version} is pinned\n`));
|
|
1303
1386
|
}
|
|
@@ -1350,6 +1433,43 @@ export function registerRunCommand(program) {
|
|
|
1350
1433
|
}
|
|
1351
1434
|
}
|
|
1352
1435
|
const defaultVersion = version ?? resolveVersion(agent, cwd);
|
|
1436
|
+
// Login preflight (advisory, warn + continue). On a local INTERACTIVE
|
|
1437
|
+
// launch, probe whether this agent's account has a credential and print a
|
|
1438
|
+
// one-line warning if it looks logged out — so you find out BEFORE the TUI
|
|
1439
|
+
// opens, not after typing a prompt and getting "/login" back. Uses the same
|
|
1440
|
+
// account-global probe as `checkCliSignedIn` / `agents doctor`
|
|
1441
|
+
// (getAccountInfo with no home): file-based, no Keychain ACL prompt, and
|
|
1442
|
+
// correct for HOME-global credential agents (grok/codex) where a per-version
|
|
1443
|
+
// home would false-negative. It can still false-negative for opaque
|
|
1444
|
+
// credentials, so this NEVER blocks — it warns and launches anyway. Skipped
|
|
1445
|
+
// for --json/--quiet, when a rotation already picked a signed-in account,
|
|
1446
|
+
// and via --no-auth-check / AGENTS_NO_AUTH_CHECK=1. (--host/--lease return
|
|
1447
|
+
// earlier.)
|
|
1448
|
+
{
|
|
1449
|
+
const { shouldCheckLoginBeforeLaunch, loginHint } = await import('../lib/signin-badge.js');
|
|
1450
|
+
const preflight = shouldCheckLoginBeforeLaunch({
|
|
1451
|
+
interactive: options.interactive,
|
|
1452
|
+
forceInteractive, // a resumed interactive session (e.g. `run kimi --resume`) opens the TUI too
|
|
1453
|
+
headless: options.headless,
|
|
1454
|
+
hasPrompt: prompt !== undefined,
|
|
1455
|
+
json: options.json,
|
|
1456
|
+
quiet: options.quiet,
|
|
1457
|
+
authCheckDisabled: options.authCheck === false || process.env.AGENTS_NO_AUTH_CHECK === '1',
|
|
1458
|
+
rotated: !!rotationResult || accountPickerRequested,
|
|
1459
|
+
});
|
|
1460
|
+
if (preflight) {
|
|
1461
|
+
try {
|
|
1462
|
+
const { getAccountInfo } = await import('../lib/agents.js');
|
|
1463
|
+
const info = await getAccountInfo(agent);
|
|
1464
|
+
if (!info.signedIn) {
|
|
1465
|
+
process.stderr.write(chalk.yellow(`⚠ ${agent} looks logged out — log in with: ${loginHint(agent)}. Launching anyway...\n`));
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
catch {
|
|
1469
|
+
// Advisory only — a probe failure must never block a launch.
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1353
1473
|
const runDefaults = fromProfile
|
|
1354
1474
|
? { sources: {} }
|
|
1355
1475
|
: resolveRunDefaults(agent, defaultVersion, cwd);
|
|
@@ -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
|
+
}
|
package/dist/commands/plugins.js
CHANGED
|
@@ -58,8 +58,14 @@ When to use:
|
|
|
58
58
|
- Team onboarding: distribute a full toolkit via a single plugin directory
|
|
59
59
|
`);
|
|
60
60
|
// Shared list implementation — reused by `list` and the bare `agents plugins` default.
|
|
61
|
-
const runList = async () => {
|
|
61
|
+
const runList = async (options = {}) => {
|
|
62
62
|
const plugins = discoverPlugins();
|
|
63
|
+
if (options.json) {
|
|
64
|
+
// Structured dump (name/description + per-agent-version sync targets) — the
|
|
65
|
+
// same rows the table is built from, mirroring `plugins marketplaces --json`.
|
|
66
|
+
console.log(JSON.stringify(plugins.length === 0 ? [] : buildPluginRows(plugins), null, 2));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
63
69
|
if (plugins.length === 0) {
|
|
64
70
|
console.log(chalk.gray('No plugins found in ~/.agents/plugins/'));
|
|
65
71
|
console.log(chalk.gray('Plugins are directories with .claude-plugin/plugin.json'));
|
|
@@ -75,12 +81,15 @@ When to use:
|
|
|
75
81
|
centralPath: getPluginsDir(),
|
|
76
82
|
});
|
|
77
83
|
};
|
|
78
|
-
// Bare `agents plugins` → same as `list`.
|
|
84
|
+
// Bare `agents plugins` → same as `list`. (--json lives on the explicit `list`
|
|
85
|
+
// subcommand only; declaring it on both parent + child makes commander bind the
|
|
86
|
+
// flag to the parent for `plugins list --json`, dropping it from the subcommand.)
|
|
79
87
|
pluginsCmd.action(runList);
|
|
80
88
|
// agents plugins list
|
|
81
89
|
pluginsCmd
|
|
82
90
|
.command('list')
|
|
83
91
|
.description('Show plugins in a table with sync status across agent versions')
|
|
92
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
84
93
|
.action(runList);
|
|
85
94
|
// agents plugins marketplaces
|
|
86
95
|
const marketplacesCmd = pluginsCmd
|
package/dist/commands/repo.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { visibleWidth, padVisible } from '../lib/format.js';
|
|
3
|
+
import { stripAnsi } from '../lib/session/width.js';
|
|
3
4
|
import ora from 'ora';
|
|
4
5
|
import * as fs from 'fs';
|
|
5
6
|
import * as path from 'path';
|
|
@@ -28,7 +29,7 @@ function resolveRepoPath(target) {
|
|
|
28
29
|
return path.join(HOME, `.agents-${trimmed}`);
|
|
29
30
|
}
|
|
30
31
|
import { applyExtraAliasToVersions, ensureAgentsDir, getExtraRepoDir, getSystemAgentsDir, getUserAgentsDir, readMeta, resolveExtraRepoDir, updateMeta, } from '../lib/state.js';
|
|
31
|
-
import { parseSource, pullRepo, commitAndPush, isGitRepo, isSystemRepoOrigin, adoptRepo, displayHomePath, } from '../lib/git.js';
|
|
32
|
+
import { parseSource, pullRepo, commitAndPush, isGitRepo, isSystemRepoOrigin, adoptRepo, getRemoteUrl, sameGitRemote, displayHomePath, } from '../lib/git.js';
|
|
32
33
|
import { DEFAULT_SYSTEM_REPO } from '../lib/types.js';
|
|
33
34
|
import { ALL_AGENT_IDS, isAgentName, resolveAgentName } from '../lib/agents.js';
|
|
34
35
|
import { refresh } from '../lib/refresh.js';
|
|
@@ -444,10 +445,22 @@ async function listRepos(alias, opts = {}) {
|
|
|
444
445
|
return;
|
|
445
446
|
}
|
|
446
447
|
if (targets.length === 0) {
|
|
448
|
+
if (opts.json) {
|
|
449
|
+
console.log('[]');
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
447
452
|
console.log(chalk.gray('No repos to show.'));
|
|
448
453
|
return;
|
|
449
454
|
}
|
|
450
455
|
const rows = await Promise.all(targets.map(renderRepoRow));
|
|
456
|
+
if (opts.json) {
|
|
457
|
+
// Structured dump of the same rows so agents can enumerate repos + sync state.
|
|
458
|
+
// `raw` (the missing / no-git-remote human label) is the only colored field —
|
|
459
|
+
// strip ANSI so the JSON stays clean; every other field is already structured.
|
|
460
|
+
const clean = rows.map((r) => (r.raw !== undefined ? { ...r, raw: stripAnsi(r.raw) } : r));
|
|
461
|
+
console.log(JSON.stringify(clean, null, 2));
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
451
464
|
// TTY width when interactive; fall back to $COLUMNS (honored when piped) then 80.
|
|
452
465
|
const cols = process.stdout.columns || Number(process.env.COLUMNS) || 80;
|
|
453
466
|
console.log('');
|
|
@@ -600,6 +613,7 @@ export function registerRepoCommands(program) {
|
|
|
600
613
|
.command('add <source>')
|
|
601
614
|
.description('Register an existing local repo or clone a remote repo into ~/.agents-<alias>/')
|
|
602
615
|
.option('--as <alias>', 'Override the auto-derived alias (letters, digits, _ or -)')
|
|
616
|
+
.option('--adopt', 'If the target directory already holds a git checkout, register it in place instead of erroring (even if its origin differs)')
|
|
603
617
|
.action(async (source, options) => {
|
|
604
618
|
const meta = readMeta();
|
|
605
619
|
const extras = { ...(meta.extraRepos || {}) };
|
|
@@ -637,7 +651,36 @@ export function registerRepoCommands(program) {
|
|
|
637
651
|
ensureAgentsDir();
|
|
638
652
|
const targetDir = getExtraRepoDir(alias);
|
|
639
653
|
if (fs.existsSync(targetDir)) {
|
|
640
|
-
|
|
654
|
+
// Adopt an existing checkout instead of hard-erroring. The common case:
|
|
655
|
+
// the user already cloned ~/.agents-<alias> by hand, then ran `repos add`
|
|
656
|
+
// and hit a dead end — which forced a second, inconsistent install method.
|
|
657
|
+
// If it's already a git repo whose origin matches the requested source,
|
|
658
|
+
// register it in place (no re-clone); adopt a mismatched/remoteless repo
|
|
659
|
+
// only with an explicit --adopt.
|
|
660
|
+
if (isGitRepo(targetDir)) {
|
|
661
|
+
const existingUrl = await getRemoteUrl(targetDir);
|
|
662
|
+
const matches = sameGitRemote(existingUrl, parsed.url);
|
|
663
|
+
if (matches || options.adopt) {
|
|
664
|
+
const log = await simpleGit(targetDir).log({ maxCount: 1 }).catch(() => null);
|
|
665
|
+
const commit = log?.latest?.hash.slice(0, 8) || 'unknown';
|
|
666
|
+
extras[alias] = { url: parsed.url, path: targetDir, enabled: true };
|
|
667
|
+
updateMeta({ extraRepos: extras });
|
|
668
|
+
syncExtraAliasAcrossVersions(alias, true);
|
|
669
|
+
syncMarketplacesForDefaults();
|
|
670
|
+
console.log(chalk.green(`Adopted existing repo "${alias}" -> ${targetDir} (${commit})`));
|
|
671
|
+
if (!matches) {
|
|
672
|
+
console.log(chalk.gray(` note: its origin is ${existingUrl ?? '(none)'}, not ${parsed.url} — adopted via --adopt.`));
|
|
673
|
+
}
|
|
674
|
+
console.log(chalk.gray(`Skills and commands from this repo will be picked up the next time you launch any agent.`));
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
console.log(chalk.red(`Directory already exists and is a different repo: ${targetDir}`));
|
|
678
|
+
console.log(chalk.gray(` its origin is ${existingUrl ?? '(none)'}, not ${parsed.url}.`));
|
|
679
|
+
console.log(chalk.gray(' Adopt it anyway with --adopt, or pick a different alias with --as.'));
|
|
680
|
+
process.exitCode = 1;
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
console.log(chalk.red(`Directory already exists (and is not a git repo): ${targetDir}`));
|
|
641
684
|
console.log(chalk.gray('Remove it manually or pick a different alias with --as.'));
|
|
642
685
|
process.exitCode = 1;
|
|
643
686
|
return;
|
|
@@ -676,6 +719,7 @@ export function registerRepoCommands(program) {
|
|
|
676
719
|
.alias('ls')
|
|
677
720
|
.description('Show all repos with resource-level sync (skills/commands/plugins to pull or push) and local changes.')
|
|
678
721
|
.option('-v, --verbose', 'Full per-resource detail in a table, regardless of terminal width')
|
|
722
|
+
.option('--json', 'machine-readable JSON output')
|
|
679
723
|
.action(async (alias, options) => {
|
|
680
724
|
await listRepos(alias, options);
|
|
681
725
|
});
|
|
@@ -951,6 +995,7 @@ export function registerRepoCommands(program) {
|
|
|
951
995
|
.command('status [alias]', { hidden: true })
|
|
952
996
|
.description('Alias of `list` (kept for muscle memory).')
|
|
953
997
|
.option('-v, --verbose', 'Full per-resource detail in a table, regardless of terminal width')
|
|
998
|
+
.option('--json', 'machine-readable JSON output')
|
|
954
999
|
.action(async (alias, options) => {
|
|
955
1000
|
await listRepos(alias, options);
|
|
956
1001
|
});
|
|
@@ -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>;
|