@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/dist/commands/ssh.js
CHANGED
|
@@ -15,6 +15,7 @@ import * as os from 'os';
|
|
|
15
15
|
import * as path from 'path';
|
|
16
16
|
import chalk from 'chalk';
|
|
17
17
|
import ora from 'ora';
|
|
18
|
+
import { getCliVersion } from '../lib/version.js';
|
|
18
19
|
import { readAndResolveBundleEnv, isHeadlessSecretsContext } from '../lib/secrets/bundles.js';
|
|
19
20
|
import { machineId } from '../lib/session/sync/config.js';
|
|
20
21
|
import { addIgnored, getDevice, loadDevices, loadIgnored, removeDevice, removeIgnored, upsertDevice, } from '../lib/devices/registry.js';
|
|
@@ -28,8 +29,13 @@ import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
|
28
29
|
import { hostNameFor, renderSshConfig } from '../lib/devices/ssh-config.js';
|
|
29
30
|
import { ASKPASS_BUNDLE_ENV, ASKPASS_KEY_ENV, buildSshInvocation, writeAskpassShim, } from '../lib/devices/connect.js';
|
|
30
31
|
import { ensureManagedKnownHostsDir, isHostPinned } from '../lib/devices/known-hosts.js';
|
|
31
|
-
import { planFleetTargets, runFleet, skipLabel, upgradeCommand, } from '../lib/devices/fleet.js';
|
|
32
|
-
import { fleetCapacity, fmtBytes, headroom, probeFleetStats, } from '../lib/devices/health.js';
|
|
32
|
+
import { fanOutDevices, planFleetTargets, remoteFleetTargets, runFleet, skipLabel, upgradeCommand, } from '../lib/devices/fleet.js';
|
|
33
|
+
import { fleetCapacity, fmtBytes, headroom, probeLocalStats, probeFleetStats, } from '../lib/devices/health.js';
|
|
34
|
+
import { buildFleetHealthReport, renderFleetMatrix, renderFleetWarnings, } from '../lib/devices/health-report.js';
|
|
35
|
+
import { checkSyncStatus, countOrphans } from '../lib/drift.js';
|
|
36
|
+
import { checkAllClis } from '../lib/teams/agents.js';
|
|
37
|
+
import { buildRemoteAgentsInvocation } from '../lib/hosts/remote-cmd.js';
|
|
38
|
+
import { sshExecAsync } from '../lib/ssh-exec.js';
|
|
33
39
|
/** One-line summary of a device for `list`. `isSelf` marks the machine this
|
|
34
40
|
* command is running on so it stands out from the rest of the tailnet. */
|
|
35
41
|
function deviceSummary(d, isSelf = false) {
|
|
@@ -219,6 +225,92 @@ function printFleetResults(results) {
|
|
|
219
225
|
if (failed > 0)
|
|
220
226
|
process.exitCode = 1;
|
|
221
227
|
}
|
|
228
|
+
function localHealthRow(self, stats) {
|
|
229
|
+
return {
|
|
230
|
+
name: self,
|
|
231
|
+
platform: process.platform === 'darwin' ? 'macos' : process.platform,
|
|
232
|
+
version: getCliVersion(),
|
|
233
|
+
stats,
|
|
234
|
+
clis: checkAllClis(),
|
|
235
|
+
sync: checkSyncStatus(process.cwd()),
|
|
236
|
+
orphans: countOrphans(),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
async function probeRemoteHealth(target) {
|
|
240
|
+
const isWin = /^win/i.test((target.platform ?? '').trim());
|
|
241
|
+
const env = isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' };
|
|
242
|
+
const versionCmd = buildRemoteAgentsInvocation(['--version'], undefined, isWin ? 'windows' : undefined, env);
|
|
243
|
+
const versionRes = await sshExecAsync(target.name, versionCmd, { timeoutMs: 15000, multiplex: true });
|
|
244
|
+
const version = versionRes.code === 0 ? versionRes.stdout.trim().split(/\s+/)[0] || null : null;
|
|
245
|
+
const doctorCmd = buildRemoteAgentsInvocation(['doctor', '--json'], undefined, isWin ? 'windows' : undefined, env);
|
|
246
|
+
const doctorRes = await sshExecAsync(target.name, doctorCmd, { timeoutMs: 30000, multiplex: true });
|
|
247
|
+
if (doctorRes.code !== 0) {
|
|
248
|
+
throw new Error(doctorRes.timedOut ? 'timed out' : (doctorRes.stderr.trim() || `exit ${doctorRes.code ?? 'unknown'}`));
|
|
249
|
+
}
|
|
250
|
+
const parsed = JSON.parse(doctorRes.stdout);
|
|
251
|
+
return {
|
|
252
|
+
version,
|
|
253
|
+
clis: parsed.clis ?? {},
|
|
254
|
+
sync: parsed.sync ?? [],
|
|
255
|
+
orphans: parsed.orphans ?? [],
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
async function runFleetStatus(opts) {
|
|
259
|
+
const reg = await loadDevices();
|
|
260
|
+
const self = machineId();
|
|
261
|
+
const planned = planFleetTargets(reg);
|
|
262
|
+
const probeable = planned.filter((t) => !t.skip).map((t) => t.device);
|
|
263
|
+
const statsMap = opts.stats === false
|
|
264
|
+
? new Map()
|
|
265
|
+
: await probeFleetStats(probeable, { selfName: self });
|
|
266
|
+
if (opts.stats !== false && !statsMap.has(self)) {
|
|
267
|
+
statsMap.set(self, await probeLocalStats(self));
|
|
268
|
+
}
|
|
269
|
+
const rows = [localHealthRow(self, statsMap.get(self))];
|
|
270
|
+
const remoteTargets = remoteFleetTargets(planned, self)
|
|
271
|
+
.map((t) => ({
|
|
272
|
+
name: t.device.name,
|
|
273
|
+
platform: t.device.platform,
|
|
274
|
+
skip: t.skip,
|
|
275
|
+
}));
|
|
276
|
+
const remote = await fanOutDevices(remoteTargets, probeRemoteHealth);
|
|
277
|
+
for (const result of remote) {
|
|
278
|
+
const profile = reg[result.name];
|
|
279
|
+
if (result.status === 'ok' && result.value) {
|
|
280
|
+
rows.push({
|
|
281
|
+
name: result.name,
|
|
282
|
+
platform: profile?.platform,
|
|
283
|
+
stats: statsMap.get(result.name),
|
|
284
|
+
...result.value,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
rows.push({
|
|
289
|
+
name: result.name,
|
|
290
|
+
platform: profile?.platform,
|
|
291
|
+
stats: statsMap.get(result.name),
|
|
292
|
+
skipped: result.reason ? String(result.reason) : undefined,
|
|
293
|
+
error: result.error,
|
|
294
|
+
clis: {},
|
|
295
|
+
sync: [],
|
|
296
|
+
orphans: [],
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const report = buildFleetHealthReport(rows);
|
|
301
|
+
if (opts.json) {
|
|
302
|
+
console.log(JSON.stringify(report, null, 2));
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
for (const line of renderFleetWarnings(report))
|
|
306
|
+
console.log(line);
|
|
307
|
+
console.log();
|
|
308
|
+
for (const line of renderFleetMatrix(report))
|
|
309
|
+
console.log(line);
|
|
310
|
+
}
|
|
311
|
+
if (opts.strict && report.hasWarnings)
|
|
312
|
+
process.exitCode = 1;
|
|
313
|
+
}
|
|
222
314
|
/** Register the `agents devices` command tree (also aliased as `fleet`). */
|
|
223
315
|
function registerDevicesCommands(program) {
|
|
224
316
|
const devicesCmd = program
|
|
@@ -346,6 +438,15 @@ Typical workflow:
|
|
|
346
438
|
for (const line of renderDeviceTable(reg, names, self, statsMap, opts.full))
|
|
347
439
|
console.log(line);
|
|
348
440
|
});
|
|
441
|
+
devicesCmd
|
|
442
|
+
.command('status')
|
|
443
|
+
.description('Show fleet health: warnings rollup, per-device sync drift, CLI readiness, version skew, and resource headroom.')
|
|
444
|
+
.option('--json', 'output machine-readable JSON')
|
|
445
|
+
.option('--strict', 'exit non-zero when any device has drift or is unreachable')
|
|
446
|
+
.option('--no-stats', 'skip the live resource probe')
|
|
447
|
+
.action(async (opts) => {
|
|
448
|
+
await runFleetStatus(opts);
|
|
449
|
+
});
|
|
349
450
|
devicesCmd
|
|
350
451
|
.command('show <name>')
|
|
351
452
|
.description('Show the full profile for one device.')
|
package/dist/commands/teams.d.ts
CHANGED
|
@@ -32,5 +32,16 @@ export declare function decideTeamMessageRoute(status: AgentStatus, hasMessage:
|
|
|
32
32
|
* the teammate itself so we don't need the original --cloud CLI args.
|
|
33
33
|
*/
|
|
34
34
|
export declare function wireCloudDispatcher(mgr: AgentManager): void;
|
|
35
|
+
/**
|
|
36
|
+
* `teams add --remote-cwd` is a no-op trap. `--remote-cwd` comes from the shared
|
|
37
|
+
* `--host` option family (option.ts) and is meaningful for commands that *route*
|
|
38
|
+
* to a host, but `teams add` special-cases `--host`/`--device` as PLACEMENT, so
|
|
39
|
+
* the flag is never read — a teammate's directory is the team's repo plus its
|
|
40
|
+
* `--worktree`, not a path passed here. Silently ignoring it misleads you into
|
|
41
|
+
* thinking it set the teammate's repo path (the exact wrong model that turns one
|
|
42
|
+
* team into a teardown-and-rebuild). Reject with guidance instead. Exported for
|
|
43
|
+
* the unit test that pins the message.
|
|
44
|
+
*/
|
|
45
|
+
export declare function remoteCwdOnAddError(team: string): string;
|
|
35
46
|
/** Register the `agents teams` command tree (list, create, add, status, start, remove, disband, logs, doctor). */
|
|
36
47
|
export declare function registerTeamsCommands(program: Command): void;
|
package/dist/commands/teams.js
CHANGED
|
@@ -809,6 +809,23 @@ async function pickTeamOr(mgr, command) {
|
|
|
809
809
|
throw err;
|
|
810
810
|
}
|
|
811
811
|
}
|
|
812
|
+
/**
|
|
813
|
+
* `teams add --remote-cwd` is a no-op trap. `--remote-cwd` comes from the shared
|
|
814
|
+
* `--host` option family (option.ts) and is meaningful for commands that *route*
|
|
815
|
+
* to a host, but `teams add` special-cases `--host`/`--device` as PLACEMENT, so
|
|
816
|
+
* the flag is never read — a teammate's directory is the team's repo plus its
|
|
817
|
+
* `--worktree`, not a path passed here. Silently ignoring it misleads you into
|
|
818
|
+
* thinking it set the teammate's repo path (the exact wrong model that turns one
|
|
819
|
+
* team into a teardown-and-rebuild). Reject with guidance instead. Exported for
|
|
820
|
+
* the unit test that pins the message.
|
|
821
|
+
*/
|
|
822
|
+
export function remoteCwdOnAddError(team) {
|
|
823
|
+
return (`--remote-cwd has no effect on 'teams add' — it does not set a teammate's repo or directory.\n` +
|
|
824
|
+
` A teammate works in the team's repo plus its own --worktree:\n` +
|
|
825
|
+
` • Set the repo once, on the team: agents teams create ${team} --repo <url|path>\n` +
|
|
826
|
+
` • Place the teammate on a machine: agents teams add ${team} <agent> "<task>" --device <host> --worktree <name>\n` +
|
|
827
|
+
` (Remote worktrees fork from the host's freshly-fetched origin/<default> automatically.)`);
|
|
828
|
+
}
|
|
812
829
|
/** Register the `agents teams` command tree (list, create, add, status, start, remove, disband, logs, doctor). */
|
|
813
830
|
export function registerTeamsCommands(program) {
|
|
814
831
|
const teams = program
|
|
@@ -853,6 +870,20 @@ export function registerTeamsCommands(program) {
|
|
|
853
870
|
'<profile>' a profile from 'agents view' — runs through 'agents
|
|
854
871
|
run <profile>' with the profile's host harness
|
|
855
872
|
|
|
873
|
+
Placement & repos (the part people get wrong):
|
|
874
|
+
--remote-cwd does NOT place a teammate or set its repo — it is ignored on
|
|
875
|
+
'teams add' (and rejected, so you find out immediately). A teammate's
|
|
876
|
+
directory is the team's repo plus its --worktree:
|
|
877
|
+
--device <host> run THIS teammate on <host>
|
|
878
|
+
create --repo <r> ONE repo for the whole team (defaults to this
|
|
879
|
+
checkout's origin). Work spanning repos → one team
|
|
880
|
+
per repo — don't build a cross-repo team then rebuild.
|
|
881
|
+
With --enable-worktrees each teammate gets its own worktree + branch:
|
|
882
|
+
local teammate forks from your CURRENT local HEAD — no fetch, so
|
|
883
|
+
pull/sync the checkout first or it forks stale
|
|
884
|
+
remote (--device) forks from the freshly-fetched origin/<default> on
|
|
885
|
+
the host (no manual sync needed)
|
|
886
|
+
|
|
856
887
|
Short aliases:
|
|
857
888
|
teams c = create teams a = add teams s = status
|
|
858
889
|
teams rm = remove teams d = disband teams ls = list
|
|
@@ -978,7 +1009,7 @@ export function registerTeamsCommands(program) {
|
|
|
978
1009
|
.option('--use-worktree <path>', 'All teammates share this existing worktree path (mutually exclusive with --enable-worktrees)')
|
|
979
1010
|
.option('--devices <list>', 'Pool of machines this team may run teammates on (comma-separated). Enables distributed auto-scheduling.')
|
|
980
1011
|
.option('--hosts <list>', 'Alias for --devices.')
|
|
981
|
-
.option('--repo <urlOrPath>', 'How each device gets the code
|
|
1012
|
+
.option('--repo <urlOrPath>', 'How each remote (--device) teammate gets the code — ONE git URL/path for the whole team (existing checkout reused, else cloned). A team is single-repo; for work across repos, make one team per repo. Defaults to this checkout origin.')
|
|
982
1013
|
.option('--json', 'Output machine-readable JSON')
|
|
983
1014
|
.action(async (team, opts) => {
|
|
984
1015
|
try {
|
|
@@ -1059,7 +1090,7 @@ export function registerTeamsCommands(program) {
|
|
|
1059
1090
|
.alias('a')
|
|
1060
1091
|
.description("Add a teammate to work on a task. Runs in background; returns immediately. Use 'status' to check in.")
|
|
1061
1092
|
.option('-n, --name <name>', 'Friendly name for this teammate (e.g. alice). Required if using --after. Unique within team.')
|
|
1062
|
-
.option('-m, --mode <mode>', `Permissions: plan (read-only) | edit (can write files) | auto (smart classifier auto-approves safe ops) | skip (bypass all permission prompts). 'full' accepted as alias for skip.`, 'edit')
|
|
1093
|
+
.option('-m, --mode <mode>', `Permissions: plan (read-only) | edit (can write files) | auto (smart classifier auto-approves safe ops) | skip (bypass all permission prompts). 'full' accepted as alias for skip. Teammates run headless: plan works headless on claude/codex/droid/opencode; kimi/grok/cursor/antigravity have no headless plan mode and auto-downgrade a plan request to auto.`, 'edit')
|
|
1063
1094
|
.option('-e, --effort <effort>', `Reasoning intensity: ${VALID_EFFORTS.join('|')}`, 'medium')
|
|
1064
1095
|
.option('--model <model>', 'Override the effort tier and use this specific model (e.g. claude-opus-4-6)')
|
|
1065
1096
|
.option('--env <key=value>', 'Set an environment variable for this teammate (repeatable for multiple vars)', (val, prev) => [...prev, val], [])
|
|
@@ -1073,6 +1104,13 @@ export function registerTeamsCommands(program) {
|
|
|
1073
1104
|
.option('--force', "Skip the advisory 'may not be signed in' / 'account throttled' warnings")
|
|
1074
1105
|
.option('--json', 'Output machine-readable JSON')
|
|
1075
1106
|
.action(async (team, teammate, task, opts) => {
|
|
1107
|
+
// `--remote-cwd` rides the shared --host option family but is never read by
|
|
1108
|
+
// `teams add` (placement, not routing). Fail loud with guidance rather than
|
|
1109
|
+
// silently ignoring it — see remoteCwdOnAddError. `!== undefined` so even an
|
|
1110
|
+
// explicit empty value (`--remote-cwd ""`) is rejected, not silently dropped.
|
|
1111
|
+
if (opts.remoteCwd !== undefined) {
|
|
1112
|
+
die(remoteCwdOnAddError(team));
|
|
1113
|
+
}
|
|
1076
1114
|
if (!VALID_MODES.includes(opts.mode)) {
|
|
1077
1115
|
die(`Invalid mode '${opts.mode}'. Use one of: ${VALID_MODES.join(', ')}`);
|
|
1078
1116
|
}
|
package/dist/commands/view.d.ts
CHANGED
|
@@ -133,6 +133,7 @@ export declare function viewAction(agentArg?: string, options?: {
|
|
|
133
133
|
dryRun?: boolean;
|
|
134
134
|
resources?: string | boolean;
|
|
135
135
|
detailed?: boolean;
|
|
136
|
+
refresh?: boolean;
|
|
136
137
|
} & ViewSectionFilter): Promise<void>;
|
|
137
138
|
/** Register the `agents view` command. */
|
|
138
139
|
export declare function registerViewCommand(program: Command): void;
|
package/dist/commands/view.js
CHANGED
|
@@ -5,7 +5,8 @@ import ora from 'ora';
|
|
|
5
5
|
import * as fs from 'fs';
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import { AGENTS, ALL_AGENT_IDS, accountOrgBadge, getAllCliStates, getAccountInfo, resolveAgentName, formatAgentError, agentLabel, colorAgent, } from '../lib/agents.js';
|
|
8
|
-
import {
|
|
8
|
+
import { loginHint } from '../lib/signin-badge.js';
|
|
9
|
+
import { agentReportsUsage, deriveUsageStatusFromSnapshot, formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
|
|
9
10
|
import { readManifest } from '../lib/manifest.js';
|
|
10
11
|
import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, promptNewResourceSelection, syncResourcesToVersion, removeVersion, printTrashFooter, reconcileStaleLatestForAgent, isGlobalBinaryAgent, getLiveVersion, } from '../lib/versions.js';
|
|
11
12
|
import { ensureVersionedAliasCurrent, removeShim, } from '../lib/shims.js';
|
|
@@ -244,7 +245,7 @@ function renderHostClisSection(cwd) {
|
|
|
244
245
|
console.log(` ${chalk.red('error')} ${chalk.gray(err.file)}: ${chalk.gray(err.reason)}`);
|
|
245
246
|
}
|
|
246
247
|
}
|
|
247
|
-
async function showInstalledVersions(filterAgentId) {
|
|
248
|
+
async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
248
249
|
const spinnerText = filterAgentId
|
|
249
250
|
? `Checking ${agentLabel(filterAgentId)} agents...`
|
|
250
251
|
: 'Checking installed agents...';
|
|
@@ -326,7 +327,7 @@ async function showInstalledVersions(filterAgentId) {
|
|
|
326
327
|
cliVersion,
|
|
327
328
|
info,
|
|
328
329
|
})),
|
|
329
|
-
]);
|
|
330
|
+
], { forceRefresh: viewOpts?.forceRefresh });
|
|
330
331
|
const mergeCanonical = (info) => {
|
|
331
332
|
const key = getUsageLookupKey(info);
|
|
332
333
|
if (!key)
|
|
@@ -417,7 +418,8 @@ async function showInstalledVersions(filterAgentId) {
|
|
|
417
418
|
const info = rawInfo ? mergeCanonical(rawInfo) : undefined;
|
|
418
419
|
const usageKey = getUsageLookupKey(info);
|
|
419
420
|
const usageInfo = usageKey ? usageByKey.get(usageKey) : undefined;
|
|
420
|
-
const
|
|
421
|
+
const usageUnavailable = agentReportsUsage(agentId) && !!info?.signedIn && !usageInfo?.snapshot;
|
|
422
|
+
const usageStr = formatUsageSummary(info?.plan || null, usageInfo?.snapshot || null, maxPlanWidth, { unavailable: usageUnavailable });
|
|
421
423
|
maxUsageWidth = Math.max(maxUsageWidth, visibleWidth(usageStr));
|
|
422
424
|
const statusStr = formatUsageStatusBadge(info?.usageStatus);
|
|
423
425
|
maxStatusWidth = Math.max(maxStatusWidth, visibleWidth(statusStr));
|
|
@@ -457,7 +459,8 @@ async function showInstalledVersions(filterAgentId) {
|
|
|
457
459
|
const parts = [` ${label}`];
|
|
458
460
|
const hasEmail = !!vInfo?.email;
|
|
459
461
|
const signedIn = !!vInfo?.signedIn;
|
|
460
|
-
const
|
|
462
|
+
const usageUnavailable = agentReportsUsage(agentId) && signedIn && !usageInfo?.snapshot;
|
|
463
|
+
const usageStr = formatUsageSummary(vInfo?.plan || null, usageInfo?.snapshot || null, maxPlanWidth, { unavailable: usageUnavailable });
|
|
461
464
|
const hasUsage = usageStr.length > 0;
|
|
462
465
|
// Only show lastActive for versions with an actual logged-in account.
|
|
463
466
|
// Otherwise it reflects install time (misleading "just now" for fresh installs).
|
|
@@ -471,7 +474,7 @@ async function showInstalledVersions(filterAgentId) {
|
|
|
471
474
|
runDefaultBits.push(`model:${runDefaults.model}`);
|
|
472
475
|
if (!hasEmail && !hasUsage && !signedIn) {
|
|
473
476
|
// Installed but never signed in
|
|
474
|
-
parts.push(chalk.gray('(
|
|
477
|
+
parts.push(chalk.gray('(logged out — log in with: ' + loginHint(agentId) + ')'));
|
|
475
478
|
}
|
|
476
479
|
else {
|
|
477
480
|
if (hasEmail || hasUsage || hasActive || signedIn) {
|
|
@@ -1438,7 +1441,7 @@ export async function viewAction(agentArg, options) {
|
|
|
1438
1441
|
return;
|
|
1439
1442
|
}
|
|
1440
1443
|
// No argument: show all installed versions
|
|
1441
|
-
await showInstalledVersions();
|
|
1444
|
+
await showInstalledVersions(undefined, { forceRefresh: options?.refresh === true });
|
|
1442
1445
|
return;
|
|
1443
1446
|
}
|
|
1444
1447
|
// Parse agent@version syntax
|
|
@@ -1499,7 +1502,7 @@ export async function viewAction(agentArg, options) {
|
|
|
1499
1502
|
}
|
|
1500
1503
|
else {
|
|
1501
1504
|
// Just agent name: show versions for that agent
|
|
1502
|
-
await showInstalledVersions(agentId);
|
|
1505
|
+
await showInstalledVersions(agentId, { forceRefresh: options?.refresh === true });
|
|
1503
1506
|
}
|
|
1504
1507
|
}
|
|
1505
1508
|
/** Register the `agents view` command. */
|
|
@@ -1509,6 +1512,7 @@ export function registerViewCommand(program) {
|
|
|
1509
1512
|
.option('--json', 'Emit machine-readable JSON (version list, usage, signed-in status).')
|
|
1510
1513
|
.option('--resources [sections]', 'In --json mode, include each version\'s resources: "all" (default) or a comma list (skills,plugins,mcp,commands,workflows,memory,hooks). Implies --json.')
|
|
1511
1514
|
.option('--detailed', 'Include all resources in --json output (alias for --resources all). Implies --json.')
|
|
1515
|
+
.option('-r, --refresh', 'Force a live usage refresh, bypassing the cache (slower). Repopulates the S:/W: limit bars for every account whose token is reachable.')
|
|
1512
1516
|
.option('--prune', 'Remove older installed versions that share an account with a newer installed version. Skips the global default.')
|
|
1513
1517
|
.option('--dry-run', 'With --prune, show duplicate versions without deleting')
|
|
1514
1518
|
.option('-y, --yes', 'Skip the prune confirmation prompt.')
|
package/dist/lib/agents.d.ts
CHANGED
|
@@ -139,11 +139,13 @@ export interface AccountInfo {
|
|
|
139
139
|
*/
|
|
140
140
|
export declare function formatClaudeOrgLabel(orgType: string | null | undefined): string | null;
|
|
141
141
|
/**
|
|
142
|
-
* Short badge identifying which
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
142
|
+
* Short badge identifying which ORG an account belongs to — "Turing Labs" for a
|
|
143
|
+
* multi-seat Team/Enterprise org, whose name is real identity that disambiguates
|
|
144
|
+
* a Team seat from a same-email personal plan. Returns null for personal plans
|
|
145
|
+
* (Max/Pro/Free): the tier label now lives in the aligned plan column, so a badge
|
|
146
|
+
* would only duplicate it, and a personal org's name is auto-generated boilerplate
|
|
147
|
+
* ("<email>'s Organization"), not identity. Also null when the account carries no
|
|
148
|
+
* organizationType (signed out, non-Claude agents, configs predating the field).
|
|
147
149
|
*/
|
|
148
150
|
export declare function accountOrgBadge(info?: Pick<AccountInfo, 'organizationType' | 'organizationName'> | null): string | null;
|
|
149
151
|
/** Return the email address associated with the agent's auth config, or null. */
|
|
@@ -303,6 +305,10 @@ export declare function getUserMcpConfigPath(agentId: AgentId): string;
|
|
|
303
305
|
* Get MCP config path for a specific HOME directory (used for version-managed agents).
|
|
304
306
|
*/
|
|
305
307
|
export declare function getMcpConfigPathForHome(agentId: AgentId, home: string): string;
|
|
308
|
+
/**
|
|
309
|
+
* Get project-scoped MCP config path for an agent.
|
|
310
|
+
*/
|
|
311
|
+
export declare function getProjectMcpConfigPath(agentId: AgentId, cwd?: string): string;
|
|
306
312
|
/**
|
|
307
313
|
* Parse MCP config based on agent type.
|
|
308
314
|
*/
|
package/dist/lib/agents.js
CHANGED
|
@@ -505,6 +505,10 @@ export const AGENTS = {
|
|
|
505
505
|
workflows: false,
|
|
506
506
|
memory: true,
|
|
507
507
|
modes: ['plan', 'edit', 'skip'],
|
|
508
|
+
// grok's `--permission-mode plan` silently stalls a headless `-p` run at
|
|
509
|
+
// its ExitPlanMode gate (no TTY to approve). Headless plan auto-downgrades
|
|
510
|
+
// to auto (→ edit via resolveMode). Interactive plan is unaffected.
|
|
511
|
+
headlessPlan: false,
|
|
508
512
|
rulesImports: true,
|
|
509
513
|
},
|
|
510
514
|
},
|
|
@@ -544,6 +548,10 @@ export const AGENTS = {
|
|
|
544
548
|
workflows: true,
|
|
545
549
|
memory: false,
|
|
546
550
|
modes: ['plan', 'edit', 'auto', 'skip'],
|
|
551
|
+
// kimi's headless `-p` refuses to combine with `--plan` (`Cannot combine
|
|
552
|
+
// --prompt with --plan`). Headless plan auto-downgrades to auto (kimi -p
|
|
553
|
+
// auto-runs). Interactive plan is unaffected.
|
|
554
|
+
headlessPlan: false,
|
|
547
555
|
rulesImports: false,
|
|
548
556
|
},
|
|
549
557
|
},
|
|
@@ -970,20 +978,19 @@ export function formatClaudeOrgLabel(orgType) {
|
|
|
970
978
|
.join(' ');
|
|
971
979
|
}
|
|
972
980
|
/**
|
|
973
|
-
* Short badge identifying which
|
|
974
|
-
*
|
|
975
|
-
*
|
|
976
|
-
*
|
|
977
|
-
*
|
|
981
|
+
* Short badge identifying which ORG an account belongs to — "Turing Labs" for a
|
|
982
|
+
* multi-seat Team/Enterprise org, whose name is real identity that disambiguates
|
|
983
|
+
* a Team seat from a same-email personal plan. Returns null for personal plans
|
|
984
|
+
* (Max/Pro/Free): the tier label now lives in the aligned plan column, so a badge
|
|
985
|
+
* would only duplicate it, and a personal org's name is auto-generated boilerplate
|
|
986
|
+
* ("<email>'s Organization"), not identity. Also null when the account carries no
|
|
987
|
+
* organizationType (signed out, non-Claude agents, configs predating the field).
|
|
978
988
|
*/
|
|
979
989
|
export function accountOrgBadge(info) {
|
|
980
|
-
const label = formatClaudeOrgLabel(info?.organizationType);
|
|
981
|
-
if (!label)
|
|
982
|
-
return null;
|
|
983
990
|
const isMultiSeat = info?.organizationType === 'claude_team' || info?.organizationType === 'claude_enterprise';
|
|
984
991
|
if (isMultiSeat && info?.organizationName)
|
|
985
|
-
return
|
|
986
|
-
return
|
|
992
|
+
return info.organizationName;
|
|
993
|
+
return null;
|
|
987
994
|
}
|
|
988
995
|
/** Return the email address associated with the agent's auth config, or null. */
|
|
989
996
|
export async function getAccountEmail(agentId, home) {
|
|
@@ -1313,19 +1320,23 @@ export async function getAccountInfo(agentId, home) {
|
|
|
1313
1320
|
['org', organizationId],
|
|
1314
1321
|
]);
|
|
1315
1322
|
const usageKey = buildIdentityKey(agentId, [['org', organizationId]]);
|
|
1316
|
-
// Plan is derived from .claude.json's
|
|
1317
|
-
//
|
|
1318
|
-
//
|
|
1319
|
-
//
|
|
1320
|
-
//
|
|
1321
|
-
//
|
|
1322
|
-
//
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1323
|
+
// Plan tier is derived from .claude.json's organizationType, which carries
|
|
1324
|
+
// the TRUE tier (claude_max → "Max", claude_pro → "Pro", claude_team →
|
|
1325
|
+
// "Team") and is already in-hand from the config we just read — no Keychain
|
|
1326
|
+
// prompt. billingType only distinguishes "has a Stripe subscription" and so
|
|
1327
|
+
// mislabels every Max account as "Pro"; keep it as a fallback for older
|
|
1328
|
+
// configs predating organizationType. (Reading subscriptionType from the
|
|
1329
|
+
// keychain item would force a macOS Keychain ACL prompt on every `agents
|
|
1330
|
+
// run`, so we deliberately avoid it — organizationType gives us the tier
|
|
1331
|
+
// without that cost.)
|
|
1332
|
+
let plan = formatClaudeOrgLabel(oa?.organizationType);
|
|
1333
|
+
if (!plan) {
|
|
1334
|
+
if (oa?.billingType === 'stripe_subscription') {
|
|
1335
|
+
plan = 'Pro';
|
|
1336
|
+
}
|
|
1337
|
+
else if (oa?.billingType) {
|
|
1338
|
+
plan = oa.billingType;
|
|
1339
|
+
}
|
|
1329
1340
|
}
|
|
1330
1341
|
// usageStatus is NOT derived from cachedExtraUsageDisabledReason. That
|
|
1331
1342
|
// field reports why pay-as-you-go overage is off (out_of_credits = no
|
|
@@ -2186,7 +2197,7 @@ export function getMcpConfigPathForHome(agentId, home) {
|
|
|
2186
2197
|
/**
|
|
2187
2198
|
* Get project-scoped MCP config path for an agent.
|
|
2188
2199
|
*/
|
|
2189
|
-
function getProjectMcpConfigPath(agentId, cwd = process.cwd()) {
|
|
2200
|
+
export function getProjectMcpConfigPath(agentId, cwd = process.cwd()) {
|
|
2190
2201
|
switch (agentId) {
|
|
2191
2202
|
case 'claude':
|
|
2192
2203
|
// Claude uses .mcp.json at project root for project-scoped MCPs
|
|
@@ -228,27 +228,6 @@ function hasSshEndpoint(endpoints) {
|
|
|
228
228
|
}
|
|
229
229
|
});
|
|
230
230
|
}
|
|
231
|
-
/**
|
|
232
|
-
* True when any endpoint is an `ssh://…?os=windows` target — i.e. the browser
|
|
233
|
-
* lives on a remote Windows host. Such a profile's binary (`msedge.exe`) will
|
|
234
|
-
* never exist on this Mac, so create-time local-binary validation must be
|
|
235
|
-
* skipped; the binary is resolved on the remote at connect time instead.
|
|
236
|
-
*/
|
|
237
|
-
function hasRemoteWindowsEndpoint(endpoints) {
|
|
238
|
-
const targets = Array.isArray(endpoints)
|
|
239
|
-
? endpoints
|
|
240
|
-
: Object.values(endpoints).map((preset) => preset.target);
|
|
241
|
-
return targets.some((target) => {
|
|
242
|
-
try {
|
|
243
|
-
const url = new URL(target);
|
|
244
|
-
return (url.protocol === 'ssh:' &&
|
|
245
|
-
(url.searchParams.get('os') || '').toLowerCase() === 'windows');
|
|
246
|
-
}
|
|
247
|
-
catch {
|
|
248
|
-
return false;
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
231
|
export async function createProfile(profile) {
|
|
253
232
|
const meta = readMeta();
|
|
254
233
|
if (meta.browser?.[profile.name]) {
|
|
@@ -276,10 +255,10 @@ export async function createProfile(profile) {
|
|
|
276
255
|
// deferring the failure to the first task. `findBrowserPath` short-circuits
|
|
277
256
|
// for browser=custom without a binary by throwing — same outcome.
|
|
278
257
|
//
|
|
279
|
-
// Skip for
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
if (!
|
|
258
|
+
// Skip for SSH profiles: the browser binary lives on the remote host, so a
|
|
259
|
+
// local lookup would validate the wrong machine. The remote launcher resolves
|
|
260
|
+
// it at connect time.
|
|
261
|
+
if (!hasSshEndpoint(profile.endpoints)) {
|
|
283
262
|
findBrowserPath(profile.browser, profile.binary);
|
|
284
263
|
}
|
|
285
264
|
meta.browser = meta.browser ?? {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const BUN_VIRTUAL_ROOT: RegExp;
|
|
2
|
+
export declare function getAgentsBinPath(argv1?: string | undefined, execPath?: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* A CLI entry must be launched through the Node runtime when it is a Node
|
|
5
|
+
* script — a `.js`/`.cjs`/`.mjs` file, OR a symlink/extension-less shim whose
|
|
6
|
+
* shebang names `node`. Package installs link `bin/agents` to a `dist/index.js`
|
|
7
|
+
* (a symlink) or drop an extension-less `#!/usr/bin/env node` shim, so an
|
|
8
|
+
* extension check alone misses them and they get run directly. A real compiled
|
|
9
|
+
* binary (Mach-O/ELF/PE) has no `#!node` shebang, so it takes the direct branch
|
|
10
|
+
* and owns its own runtime resolution.
|
|
11
|
+
*/
|
|
12
|
+
export declare function isNodeScriptEntry(agentsBin: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Build the `{ command, args }` to re-invoke this CLI with `sub` as its argv,
|
|
15
|
+
* resolving the JS-vs-standalone shape above. This is the single primitive behind
|
|
16
|
+
* both the daemon launch and the secrets-broker spawn — never hand-roll
|
|
17
|
+
* `[process.execPath, process.argv[1], …]`, which appends the bun virtual entry
|
|
18
|
+
* as a bogus subcommand on standalone builds.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getCliLaunch(sub: string[], agentsBin?: string): {
|
|
21
|
+
command: string;
|
|
22
|
+
args: string[];
|
|
23
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve how to re-invoke THIS `agents` CLI as a child process, correctly across
|
|
3
|
+
* both install shapes:
|
|
4
|
+
*
|
|
5
|
+
* 1. **JS install** — `agents` is a `dist/index.js` (or a symlink / `#!node`
|
|
6
|
+
* shim to it). `process.execPath` is `node`, `process.argv[1]` is the script.
|
|
7
|
+
* Relaunch as `node <entry> <sub…>`.
|
|
8
|
+
* 2. **Bun standalone binary** (#315) — `agents` is a compiled Mach-O/ELF/PE.
|
|
9
|
+
* `process.execPath` is the physical signed binary, and `process.argv[1]` is
|
|
10
|
+
* the *virtual* embedded entry `/$bunfs/root/agents`, which Bun reports as an
|
|
11
|
+
* existing path. Passing that virtual path as an argv element makes the CLI
|
|
12
|
+
* receive it as a subcommand and die with `unknown command '/$bunfs/root/agents'`.
|
|
13
|
+
* Relaunch by executing the physical binary directly: `<binary> <sub…>`.
|
|
14
|
+
*
|
|
15
|
+
* Both `getDaemonLaunch` (daemon.ts) and the secrets-broker `cliSpawn`
|
|
16
|
+
* (secrets/agent.ts) route through here so the two never drift. This module is a
|
|
17
|
+
* leaf — it imports nothing from `lib/` — so it can be pulled into either without
|
|
18
|
+
* an import cycle (daemon.ts ↔ secrets/agent.ts already form one).
|
|
19
|
+
*/
|
|
20
|
+
import { execFileSync } from 'child_process';
|
|
21
|
+
import * as fs from 'fs';
|
|
22
|
+
import * as path from 'path';
|
|
23
|
+
export const BUN_VIRTUAL_ROOT = /[/\\]\$bunfs[/\\]root[/\\]/;
|
|
24
|
+
function resolveBunStandaloneEntry(entry, execPath) {
|
|
25
|
+
if (!BUN_VIRTUAL_ROOT.test(entry))
|
|
26
|
+
return entry;
|
|
27
|
+
if (!execPath || BUN_VIRTUAL_ROOT.test(execPath) || !fs.existsSync(execPath)) {
|
|
28
|
+
throw new Error(`Cannot resolve agents CLI: Bun standalone executable not found at ${execPath || '(empty path)'}`);
|
|
29
|
+
}
|
|
30
|
+
return execPath;
|
|
31
|
+
}
|
|
32
|
+
export function getAgentsBinPath(argv1 = process.argv[1], execPath = process.execPath) {
|
|
33
|
+
// Prefer the binary actively executing this code. `which agents` returns
|
|
34
|
+
// whatever happens to be first on PATH, which means a side-by-side dev
|
|
35
|
+
// build at ~/.local/bin would silently spawn the registry-installed
|
|
36
|
+
// daemon and run stale code. For a JS install, process.argv[1] is the
|
|
37
|
+
// absolute entrypoint the user actually invoked. A Bun standalone instead
|
|
38
|
+
// exposes its embedded /$bunfs/root entry at argv[1] and its physical signed
|
|
39
|
+
// executable at process.execPath; Bun reports both as existing paths.
|
|
40
|
+
const runningEntry = argv1 ? resolveBunStandaloneEntry(argv1, execPath) : undefined;
|
|
41
|
+
if (runningEntry && fs.existsSync(runningEntry)) {
|
|
42
|
+
// The package's browser/computer entrypoints are sibling shims without a
|
|
43
|
+
// `daemon` command. A daemon started as their IPC side effect must launch
|
|
44
|
+
// through the main agents entrypoint instead of replaying the shim path.
|
|
45
|
+
const entryName = path.basename(runningEntry);
|
|
46
|
+
const compiledShim = /^(browser|computer)\.(c|m)?js$/.test(entryName);
|
|
47
|
+
const installedShim = /^(browser|computer)$/.test(entryName);
|
|
48
|
+
if (compiledShim || installedShim) {
|
|
49
|
+
const agentsEntry = path.join(path.dirname(runningEntry), compiledShim ? 'index.js' : 'agents');
|
|
50
|
+
if (!fs.existsSync(agentsEntry)) {
|
|
51
|
+
throw new Error(`Cannot start agents daemon: main CLI entry not found at ${agentsEntry}`);
|
|
52
|
+
}
|
|
53
|
+
return agentsEntry;
|
|
54
|
+
}
|
|
55
|
+
return runningEntry;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
return execFileSync('which', ['agents'], { encoding: 'utf-8' }).trim();
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return 'agents';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A CLI entry must be launched through the Node runtime when it is a Node
|
|
66
|
+
* script — a `.js`/`.cjs`/`.mjs` file, OR a symlink/extension-less shim whose
|
|
67
|
+
* shebang names `node`. Package installs link `bin/agents` to a `dist/index.js`
|
|
68
|
+
* (a symlink) or drop an extension-less `#!/usr/bin/env node` shim, so an
|
|
69
|
+
* extension check alone misses them and they get run directly. A real compiled
|
|
70
|
+
* binary (Mach-O/ELF/PE) has no `#!node` shebang, so it takes the direct branch
|
|
71
|
+
* and owns its own runtime resolution.
|
|
72
|
+
*/
|
|
73
|
+
export function isNodeScriptEntry(agentsBin) {
|
|
74
|
+
let resolved = agentsBin;
|
|
75
|
+
try {
|
|
76
|
+
resolved = fs.realpathSync(agentsBin);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Unresolvable (e.g. a template path that does not exist on this box): fall
|
|
80
|
+
// back to the extension check on the path as given.
|
|
81
|
+
}
|
|
82
|
+
if (/\.(c|m)?js$/.test(resolved))
|
|
83
|
+
return true;
|
|
84
|
+
try {
|
|
85
|
+
const fd = fs.openSync(resolved, 'r');
|
|
86
|
+
try {
|
|
87
|
+
const buf = Buffer.alloc(128);
|
|
88
|
+
const n = fs.readSync(fd, buf, 0, 128, 0);
|
|
89
|
+
const firstLine = buf.toString('utf-8', 0, n).split('\n', 1)[0];
|
|
90
|
+
return firstLine.startsWith('#!') && /\bnode\b/.test(firstLine);
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
fs.closeSync(fd);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Build the `{ command, args }` to re-invoke this CLI with `sub` as its argv,
|
|
102
|
+
* resolving the JS-vs-standalone shape above. This is the single primitive behind
|
|
103
|
+
* both the daemon launch and the secrets-broker spawn — never hand-roll
|
|
104
|
+
* `[process.execPath, process.argv[1], …]`, which appends the bun virtual entry
|
|
105
|
+
* as a bogus subcommand on standalone builds.
|
|
106
|
+
*/
|
|
107
|
+
export function getCliLaunch(sub, agentsBin = getAgentsBinPath()) {
|
|
108
|
+
// Resolve a bun virtual entry to the physical executable even when a caller
|
|
109
|
+
// passes agentsBin explicitly (getAgentsBinPath already does this for the
|
|
110
|
+
// default), so a `/$bunfs/root/agents` never becomes the command or an argv.
|
|
111
|
+
const bin = resolveBunStandaloneEntry(agentsBin, process.execPath);
|
|
112
|
+
if (isNodeScriptEntry(bin)) {
|
|
113
|
+
return { command: process.execPath, args: [bin, ...sub] };
|
|
114
|
+
}
|
|
115
|
+
return { command: bin, args: [...sub] };
|
|
116
|
+
}
|