@phnx-labs/agents-cli 1.20.66 → 1.20.68
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 +88 -0
- package/README.md +3 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/check.js +96 -2
- package/dist/commands/doctor.js +37 -9
- package/dist/commands/exec.js +45 -1
- package/dist/commands/plugins.js +11 -2
- package/dist/commands/repo.js +47 -2
- package/dist/commands/ssh.js +103 -2
- package/dist/commands/teams.d.ts +11 -0
- package/dist/commands/teams.js +40 -2
- package/dist/commands/view.d.ts +1 -0
- package/dist/commands/view.js +12 -8
- package/dist/lib/agents.d.ts +11 -5
- package/dist/lib/agents.js +35 -24
- 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/daemon.d.ts +2 -2
- package/dist/lib/daemon.js +8 -89
- 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/exec.d.ts +15 -0
- package/dist/lib/exec.js +31 -6
- 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/resources/mcp.js +20 -342
- package/dist/lib/routines.d.ts +13 -0
- package/dist/lib/routines.js +21 -0
- package/dist/lib/runner.js +24 -47
- package/dist/lib/secrets/agent.js +11 -15
- 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/types.d.ts +10 -0
- package/dist/lib/usage.d.ts +17 -3
- package/dist/lib/usage.js +63 -16
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,93 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.20.68
|
|
4
|
+
|
|
5
|
+
- **MCP resource handler now syncs project-level agent configs alongside user-level configs (RUSH-671).** `McpHandler.sync` previously wrote resolved MCP servers only to the version-home (user-level) config path. It now also writes project-layer MCP servers to each agent CLI's project-level config path (e.g., `.mcp.json` for Claude, `.codex/config.toml` for Codex) so agent CLIs can discover project-scoped MCPs natively. User-level sync is unchanged. Source: `apps/cli/src/lib/resources/mcp.ts`, `apps/cli/src/lib/agents.ts` (`getProjectMcpConfigPath` exported), `apps/cli/src/lib/resources/mcp.test.ts`.
|
|
6
|
+
- **Production MCP sync path also writes project-level configs.** `installMcpServers` now merges project-layer servers into the agent's project-level config file, using a shared `writeMcpConfig` serializer with overwrite/merge modes. OpenClaw serialization is corrected to nest under `mcp.servers`, matching the existing reader; Grok/OpenClaw user-level configs are written directly with merge mode so multiple servers don't clobber each other; and `installMcpServers` only reports `applied` for agents it actually wrote a config for. Source: `apps/cli/src/lib/mcp.ts`, `apps/cli/src/lib/mcp.test.ts`.
|
|
7
|
+
|
|
8
|
+
- **Accurate account tier and legible usage limits in `agents view`.** Each row's plan
|
|
9
|
+
tier is now derived from `organizationType` (Max/Pro/Team/Enterprise) instead of a
|
|
10
|
+
billingType guess that mislabelled every Max account as "Pro", and the redundant tier
|
|
11
|
+
badge next to the email is dropped for personal plans (multi-seat orgs keep their org
|
|
12
|
+
name, which is real identity). The compact `S:`/`W:` usage bars now show the exact
|
|
13
|
+
percentage and a compact reset hint (`S: ███░░ 58% (3d)`), a signed-in account whose
|
|
14
|
+
usage can't be fetched reads `usage unavailable` instead of a blank gauge, and a new
|
|
15
|
+
`agents view --refresh` (`-r`) forces a live usage refresh past the cache. Source:
|
|
16
|
+
`apps/cli/src/lib/agents.ts`, `apps/cli/src/lib/usage.ts`, `apps/cli/src/commands/view.ts`.
|
|
17
|
+
|
|
18
|
+
- **Fleet health status and drift gate.** `agents fleet status` now renders a fleet-wide warnings rollup plus a device matrix for reachability, resource headroom, sync drift, CLI readiness, and agents-cli version skew; `--json` emits the same report for scripts and `--strict` exits non-zero when any warning is present. `agents check --devices` now fans the existing drift gate across registered devices and exits non-zero when any device is drifted or unreachable. Source: `apps/cli/src/commands/ssh.ts`, `apps/cli/src/commands/check.ts`, `apps/cli/src/lib/devices/fleet.ts`, `apps/cli/src/lib/devices/health-report.ts`.
|
|
19
|
+
|
|
20
|
+
- **Login state per agent — `agents doctor` shows signed-in/logged-out, and `agents run` warns before launching into a logged-out account.** `agents doctor`'s **Agent CLIs** section now renders a `✓ signed in <account>` / `✗ logged out` badge per installed agent (also surfaced in `--json` under `signIn`), and an interactive `agents run <agent>` now prints a one-line stderr warning — `⚠ <agent> looks logged out — log in with: <cmd>. Launching anyway...` — when the account looks logged out, so you find out **before** the TUI opens instead of after typing a prompt and getting `/login` back. It is advisory (file-based `getAccountInfo`, no Keychain ACL prompt) and **never blocks**: skipped for `--json`/`--quiet`, when a rotation already picked a signed-in account, for `--host`/`--lease`, and via `--no-auth-check` / `AGENTS_NO_AUTH_CHECK=1`. `agents view`'s logged-out line now names the exact login command too. One shared badge/hint renderer keeps all three surfaces consistent across claude, codex, kimi, grok, opencode, and gemini. Source: `apps/cli/src/lib/signin-badge.ts` (+ `signin-badge.test.ts`), `apps/cli/src/commands/exec.ts`, `apps/cli/src/commands/doctor.ts`, `apps/cli/src/commands/view.ts`.
|
|
21
|
+
|
|
22
|
+
- **`agents repos add` adopts an existing checkout instead of dead-ending.** When
|
|
23
|
+
the target `~/.agents-<alias>/` already holds a git repo whose origin matches the
|
|
24
|
+
requested source, `repos add` now registers it in place (no re-clone) rather than
|
|
25
|
+
erroring `Directory already exists`. A repo with a *different* origin is left
|
|
26
|
+
untouched unless you pass `--adopt`. This removes the trap that forced a second,
|
|
27
|
+
inconsistent install method when a repo had been cloned by hand. Remote matching
|
|
28
|
+
is transport-agnostic (SSH and HTTPS forms of the same repo compare equal). Source:
|
|
29
|
+
`apps/cli/src/commands/repo.ts`, `apps/cli/src/lib/git.ts`.
|
|
30
|
+
|
|
31
|
+
- **`--json` for `repos list` and `plugins list`.** Both list commands now emit
|
|
32
|
+
machine-readable JSON with `--json`, matching the `repos view --json` /
|
|
33
|
+
`plugins marketplaces --json` that already existed — so an agent can enumerate
|
|
34
|
+
registered repos (with per-repo sync/drift state) and installed plugins (with
|
|
35
|
+
per-agent-version sync targets) without scraping the human table. Source:
|
|
36
|
+
`apps/cli/src/commands/repo.ts`, `apps/cli/src/commands/plugins.ts`.
|
|
37
|
+
|
|
38
|
+
- **`teams add --remote-cwd` now fails loud instead of silently doing nothing.** The
|
|
39
|
+
flag rides the shared `--host` option family but `teams add` treats
|
|
40
|
+
`--host`/`--device` as placement and never reads it, so passing it used to be a
|
|
41
|
+
silent no-op that misled you into thinking it set the teammate's repo path. It is
|
|
42
|
+
now rejected with guidance (place with `--device`, set the code with the team's
|
|
43
|
+
`--repo`, one team per repo). The shared `--remote-cwd` help also warns that a
|
|
44
|
+
local `~` expands on your machine, not the remote host — pass a single-quoted
|
|
45
|
+
`'$HOME/…'` path or a valid remote absolute path. Teams docs + skill lead with
|
|
46
|
+
this. Source: `apps/cli/src/commands/teams.ts` (`remoteCwdOnAddError`),
|
|
47
|
+
`apps/cli/src/lib/hosts/option.ts`, `apps/cli/docs/teams.md`, `skills/teams/SKILL.md`.
|
|
48
|
+
|
|
49
|
+
- **No more CLIXML blobs from Windows hosts.** A remote `agents …` invocation
|
|
50
|
+
routed to a Windows box (`--host win-mini`, `agents doctor --devices`,
|
|
51
|
+
`agents fleet status`) no longer comes back wrapped in a raw `#< CLIXML <Objs …>`
|
|
52
|
+
envelope. PowerShell 5.1 serializes its progress stream ("Preparing modules for
|
|
53
|
+
first use.") to CLIXML when stderr is a captured pipe rather than a console; the
|
|
54
|
+
Windows command builder now silences that stream (`$ProgressPreference =
|
|
55
|
+
'SilentlyContinue'`) so failures read as plain text for humans and the JSON
|
|
56
|
+
parsers that consume the output. Source: `apps/cli/src/lib/hosts/remote-cmd.ts`.
|
|
57
|
+
|
|
58
|
+
## 1.20.67
|
|
59
|
+
|
|
60
|
+
- **Interactive session browser — `agents sessions --active` and a bare `agents sessions`
|
|
61
|
+
now open a live, filterable picker on a TTY (RUSH-1802).** One canonical filter driven by
|
|
62
|
+
single keys, re-pulled across the fleet as you toggle: `s` search, `r` running-only, `c`
|
|
63
|
+
teams, `a` agent (cycles), `d` device (cycles), `p` this-repo↔all-dirs, `w` time window;
|
|
64
|
+
filters **stack** (AND together) and the active set shows in the header, with a live
|
|
65
|
+
preview of the highlighted row and `⏎` to resume/attach via the existing dispatch. Every
|
|
66
|
+
hotkey mirrors a flag, so the view is reproducible as a command — `y` copies (and
|
|
67
|
+
`--print-cmd` prints) the exact `ag sessions …` line the filters map to, bridging the
|
|
68
|
+
human picker and the agent/script flag surface. The interactive front-end is TTY-only:
|
|
69
|
+
`--json`, a pipe, or the new `--no-interactive` keep the existing static listing verbatim,
|
|
70
|
+
so scripts and headless agents are unchanged. Adds `-p` as the short form of `--project`,
|
|
71
|
+
`--print-cmd`, `--preview` (`agents sessions <id> --preview` prints the compact digest
|
|
72
|
+
without the pager), and `--no-interactive`. Built on a new async-refetch `dynamicPicker`
|
|
73
|
+
variant that reuses the existing render/pagination/preview machinery, the fleet SSH
|
|
74
|
+
fan-out, and the resume/focus path. Source: `apps/cli/src/lib/picker.ts` (`dynamicPicker`),
|
|
75
|
+
`apps/cli/src/commands/sessions-browser.ts` (+ `sessions-browser.test.ts`),
|
|
76
|
+
`apps/cli/src/commands/sessions.ts`.
|
|
77
|
+
|
|
78
|
+
- **kimi/grok headless `--mode plan` now auto-downgrades to `auto` instead of
|
|
79
|
+
crashing or stalling (RUSH-1810).** kimi's headless `-p` refuses to combine with
|
|
80
|
+
`--plan` (it hard-failed at spawn) and grok's `--permission-mode plan` silently
|
|
81
|
+
stalls a headless run at its ExitPlanMode gate. Both now model this honestly with
|
|
82
|
+
a `capabilities.headlessPlan: false` flag: a headless plan request degrades to
|
|
83
|
+
`auto` (kimi `-p` auto-runs; grok maps `auto`→`edit`) with a one-line stderr
|
|
84
|
+
warning, mirroring the graceful plan→edit degrade cursor/antigravity already get.
|
|
85
|
+
Interactive plan is unchanged, and claude/codex/droid/opencode keep read-only
|
|
86
|
+
plan headless. The same downgrade covers `agents run`, `agents teams add`
|
|
87
|
+
teammates, and routine jobs. Source: `apps/cli/src/lib/exec.ts`
|
|
88
|
+
(`resolveHeadlessMode`), `apps/cli/src/lib/runner.ts`, `apps/cli/src/lib/agents.ts`,
|
|
89
|
+
`apps/cli/src/lib/types.ts`.
|
|
90
|
+
|
|
3
91
|
## 1.20.66
|
|
4
92
|
|
|
5
93
|
- **Fix (`agents monitors`, RUSH-1782 follow-up): `--watch-device` no longer silently watches the local machine on a bad name.** An unregistered or mistyped `--watch-device` name is now rejected at `add` time (same registry gate as `--device`/`--devices`), and the device source evaluator returns an explicit `device not registered` observation instead of falling back to local stats if a watched device is removed later — closing a "monitors the wrong box, silently" gap. The rate-limit firehose trip now also writes a fire record (`ok:false, error:'rate limited'`), so `agents monitors runs` reflects the auto-pause that `view`'s `last fired` already showed. Adds `sources/device.test.ts`. Source: `apps/cli/src/lib/monitors/sources/device.ts`, `apps/cli/src/commands/monitors.ts`, `apps/cli/src/lib/monitors/engine.ts`.
|
package/README.md
CHANGED
|
@@ -424,6 +424,9 @@ agents sync --host gpu-box # make the remote machine current
|
|
|
424
424
|
agents doctor --devices # readiness matrix for every registered device
|
|
425
425
|
agents doctor --devices --json # machine-readable fleet readiness
|
|
426
426
|
agents doctor --device mac-mini # same matrix, scoped to one device
|
|
427
|
+
agents fleet status # warnings rollup + health/sync/version matrix
|
|
428
|
+
agents fleet status --json --strict # scriptable fleet health gate
|
|
429
|
+
agents check --devices # CI drift gate across every registered device
|
|
427
430
|
|
|
428
431
|
# Your Tailscale fleet, auto-discovered
|
|
429
432
|
agents devices sync # ingest `tailscale status`
|
package/dist/bin/agents
CHANGED
|
Binary file
|
package/dist/commands/check.js
CHANGED
|
@@ -2,6 +2,11 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { AGENTS, ALL_AGENT_IDS } from '../lib/agents.js';
|
|
3
3
|
import { setHelpSections } from '../lib/help.js';
|
|
4
4
|
import { computeDrift } from '../lib/drift.js';
|
|
5
|
+
import { loadDevices } from '../lib/devices/registry.js';
|
|
6
|
+
import { fanOutDevices, planFleetTargets, remoteFleetTargets } from '../lib/devices/fleet.js';
|
|
7
|
+
import { machineId } from '../lib/session/sync/config.js';
|
|
8
|
+
import { buildRemoteAgentsInvocation } from '../lib/hosts/remote-cmd.js';
|
|
9
|
+
import { sshExecAsync } from '../lib/ssh-exec.js';
|
|
5
10
|
const AGENT_NAMES = Object.fromEntries(ALL_AGENT_IDS.map((id) => [id, AGENTS[id].name]));
|
|
6
11
|
function label(row) {
|
|
7
12
|
return `${AGENT_NAMES[row.agent] || row.agent}@${row.version}`;
|
|
@@ -12,7 +17,8 @@ export function registerCheckCommand(program) {
|
|
|
12
17
|
.description('CI drift gate: exit non-zero when any installed version is out of sync (stale or never-synced), zero when clean.')
|
|
13
18
|
.option('--json', 'Output machine-readable JSON')
|
|
14
19
|
.option('-q, --quiet', 'Suppress per-version lines; print only the one-line verdict')
|
|
15
|
-
.option('--cwd <path>', 'Resolution cwd for project layer detection (default: process.cwd())')
|
|
20
|
+
.option('--cwd <path>', 'Resolution cwd for project layer detection (default: process.cwd())')
|
|
21
|
+
.option('--devices', 'Run the drift gate across every registered online device');
|
|
16
22
|
setHelpSections(checkCmd, {
|
|
17
23
|
examples: `
|
|
18
24
|
# Fail the build if anything drifted (exit 1), pass if clean (exit 0)
|
|
@@ -24,12 +30,19 @@ export function registerCheckCommand(program) {
|
|
|
24
30
|
# Machine-readable, for scripting
|
|
25
31
|
agents check --json
|
|
26
32
|
|
|
33
|
+
# Gate every registered device
|
|
34
|
+
agents check --devices
|
|
35
|
+
|
|
27
36
|
# Gate in a CI step
|
|
28
37
|
agents check || { echo "resources drifted — run 'agents doctor --fix'"; exit 1; }
|
|
29
38
|
`,
|
|
30
39
|
});
|
|
31
|
-
checkCmd.action((opts) => {
|
|
40
|
+
checkCmd.action(async (opts) => {
|
|
32
41
|
const cwd = opts.cwd ? opts.cwd : process.cwd();
|
|
42
|
+
if (opts.devices) {
|
|
43
|
+
await runDevicesCheck(opts, cwd);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
33
46
|
const drift = computeDrift(cwd);
|
|
34
47
|
if (opts.json) {
|
|
35
48
|
console.log(JSON.stringify({
|
|
@@ -82,3 +95,84 @@ export function registerCheckCommand(program) {
|
|
|
82
95
|
process.exit(1);
|
|
83
96
|
});
|
|
84
97
|
}
|
|
98
|
+
function checkPayload(device, drift) {
|
|
99
|
+
return {
|
|
100
|
+
device,
|
|
101
|
+
hasDrift: drift.hasDrift,
|
|
102
|
+
stale: drift.staleCount,
|
|
103
|
+
neverSynced: drift.neverSyncedCount,
|
|
104
|
+
orphanVersions: drift.orphanVersionCount,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
async function probeDeviceCheck(target) {
|
|
108
|
+
const isWin = /^win/i.test((target.platform ?? '').trim());
|
|
109
|
+
const remoteCmd = buildRemoteAgentsInvocation(['check', '--json'], undefined, isWin ? 'windows' : undefined, isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' });
|
|
110
|
+
const res = await sshExecAsync(target.name, remoteCmd, { timeoutMs: 30000, multiplex: true });
|
|
111
|
+
if (res.code !== 0 && !res.stdout.trim()) {
|
|
112
|
+
throw new Error(res.timedOut ? 'timed out' : (res.stderr.trim() || `exit ${res.code ?? 'unknown'}`));
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const parsed = JSON.parse(res.stdout);
|
|
116
|
+
return {
|
|
117
|
+
device: target.name,
|
|
118
|
+
hasDrift: Boolean(parsed.hasDrift),
|
|
119
|
+
stale: parsed.stale ?? 0,
|
|
120
|
+
neverSynced: parsed.neverSynced ?? 0,
|
|
121
|
+
orphanVersions: parsed.orphanVersions ?? 0,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
throw new Error(`invalid JSON (${err?.message ?? 'parse error'})`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async function runDevicesCheck(opts, cwd) {
|
|
129
|
+
const registry = await loadDevices();
|
|
130
|
+
const self = machineId();
|
|
131
|
+
const planned = planFleetTargets(registry);
|
|
132
|
+
const local = checkPayload(self, computeDrift(cwd));
|
|
133
|
+
const remoteTargets = remoteFleetTargets(planned, self)
|
|
134
|
+
.map((t) => ({
|
|
135
|
+
name: t.device.name,
|
|
136
|
+
platform: t.device.platform,
|
|
137
|
+
skip: t.skip,
|
|
138
|
+
}));
|
|
139
|
+
const remote = await fanOutDevices(remoteTargets, probeDeviceCheck);
|
|
140
|
+
const devices = [local];
|
|
141
|
+
for (const result of remote) {
|
|
142
|
+
if (result.status === 'ok' && result.value) {
|
|
143
|
+
devices.push(result.value);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
devices.push({
|
|
147
|
+
device: result.name,
|
|
148
|
+
hasDrift: true,
|
|
149
|
+
stale: 0,
|
|
150
|
+
neverSynced: 0,
|
|
151
|
+
orphanVersions: 0,
|
|
152
|
+
error: result.error ?? String(result.reason ?? 'skipped'),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const hasDrift = devices.some((d) => d.hasDrift || d.error);
|
|
157
|
+
if (opts.json) {
|
|
158
|
+
console.log(JSON.stringify({ hasDrift, devices }, null, 2));
|
|
159
|
+
process.exit(hasDrift ? 1 : 0);
|
|
160
|
+
}
|
|
161
|
+
if (!hasDrift) {
|
|
162
|
+
console.log(chalk.green('ok') + chalk.gray(` ${devices.length} device(s) in sync`));
|
|
163
|
+
process.exit(0);
|
|
164
|
+
}
|
|
165
|
+
console.error(chalk.red('drift') + chalk.gray(` ${devices.filter((d) => d.hasDrift || d.error).length} of ${devices.length} device(s)`));
|
|
166
|
+
if (!opts.quiet) {
|
|
167
|
+
for (const d of devices) {
|
|
168
|
+
if (!d.hasDrift && !d.error)
|
|
169
|
+
continue;
|
|
170
|
+
const detail = d.error
|
|
171
|
+
? d.error
|
|
172
|
+
: [`${d.stale} stale`, `${d.neverSynced} never-synced`].filter((p) => !p.startsWith('0 ')).join(', ');
|
|
173
|
+
console.error(` ${chalk.yellow(d.device.padEnd(18))} ${detail || 'drift'}`);
|
|
174
|
+
}
|
|
175
|
+
console.error(chalk.gray('\nReconcile each device with `agents doctor --fix` or `agents repo pull user`.'));
|
|
176
|
+
}
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
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.js
CHANGED
|
@@ -259,6 +259,7 @@ export function registerRunCommand(program) {
|
|
|
259
259
|
.option('--json', 'Stream events as JSON lines (for parsing by other tools)')
|
|
260
260
|
.option('--quiet', 'Suppress preamble (rotation banner, "Running:" line). Useful when piping JSON events to a parser.', false)
|
|
261
261
|
.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)
|
|
262
|
+
.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
263
|
.option('-i, --interactive', 'Force interactive mode even when a prompt is provided. Mutually exclusive with --headless.')
|
|
263
264
|
.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
265
|
.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 +282,7 @@ export function registerRunCommand(program) {
|
|
|
281
282
|
.option('--interval <dur>', 'Loop delay between iterations ("0" back-to-back, "30m" paces). Loop only.')
|
|
282
283
|
.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
284
|
.option('--device <name>', 'Alias of --host: offload this run onto a registered device (from `agents devices`).')
|
|
284
|
-
.option('--remote-cwd <dir>',
|
|
285
|
+
.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
286
|
.option('--no-follow', 'With --host, dispatch detached and return immediately (track via `agents hosts ps/logs`).')
|
|
286
287
|
.option('--any', 'With --host <cap> (a capability tag), pick any matching host instead of erroring when several match.')
|
|
287
288
|
.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.')
|
|
@@ -328,6 +329,12 @@ export function registerRunCommand(program) {
|
|
|
328
329
|
skip bypass every permission prompt (dangerously-skip-permissions)
|
|
329
330
|
Legacy 'full' is silently rewritten to 'skip'.
|
|
330
331
|
|
|
332
|
+
Headless plan support (a prompt makes the run headless):
|
|
333
|
+
plan works headless on claude, codex, droid, opencode.
|
|
334
|
+
kimi, grok, cursor, antigravity have no headless plan mode — a headless
|
|
335
|
+
--mode plan auto-downgrades to --mode auto (with a stderr warning).
|
|
336
|
+
Interactive plan (omit the prompt) works everywhere it is listed.
|
|
337
|
+
|
|
331
338
|
Run strategy (set via --strategy or run.<agent>.strategy in agents.yaml):
|
|
332
339
|
pinned use the workspace/global pinned version
|
|
333
340
|
available use pinned if it can run right now; otherwise switch to another signed-in version
|
|
@@ -1344,6 +1351,43 @@ export function registerRunCommand(program) {
|
|
|
1344
1351
|
}
|
|
1345
1352
|
}
|
|
1346
1353
|
const defaultVersion = version ?? resolveVersion(agent, cwd);
|
|
1354
|
+
// Login preflight (advisory, warn + continue). On a local INTERACTIVE
|
|
1355
|
+
// launch, probe whether this agent's account has a credential and print a
|
|
1356
|
+
// one-line warning if it looks logged out — so you find out BEFORE the TUI
|
|
1357
|
+
// opens, not after typing a prompt and getting "/login" back. Uses the same
|
|
1358
|
+
// account-global probe as `checkCliSignedIn` / `agents doctor`
|
|
1359
|
+
// (getAccountInfo with no home): file-based, no Keychain ACL prompt, and
|
|
1360
|
+
// correct for HOME-global credential agents (grok/codex) where a per-version
|
|
1361
|
+
// home would false-negative. It can still false-negative for opaque
|
|
1362
|
+
// credentials, so this NEVER blocks — it warns and launches anyway. Skipped
|
|
1363
|
+
// for --json/--quiet, when a rotation already picked a signed-in account,
|
|
1364
|
+
// and via --no-auth-check / AGENTS_NO_AUTH_CHECK=1. (--host/--lease return
|
|
1365
|
+
// earlier.)
|
|
1366
|
+
{
|
|
1367
|
+
const { shouldCheckLoginBeforeLaunch, loginHint } = await import('../lib/signin-badge.js');
|
|
1368
|
+
const preflight = shouldCheckLoginBeforeLaunch({
|
|
1369
|
+
interactive: options.interactive,
|
|
1370
|
+
forceInteractive, // a resumed interactive session (e.g. `run kimi --resume`) opens the TUI too
|
|
1371
|
+
headless: options.headless,
|
|
1372
|
+
hasPrompt: prompt !== undefined,
|
|
1373
|
+
json: options.json,
|
|
1374
|
+
quiet: options.quiet,
|
|
1375
|
+
authCheckDisabled: options.authCheck === false || process.env.AGENTS_NO_AUTH_CHECK === '1',
|
|
1376
|
+
rotated: !!rotationResult,
|
|
1377
|
+
});
|
|
1378
|
+
if (preflight) {
|
|
1379
|
+
try {
|
|
1380
|
+
const { getAccountInfo } = await import('../lib/agents.js');
|
|
1381
|
+
const info = await getAccountInfo(agent);
|
|
1382
|
+
if (!info.signedIn) {
|
|
1383
|
+
process.stderr.write(chalk.yellow(`⚠ ${agent} looks logged out — log in with: ${loginHint(agent)}. Launching anyway...\n`));
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
catch {
|
|
1387
|
+
// Advisory only — a probe failure must never block a launch.
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1347
1391
|
const runDefaults = fromProfile
|
|
1348
1392
|
? { sources: {} }
|
|
1349
1393
|
: resolveRunDefaults(agent, defaultVersion, cwd);
|
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
|
});
|