@phnx-labs/agents-cli 1.20.68 → 1.20.70
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 +66 -0
- package/README.md +30 -7
- package/dist/bin/agents +0 -0
- package/dist/commands/computer.d.ts +26 -0
- package/dist/commands/computer.js +173 -149
- package/dist/commands/doctor.js +5 -0
- package/dist/commands/exec.d.ts +18 -0
- package/dist/commands/exec.js +88 -6
- package/dist/commands/fork.d.ts +9 -0
- package/dist/commands/fork.js +67 -0
- 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 +202 -9
- package/dist/commands/view.d.ts +4 -14
- package/dist/commands/view.js +32 -30
- package/dist/index.js +2 -1
- package/dist/lib/agents.d.ts +10 -0
- package/dist/lib/agents.js +32 -0
- package/dist/lib/auth-health.d.ts +141 -0
- package/dist/lib/auth-health.js +277 -0
- package/dist/lib/browser/chrome.d.ts +9 -0
- package/dist/lib/browser/chrome.js +22 -0
- package/dist/lib/computer/download.d.ts +54 -0
- package/dist/lib/computer/download.js +146 -0
- package/dist/lib/computer-rpc.js +9 -3
- package/dist/lib/daemon.js +38 -0
- package/dist/lib/devices/connect.d.ts +15 -0
- package/dist/lib/devices/connect.js +17 -5
- package/dist/lib/devices/health-report.d.ts +11 -0
- package/dist/lib/devices/health-report.js +73 -4
- package/dist/lib/devices/stats-cache.d.ts +36 -0
- package/dist/lib/devices/stats-cache.js +115 -0
- package/dist/lib/devices/terminfo.d.ts +44 -0
- package/dist/lib/devices/terminfo.js +167 -0
- package/dist/lib/rotate.d.ts +12 -8
- package/dist/lib/rotate.js +22 -23
- package/dist/lib/session/fork.d.ts +32 -0
- package/dist/lib/session/fork.js +101 -0
- 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 +23 -0
- package/dist/lib/usage.js +84 -0
- package/package.json +1 -1
package/dist/commands/ssh.js
CHANGED
|
@@ -29,13 +29,17 @@ import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
|
29
29
|
import { hostNameFor, renderSshConfig } from '../lib/devices/ssh-config.js';
|
|
30
30
|
import { ASKPASS_BUNDLE_ENV, ASKPASS_KEY_ENV, buildSshInvocation, writeAskpassShim, } from '../lib/devices/connect.js';
|
|
31
31
|
import { ensureManagedKnownHostsDir, isHostPinned } from '../lib/devices/known-hosts.js';
|
|
32
|
+
import { shouldSyncTerminfo, syncTerminfoToDevice, terminfoHostKey } from '../lib/devices/terminfo.js';
|
|
32
33
|
import { fanOutDevices, planFleetTargets, remoteFleetTargets, runFleet, skipLabel, upgradeCommand, } from '../lib/devices/fleet.js';
|
|
33
|
-
import { fleetCapacity, fmtBytes, headroom,
|
|
34
|
+
import { fleetCapacity, fmtBytes, headroom, } from '../lib/devices/health.js';
|
|
34
35
|
import { buildFleetHealthReport, renderFleetMatrix, renderFleetWarnings, } from '../lib/devices/health-report.js';
|
|
36
|
+
import { loadFleetStats, readStatsCache } from '../lib/devices/stats-cache.js';
|
|
35
37
|
import { checkSyncStatus, countOrphans } from '../lib/drift.js';
|
|
36
38
|
import { checkAllClis } from '../lib/teams/agents.js';
|
|
37
39
|
import { buildRemoteAgentsInvocation } from '../lib/hosts/remote-cmd.js';
|
|
38
40
|
import { sshExecAsync } from '../lib/ssh-exec.js';
|
|
41
|
+
import { ALL_AGENT_IDS } from '../lib/agents.js';
|
|
42
|
+
import { formatCheckedAge, isDeadVerdict, probeLocalFleetAuth, readAuthHealthCache, summarizeHostAuth, summarizeVerdicts, verdictLabel, writeFleetAuthRows, } from '../lib/auth-health.js';
|
|
39
43
|
/** One-line summary of a device for `list`. `isSelf` marks the machine this
|
|
40
44
|
* command is running on so it stands out from the rest of the tailnet. */
|
|
41
45
|
function deviceSummary(d, isSelf = false) {
|
|
@@ -253,19 +257,23 @@ async function probeRemoteHealth(target) {
|
|
|
253
257
|
clis: parsed.clis ?? {},
|
|
254
258
|
sync: parsed.sync ?? [],
|
|
255
259
|
orphans: parsed.orphans ?? [],
|
|
260
|
+
// The remote self-reports its own cached auth rollup (fresh via its daemon),
|
|
261
|
+
// so the Auth column is current without a prior fleet-wide `fleet ping`.
|
|
262
|
+
// Older remotes that don't emit it fall back to this host's cache below.
|
|
263
|
+
auth: parsed.auth,
|
|
256
264
|
};
|
|
257
265
|
}
|
|
258
266
|
async function runFleetStatus(opts) {
|
|
259
267
|
const reg = await loadDevices();
|
|
260
268
|
const self = machineId();
|
|
269
|
+
const forceRefresh = Boolean(opts.refresh || opts.live);
|
|
261
270
|
const planned = planFleetTargets(reg);
|
|
262
271
|
const probeable = planned.filter((t) => !t.skip).map((t) => t.device);
|
|
272
|
+
// Cache-first: serve remote stats from the daemon-warmed cache (instant),
|
|
273
|
+
// probe this machine locally, and only ssh out for missing/forced rows.
|
|
263
274
|
const statsMap = opts.stats === false
|
|
264
275
|
? new Map()
|
|
265
|
-
: await
|
|
266
|
-
if (opts.stats !== false && !statsMap.has(self)) {
|
|
267
|
-
statsMap.set(self, await probeLocalStats(self));
|
|
268
|
-
}
|
|
276
|
+
: (await loadFleetStats(probeable, { forceRefresh, selfName: self })).stats;
|
|
269
277
|
const rows = [localHealthRow(self, statsMap.get(self))];
|
|
270
278
|
const remoteTargets = remoteFleetTargets(planned, self)
|
|
271
279
|
.map((t) => ({
|
|
@@ -297,6 +305,15 @@ async function runFleetStatus(opts) {
|
|
|
297
305
|
});
|
|
298
306
|
}
|
|
299
307
|
}
|
|
308
|
+
// Auth column: remote rows already carry the host's self-reported rollup from
|
|
309
|
+
// its `doctor --json`. Fill the rest (this machine; older remotes that don't
|
|
310
|
+
// emit it) from this host's local cache — written by `agents fleet ping` and
|
|
311
|
+
// the daemon's local refresh. A never-probed host rolls up to "—". No network.
|
|
312
|
+
const authCache = readAuthHealthCache();
|
|
313
|
+
for (const row of rows) {
|
|
314
|
+
if (!row.auth)
|
|
315
|
+
row.auth = summarizeHostAuth(authCache, row.name);
|
|
316
|
+
}
|
|
300
317
|
const report = buildFleetHealthReport(rows);
|
|
301
318
|
if (opts.json) {
|
|
302
319
|
console.log(JSON.stringify(report, null, 2));
|
|
@@ -311,6 +328,150 @@ async function runFleetStatus(opts) {
|
|
|
311
328
|
if (opts.strict && report.hasWarnings)
|
|
312
329
|
process.exitCode = 1;
|
|
313
330
|
}
|
|
331
|
+
/** SSH into a host and run its local auth probe, returning its rows. */
|
|
332
|
+
async function probeRemoteAuth(target) {
|
|
333
|
+
const isWin = /^win/i.test((target.platform ?? '').trim());
|
|
334
|
+
const env = isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' };
|
|
335
|
+
const cmd = buildRemoteAgentsInvocation(['devices', 'ping', '--local', '--json'], undefined, isWin ? 'windows' : undefined, env);
|
|
336
|
+
const res = await sshExecAsync(target.name, cmd, { timeoutMs: 60000, multiplex: true });
|
|
337
|
+
if (res.code !== 0) {
|
|
338
|
+
throw new Error(res.timedOut ? 'timed out' : (res.stderr.trim() || `exit ${res.code ?? 'unknown'}`));
|
|
339
|
+
}
|
|
340
|
+
const parsed = JSON.parse(res.stdout);
|
|
341
|
+
return parsed.rows ?? [];
|
|
342
|
+
}
|
|
343
|
+
async function runFleetPing(opts) {
|
|
344
|
+
const self = machineId();
|
|
345
|
+
const cliVersion = getCliVersion();
|
|
346
|
+
// --local: probe just this host. Used both directly and as the fan-out worker.
|
|
347
|
+
if (opts.local) {
|
|
348
|
+
const rows = await probeLocalFleetAuth({ cliVersion });
|
|
349
|
+
writeFleetAuthRows(self, rows);
|
|
350
|
+
if (opts.json) {
|
|
351
|
+
console.log(JSON.stringify({ host: self, rows }));
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
for (const line of renderAuthMatrix([{ host: self, rows }], { verbose: opts.verbose }))
|
|
355
|
+
console.log(line);
|
|
356
|
+
}
|
|
357
|
+
if (opts.strict && rows.some((r) => isDeadVerdict(r.health.verdict))) {
|
|
358
|
+
process.exitCode = 1;
|
|
359
|
+
}
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
// Origin: probe locally, then fan out to the rest of the fleet in parallel.
|
|
363
|
+
const reg = await loadDevices();
|
|
364
|
+
const planned = planFleetTargets(reg);
|
|
365
|
+
const results = [];
|
|
366
|
+
const localRows = await probeLocalFleetAuth({ cliVersion });
|
|
367
|
+
writeFleetAuthRows(self, localRows);
|
|
368
|
+
results.push({ host: self, rows: localRows });
|
|
369
|
+
const remoteTargets = remoteFleetTargets(planned, self).map((t) => ({
|
|
370
|
+
name: t.device.name,
|
|
371
|
+
platform: t.device.platform,
|
|
372
|
+
skip: t.skip,
|
|
373
|
+
}));
|
|
374
|
+
const probeable = remoteTargets.filter((t) => !t.skip).length;
|
|
375
|
+
const spinner = isInteractiveTerminal() && !opts.json
|
|
376
|
+
? ora(`Pinging ${probeable} device${probeable === 1 ? '' : 's'}…`).start()
|
|
377
|
+
: undefined;
|
|
378
|
+
let remote;
|
|
379
|
+
try {
|
|
380
|
+
remote = await fanOutDevices(remoteTargets, probeRemoteAuth);
|
|
381
|
+
}
|
|
382
|
+
finally {
|
|
383
|
+
spinner?.stop();
|
|
384
|
+
}
|
|
385
|
+
for (const r of remote) {
|
|
386
|
+
if (r.status === 'ok' && r.value) {
|
|
387
|
+
results.push({ host: r.name, rows: r.value });
|
|
388
|
+
writeFleetAuthRows(r.name, r.value);
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
results.push({
|
|
392
|
+
host: r.name,
|
|
393
|
+
rows: [],
|
|
394
|
+
error: r.error,
|
|
395
|
+
skipped: r.reason ? String(r.reason) : undefined,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (opts.json) {
|
|
400
|
+
console.log(JSON.stringify(results, null, 2));
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
for (const line of renderAuthMatrix(results, { verbose: opts.verbose }))
|
|
404
|
+
console.log(line);
|
|
405
|
+
}
|
|
406
|
+
const anyBad = results.some((r) => r.rows.some((row) => isDeadVerdict(row.health.verdict)));
|
|
407
|
+
if (opts.strict && anyBad)
|
|
408
|
+
process.exitCode = 1;
|
|
409
|
+
}
|
|
410
|
+
/** Color a per-host×agent cell: green all-live, red any revoked/expired, yellow degraded. */
|
|
411
|
+
function authCell(summary, width) {
|
|
412
|
+
if (summary.total === 0)
|
|
413
|
+
return chalk.dim('·'.padEnd(width));
|
|
414
|
+
const text = `${summary.live}/${summary.total}`;
|
|
415
|
+
const padded = text.padEnd(width);
|
|
416
|
+
if (summary.bad > 0)
|
|
417
|
+
return chalk.red(padded);
|
|
418
|
+
if (summary.warn > 0)
|
|
419
|
+
return chalk.yellow(padded);
|
|
420
|
+
return chalk.green(padded);
|
|
421
|
+
}
|
|
422
|
+
/** Render the fleet auth matrix (device rows × agent columns) plus an optional per-account breakdown. */
|
|
423
|
+
function renderAuthMatrix(results, opts) {
|
|
424
|
+
// Only show agent columns that appear somewhere in the results.
|
|
425
|
+
const present = new Set();
|
|
426
|
+
for (const r of results)
|
|
427
|
+
for (const row of r.rows)
|
|
428
|
+
present.add(row.agent);
|
|
429
|
+
const agents = ALL_AGENT_IDS.filter((a) => present.has(a));
|
|
430
|
+
const cellW = 6; // 6 disambiguates opencode/openclaw (both 'openc' at 5)
|
|
431
|
+
const nameW = Math.max(6, ...results.map((r) => r.host.length));
|
|
432
|
+
const lines = [chalk.bold('Fleet auth')];
|
|
433
|
+
const header = ` ${'Device'.padEnd(nameW)} ${agents.map((a) => a.slice(0, cellW).padEnd(cellW)).join(' ')}`;
|
|
434
|
+
lines.push(chalk.gray(header));
|
|
435
|
+
for (const r of results) {
|
|
436
|
+
const cells = agents.map((a) => {
|
|
437
|
+
const verdicts = r.rows.filter((row) => row.agent === a).map((row) => row.health.verdict);
|
|
438
|
+
return authCell(summarizeVerdicts(verdicts), cellW);
|
|
439
|
+
});
|
|
440
|
+
let note = '';
|
|
441
|
+
if (r.skipped)
|
|
442
|
+
note = chalk.dim(` ${r.skipped}`);
|
|
443
|
+
else if (r.error)
|
|
444
|
+
note = chalk.red(` ${r.error}`);
|
|
445
|
+
else {
|
|
446
|
+
const dead = r.rows.filter((row) => isDeadVerdict(row.health.verdict)).length;
|
|
447
|
+
if (dead > 0)
|
|
448
|
+
note = chalk.red(` ${dead} revoked — re-login`);
|
|
449
|
+
}
|
|
450
|
+
lines.push(` ${r.host.padEnd(nameW)} ${cells.join(' ')}${note}`);
|
|
451
|
+
}
|
|
452
|
+
lines.push('');
|
|
453
|
+
lines.push(chalk.gray(' cell = live/total accounts · green all live · red revoked (re-login) · yellow expired/unverified/limited'));
|
|
454
|
+
if (opts?.verbose) {
|
|
455
|
+
lines.push('');
|
|
456
|
+
lines.push(chalk.bold('Accounts'));
|
|
457
|
+
for (const r of results) {
|
|
458
|
+
if (r.rows.length === 0)
|
|
459
|
+
continue;
|
|
460
|
+
for (const row of r.rows.slice().sort((x, y) => (x.agent + x.version).localeCompare(y.agent + y.version))) {
|
|
461
|
+
const v = row.health.verdict;
|
|
462
|
+
const label = v === 'live' ? chalk.green(verdictLabel(v))
|
|
463
|
+
: (v === 'revoked' || v === 'expired') ? chalk.red(verdictLabel(v))
|
|
464
|
+
: chalk.yellow(verdictLabel(v));
|
|
465
|
+
const acctRaw = row.account ?? '—';
|
|
466
|
+
const acct = row.account ? chalk.cyan(acctRaw.padEnd(28)) : chalk.dim(acctRaw.padEnd(28));
|
|
467
|
+
const detail = row.health.detail ? chalk.dim(` ${row.health.detail}`) : '';
|
|
468
|
+
const age = chalk.dim(` · ${formatCheckedAge(row.health.checkedAt)}`);
|
|
469
|
+
lines.push(` ${r.host.padEnd(nameW)} ${`${row.agent}@${row.version}`.padEnd(22)} ${acct} ${label}${detail}${age}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return lines;
|
|
474
|
+
}
|
|
314
475
|
/** Register the `agents devices` command tree (also aliased as `fleet`). */
|
|
315
476
|
function registerDevicesCommands(program) {
|
|
316
477
|
const devicesCmd = program
|
|
@@ -403,6 +564,8 @@ Typical workflow:
|
|
|
403
564
|
.description('List registered devices with platform, address, reachability, and live resource headroom.')
|
|
404
565
|
.option('--json', 'output the registry as a JSON array (for scripts and hooks)')
|
|
405
566
|
.option('--no-stats', 'skip the live resource probe (instant; names/addresses only)')
|
|
567
|
+
.option('--refresh', 'force a live probe of every device, bypassing the cache')
|
|
568
|
+
.option('--live', 'alias of --refresh (shorter to type)')
|
|
406
569
|
.option('-f, --full', 'full mode: add per-device core count and free/total memory')
|
|
407
570
|
.action(async (opts) => {
|
|
408
571
|
const reg = await loadDevices();
|
|
@@ -417,18 +580,26 @@ Typical workflow:
|
|
|
417
580
|
return;
|
|
418
581
|
}
|
|
419
582
|
const self = machineId();
|
|
583
|
+
const forceRefresh = Boolean(opts.refresh || opts.live);
|
|
420
584
|
let statsMap;
|
|
585
|
+
let freshness;
|
|
421
586
|
if (opts.stats !== false) {
|
|
422
|
-
//
|
|
423
|
-
//
|
|
587
|
+
// Cache-first: serve remote devices from the daemon-warmed cache
|
|
588
|
+
// (instant), probe this machine locally, and only ssh out for missing or
|
|
589
|
+
// forced (--refresh/--live) rows — so a warm read never hangs on a box.
|
|
424
590
|
const probeable = planFleetTargets(reg)
|
|
425
591
|
.filter((t) => !t.skip)
|
|
426
592
|
.map((t) => t.device);
|
|
427
|
-
|
|
593
|
+
// Only spin when we'll actually ssh (forced, or a cold/partial cache).
|
|
594
|
+
const cache = readStatsCache();
|
|
595
|
+
const willSsh = forceRefresh || probeable.some((d) => d.name !== self && !cache[d.name]);
|
|
596
|
+
const spinner = willSsh && isInteractiveTerminal()
|
|
428
597
|
? ora(`Probing ${probeable.length} device${probeable.length === 1 ? '' : 's'}…`).start()
|
|
429
598
|
: undefined;
|
|
430
599
|
try {
|
|
431
|
-
|
|
600
|
+
const res = await loadFleetStats(probeable, { forceRefresh, selfName: self });
|
|
601
|
+
statsMap = res.stats;
|
|
602
|
+
freshness = res;
|
|
432
603
|
}
|
|
433
604
|
finally {
|
|
434
605
|
spinner?.stop();
|
|
@@ -437,6 +608,9 @@ Typical workflow:
|
|
|
437
608
|
console.log(chalk.bold(`Devices (${names.length})`));
|
|
438
609
|
for (const line of renderDeviceTable(reg, names, self, statsMap, opts.full))
|
|
439
610
|
console.log(line);
|
|
611
|
+
if (freshness?.servedFromCache && freshness.oldestFetchedAt != null) {
|
|
612
|
+
console.log(chalk.gray(` updated ${formatCheckedAge(freshness.oldestFetchedAt)} — pass --refresh (--live) for a live probe`));
|
|
613
|
+
}
|
|
440
614
|
});
|
|
441
615
|
devicesCmd
|
|
442
616
|
.command('status')
|
|
@@ -444,9 +618,21 @@ Typical workflow:
|
|
|
444
618
|
.option('--json', 'output machine-readable JSON')
|
|
445
619
|
.option('--strict', 'exit non-zero when any device has drift or is unreachable')
|
|
446
620
|
.option('--no-stats', 'skip the live resource probe')
|
|
621
|
+
.option('--refresh', 'force a live probe of every device, bypassing the cache')
|
|
622
|
+
.option('--live', 'alias of --refresh (shorter to type)')
|
|
447
623
|
.action(async (opts) => {
|
|
448
624
|
await runFleetStatus(opts);
|
|
449
625
|
});
|
|
626
|
+
devicesCmd
|
|
627
|
+
.command('ping')
|
|
628
|
+
.description('Live auth health: complete a real request for every agent account across the fleet (unlike the cached "signed in" flag). Writes the shared auth-health cache read by `agents view` and `fleet status`.')
|
|
629
|
+
.option('--json', 'output machine-readable JSON')
|
|
630
|
+
.option('--local', 'probe only this host (used internally for fan-out)')
|
|
631
|
+
.option('--verbose', 'show a per-account breakdown, not just the per-host rollup')
|
|
632
|
+
.option('--strict', 'exit non-zero when any account is revoked (expired is soft — it self-refreshes)')
|
|
633
|
+
.action(async (opts) => {
|
|
634
|
+
await runFleetPing(opts);
|
|
635
|
+
});
|
|
450
636
|
devicesCmd
|
|
451
637
|
.command('show <name>')
|
|
452
638
|
.description('Show the full profile for one device.')
|
|
@@ -668,6 +854,13 @@ secrets bundle via an askpass shim — the password never touches argv.
|
|
|
668
854
|
const addr = hostNameFor(device);
|
|
669
855
|
const pinned = addr ? isHostPinned(addr) : false;
|
|
670
856
|
const { args, env } = buildSshInvocation(device, cmd, shim, { pinned });
|
|
857
|
+
// Interactive login: make the local terminal's terminfo (e.g.
|
|
858
|
+
// xterm-ghostty) available on the remote so backspace/colors/clear work.
|
|
859
|
+
// Best-effort + cached per host — never blocks the login (see terminfo.ts).
|
|
860
|
+
if (cmd.length === 0 && shouldSyncTerminfo({ term: process.env.TERM, shell: device.shell, interactive: process.stdout.isTTY ?? false })) {
|
|
861
|
+
const { args: tinfoArgs, env: tinfoEnv } = buildSshInvocation(device, ['tic', '-x', '-'], shim, { pinned });
|
|
862
|
+
syncTerminfoToDevice({ device, host: terminfoHostKey(device, addr), term: process.env.TERM, sshArgs: tinfoArgs, sshEnv: tinfoEnv });
|
|
863
|
+
}
|
|
671
864
|
const res = spawnSync('ssh', args, {
|
|
672
865
|
stdio: 'inherit',
|
|
673
866
|
env: { ...process.env, ...env },
|
package/dist/commands/view.d.ts
CHANGED
|
@@ -7,23 +7,12 @@
|
|
|
7
7
|
* rules, hooks, and promptcuts synced to that version.
|
|
8
8
|
*/
|
|
9
9
|
import type { Command } from 'commander';
|
|
10
|
+
import { accountDisplayLabel } from '../lib/agents.js';
|
|
10
11
|
import type { AccountInfo } from '../lib/agents.js';
|
|
11
12
|
import type { AgentId } from '../lib/types.js';
|
|
12
13
|
import { type ProfileSummary } from '../lib/profiles.js';
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
* signed-in agent whose credential carries no email but does carry an opaque
|
|
16
|
-
* account id (Kimi's user_id), show `id:<user_id>` so distinct accounts read
|
|
17
|
-
* distinctly instead of a generic "signed in". Falls back to "signed in" when we
|
|
18
|
-
* have neither (Antigravity), and empty when signed out.
|
|
19
|
-
*
|
|
20
|
-
* When the account carries a Claude organizationType, the org badge is appended
|
|
21
|
-
* — "email (Turing Labs · Team)" — so two installs signed into the same email
|
|
22
|
-
* under different orgs (personal Max vs a Team seat) read distinctly. The
|
|
23
|
-
* suffix is plain text inside the label so the existing column-width pass
|
|
24
|
-
* measures and pads it for free.
|
|
25
|
-
*/
|
|
26
|
-
export declare function accountColumnLabel(info?: Pick<AccountInfo, 'email' | 'accountId' | 'signedIn' | 'organizationType' | 'organizationName'> | null): string;
|
|
14
|
+
/** Shared account identity formatter, re-exported for the view-specific tests. */
|
|
15
|
+
export declare const accountColumnLabel: typeof accountDisplayLabel;
|
|
27
16
|
type SyncState = 'synced' | 'new' | 'modified' | 'deleted';
|
|
28
17
|
/** Per-section filter flags. When any are true, only those sections render. */
|
|
29
18
|
export interface ViewSectionFilter {
|
|
@@ -134,6 +123,7 @@ export declare function viewAction(agentArg?: string, options?: {
|
|
|
134
123
|
resources?: string | boolean;
|
|
135
124
|
detailed?: boolean;
|
|
136
125
|
refresh?: boolean;
|
|
126
|
+
live?: boolean;
|
|
137
127
|
} & ViewSectionFilter): Promise<void>;
|
|
138
128
|
/** Register the `agents view` command. */
|
|
139
129
|
export declare function registerViewCommand(program: Command): void;
|
package/dist/commands/view.js
CHANGED
|
@@ -4,8 +4,10 @@ import { visibleWidth, termLink } from '../lib/format.js';
|
|
|
4
4
|
import ora from 'ora';
|
|
5
5
|
import * as fs from 'fs';
|
|
6
6
|
import * as path from 'path';
|
|
7
|
-
import { AGENTS, ALL_AGENT_IDS,
|
|
7
|
+
import { AGENTS, ALL_AGENT_IDS, accountDisplayLabel, getAllCliStates, getAccountInfo, resolveAgentName, formatAgentError, agentLabel, colorAgent, } from '../lib/agents.js';
|
|
8
8
|
import { loginHint } from '../lib/signin-badge.js';
|
|
9
|
+
import { machineId } from '../lib/machine-id.js';
|
|
10
|
+
import { authCacheKey, formatCheckedAge, readAuthHealthCache } from '../lib/auth-health.js';
|
|
9
11
|
import { agentReportsUsage, deriveUsageStatusFromSnapshot, formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
|
|
10
12
|
import { readManifest } from '../lib/manifest.js';
|
|
11
13
|
import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, promptNewResourceSelection, syncResourcesToVersion, removeVersion, printTrashFooter, reconcileStaleLatestForAgent, isGlobalBinaryAgent, getLiveVersion, } from '../lib/versions.js';
|
|
@@ -26,33 +28,8 @@ import { loadManifest, isStale } from '../lib/staleness/index.js';
|
|
|
26
28
|
import { confirm } from '@inquirer/prompts';
|
|
27
29
|
import { formatPath, isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
28
30
|
import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/width.js';
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const SIGNED_IN_LABEL = 'signed in';
|
|
32
|
-
/**
|
|
33
|
-
* Text for the account (email) column. Prefers the real email; otherwise, for a
|
|
34
|
-
* signed-in agent whose credential carries no email but does carry an opaque
|
|
35
|
-
* account id (Kimi's user_id), show `id:<user_id>` so distinct accounts read
|
|
36
|
-
* distinctly instead of a generic "signed in". Falls back to "signed in" when we
|
|
37
|
-
* have neither (Antigravity), and empty when signed out.
|
|
38
|
-
*
|
|
39
|
-
* When the account carries a Claude organizationType, the org badge is appended
|
|
40
|
-
* — "email (Turing Labs · Team)" — so two installs signed into the same email
|
|
41
|
-
* under different orgs (personal Max vs a Team seat) read distinctly. The
|
|
42
|
-
* suffix is plain text inside the label so the existing column-width pass
|
|
43
|
-
* measures and pads it for free.
|
|
44
|
-
*/
|
|
45
|
-
export function accountColumnLabel(info) {
|
|
46
|
-
if (!info)
|
|
47
|
-
return '';
|
|
48
|
-
if (info.email) {
|
|
49
|
-
const badge = accountOrgBadge(info);
|
|
50
|
-
return badge ? `${info.email} (${badge})` : info.email;
|
|
51
|
-
}
|
|
52
|
-
if (info.signedIn)
|
|
53
|
-
return info.accountId ? `id:${info.accountId}` : SIGNED_IN_LABEL;
|
|
54
|
-
return '';
|
|
55
|
-
}
|
|
31
|
+
/** Shared account identity formatter, re-exported for the view-specific tests. */
|
|
32
|
+
export const accountColumnLabel = accountDisplayLabel;
|
|
56
33
|
/**
|
|
57
34
|
* Group profile summaries by their host harness, optionally filtered to a
|
|
58
35
|
* single agent. Profile YAMLs that fail validation are silently skipped by
|
|
@@ -245,6 +222,22 @@ function renderHostClisSection(cwd) {
|
|
|
245
222
|
console.log(` ${chalk.red('error')} ${chalk.gray(err.file)}: ${chalk.gray(err.reason)}`);
|
|
246
223
|
}
|
|
247
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* A compact live-auth chip for a version row, read from the fleet auth-health
|
|
227
|
+
* cache (written by `agents fleet ping` / the daemon). Empty when the install
|
|
228
|
+
* hasn't been probed — this is the local "signed in" flag's ground-truth
|
|
229
|
+
* companion: ● live, ○ revoked (re-login), ◐ unverified/limited, plus its age.
|
|
230
|
+
*/
|
|
231
|
+
function liveAuthChip(cache, host, agentId, version) {
|
|
232
|
+
const h = cache[authCacheKey(host, agentId, version)];
|
|
233
|
+
if (!h)
|
|
234
|
+
return '';
|
|
235
|
+
const glyph = h.verdict === 'live' ? chalk.green('●')
|
|
236
|
+
: h.verdict === 'revoked' ? chalk.red('○')
|
|
237
|
+
: h.verdict === 'expired' ? chalk.yellow('○')
|
|
238
|
+
: chalk.yellow('◐');
|
|
239
|
+
return `${glyph} ${chalk.gray(formatCheckedAge(h.checkedAt))}`;
|
|
240
|
+
}
|
|
248
241
|
async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
249
242
|
const spinnerText = filterAgentId
|
|
250
243
|
? `Checking ${agentLabel(filterAgentId)} agents...`
|
|
@@ -276,6 +269,9 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
276
269
|
}
|
|
277
270
|
// Shim healing is silent — users don't need to know about internal repairs
|
|
278
271
|
console.log(chalk.bold('Installed Agent CLIs\n'));
|
|
272
|
+
const selfHost = machineId();
|
|
273
|
+
// Read the auth-health cache once (not per version row — see the batching note above).
|
|
274
|
+
const authCache = readAuthHealthCache();
|
|
279
275
|
// Pre-fetch account info for all versions in parallel
|
|
280
276
|
const infoFetches = [];
|
|
281
277
|
const globalInfoFetches = [];
|
|
@@ -500,6 +496,9 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
500
496
|
if (runDefaultBits.length > 0) {
|
|
501
497
|
parts.push(chalk.gray(`run ${runDefaultBits.join(' ')}`));
|
|
502
498
|
}
|
|
499
|
+
const authChip = liveAuthChip(authCache, selfHost, agentId, version);
|
|
500
|
+
if (authChip)
|
|
501
|
+
parts.push(authChip);
|
|
503
502
|
console.log(parts.join(' '));
|
|
504
503
|
if (showPaths) {
|
|
505
504
|
const versionDir = getVersionDir(agentId, version);
|
|
@@ -1401,6 +1400,8 @@ export async function pruneDuplicates(filterAgentId, yes, dryRun) {
|
|
|
1401
1400
|
* Exported for use by deprecated aliases.
|
|
1402
1401
|
*/
|
|
1403
1402
|
export async function viewAction(agentArg, options) {
|
|
1403
|
+
// --live is a shorter-to-type alias of --refresh; both force a live probe.
|
|
1404
|
+
const forceRefresh = options?.refresh === true || options?.live === true;
|
|
1404
1405
|
// --resources / --detailed imply --json (they only shape structured output).
|
|
1405
1406
|
const explicitResources = options?.detailed === true || options?.resources !== undefined;
|
|
1406
1407
|
const json = options?.json === true || explicitResources;
|
|
@@ -1441,7 +1442,7 @@ export async function viewAction(agentArg, options) {
|
|
|
1441
1442
|
return;
|
|
1442
1443
|
}
|
|
1443
1444
|
// No argument: show all installed versions
|
|
1444
|
-
await showInstalledVersions(undefined, { forceRefresh
|
|
1445
|
+
await showInstalledVersions(undefined, { forceRefresh });
|
|
1445
1446
|
return;
|
|
1446
1447
|
}
|
|
1447
1448
|
// Parse agent@version syntax
|
|
@@ -1502,7 +1503,7 @@ export async function viewAction(agentArg, options) {
|
|
|
1502
1503
|
}
|
|
1503
1504
|
else {
|
|
1504
1505
|
// Just agent name: show versions for that agent
|
|
1505
|
-
await showInstalledVersions(agentId, { forceRefresh
|
|
1506
|
+
await showInstalledVersions(agentId, { forceRefresh });
|
|
1506
1507
|
}
|
|
1507
1508
|
}
|
|
1508
1509
|
/** Register the `agents view` command. */
|
|
@@ -1513,6 +1514,7 @@ export function registerViewCommand(program) {
|
|
|
1513
1514
|
.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.')
|
|
1514
1515
|
.option('--detailed', 'Include all resources in --json output (alias for --resources all). Implies --json.')
|
|
1515
1516
|
.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.')
|
|
1517
|
+
.option('--live', 'Alias of --refresh (shorter to type).')
|
|
1516
1518
|
.option('--prune', 'Remove older installed versions that share an account with a newer installed version. Skips the global default.')
|
|
1517
1519
|
.option('--dry-run', 'With --prune, show duplicate versions without deleting')
|
|
1518
1520
|
.option('-y, --yes', 'Skip the prune confirmation prompt.')
|
package/dist/index.js
CHANGED
|
@@ -50,7 +50,7 @@ if (IS_DEV_BUILD) {
|
|
|
50
50
|
// module on each invocation (which loaded the whole ~50-module tree before the
|
|
51
51
|
// first byte of output), the registry maps a command name to a thunk that
|
|
52
52
|
// imports only what that command needs. See src/lib/startup/command-registry.ts.
|
|
53
|
-
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadMonitors, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadApply, loadCheck, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadOutput, loadBudget, loadAlias, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadShare, loadFeed, loadMailboxes, } from './lib/startup/command-registry.js';
|
|
53
|
+
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadMonitors, loadRun, loadFork, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadApply, loadCheck, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadOutput, loadBudget, loadAlias, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadShare, loadFeed, loadMailboxes, } from './lib/startup/command-registry.js';
|
|
54
54
|
import { applyGlobalHelpConventions } from './lib/help.js';
|
|
55
55
|
import { renderWhatsNew } from './lib/whats-new.js';
|
|
56
56
|
import { emit, redactArgs } from './lib/events.js';
|
|
@@ -690,6 +690,7 @@ async function registerAllEagerCommands() {
|
|
|
690
690
|
await reg(loadRoutines);
|
|
691
691
|
await reg(loadMonitors);
|
|
692
692
|
await reg(loadRun);
|
|
693
|
+
await reg(loadFork);
|
|
693
694
|
await reg(loadDefaults);
|
|
694
695
|
await reg(loadModels);
|
|
695
696
|
await reg(loadPrune);
|
package/dist/lib/agents.d.ts
CHANGED
|
@@ -148,6 +148,16 @@ export declare function formatClaudeOrgLabel(orgType: string | null | undefined)
|
|
|
148
148
|
* organizationType (signed out, non-Claude agents, configs predating the field).
|
|
149
149
|
*/
|
|
150
150
|
export declare function accountOrgBadge(info?: Pick<AccountInfo, 'organizationType' | 'organizationName'> | null): string | null;
|
|
151
|
+
/** Agents whose local credential formats expose enough state for account selection. */
|
|
152
|
+
export declare const ACCOUNT_INSPECTION_AGENT_IDS: readonly ["claude", "codex", "gemini", "grok", "antigravity", "kimi", "droid", "opencode"];
|
|
153
|
+
/** Whether agents-cli can determine this agent's per-version sign-in state. */
|
|
154
|
+
export declare function supportsAccountInspection(agentId: AgentId): boolean;
|
|
155
|
+
/**
|
|
156
|
+
* Human-readable account identity shared by every account-aware surface.
|
|
157
|
+
* Prefer email, append a multi-seat Claude organization name when present,
|
|
158
|
+
* then fall back to a non-secret account id or a generic signed-in label.
|
|
159
|
+
*/
|
|
160
|
+
export declare function accountDisplayLabel(info?: Pick<AccountInfo, 'email' | 'accountId' | 'signedIn' | 'organizationType' | 'organizationName'> | null): string;
|
|
151
161
|
/** Return the email address associated with the agent's auth config, or null. */
|
|
152
162
|
export declare function getAccountEmail(agentId: AgentId, home?: string): Promise<string | null>;
|
|
153
163
|
/** Decrypted contents of Droid's auth.v2.file (subset we consume). */
|
package/dist/lib/agents.js
CHANGED
|
@@ -992,6 +992,38 @@ export function accountOrgBadge(info) {
|
|
|
992
992
|
return info.organizationName;
|
|
993
993
|
return null;
|
|
994
994
|
}
|
|
995
|
+
/** Agents whose local credential formats expose enough state for account selection. */
|
|
996
|
+
export const ACCOUNT_INSPECTION_AGENT_IDS = [
|
|
997
|
+
'claude',
|
|
998
|
+
'codex',
|
|
999
|
+
'gemini',
|
|
1000
|
+
'grok',
|
|
1001
|
+
'antigravity',
|
|
1002
|
+
'kimi',
|
|
1003
|
+
'droid',
|
|
1004
|
+
'opencode',
|
|
1005
|
+
];
|
|
1006
|
+
const ACCOUNT_INSPECTION_AGENTS = new Set(ACCOUNT_INSPECTION_AGENT_IDS);
|
|
1007
|
+
/** Whether agents-cli can determine this agent's per-version sign-in state. */
|
|
1008
|
+
export function supportsAccountInspection(agentId) {
|
|
1009
|
+
return ACCOUNT_INSPECTION_AGENTS.has(agentId);
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Human-readable account identity shared by every account-aware surface.
|
|
1013
|
+
* Prefer email, append a multi-seat Claude organization name when present,
|
|
1014
|
+
* then fall back to a non-secret account id or a generic signed-in label.
|
|
1015
|
+
*/
|
|
1016
|
+
export function accountDisplayLabel(info) {
|
|
1017
|
+
if (!info)
|
|
1018
|
+
return '';
|
|
1019
|
+
if (info.email) {
|
|
1020
|
+
const badge = accountOrgBadge(info);
|
|
1021
|
+
return badge ? `${info.email} (${badge})` : info.email;
|
|
1022
|
+
}
|
|
1023
|
+
if (info.signedIn)
|
|
1024
|
+
return info.accountId ? `id:${info.accountId}` : 'signed in';
|
|
1025
|
+
return '';
|
|
1026
|
+
}
|
|
995
1027
|
/** Return the email address associated with the agent's auth config, or null. */
|
|
996
1028
|
export async function getAccountEmail(agentId, home) {
|
|
997
1029
|
const info = await getAccountInfo(agentId, home);
|