@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.
Files changed (53) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +30 -7
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/computer.d.ts +26 -0
  5. package/dist/commands/computer.js +173 -149
  6. package/dist/commands/doctor.js +5 -0
  7. package/dist/commands/exec.d.ts +18 -0
  8. package/dist/commands/exec.js +88 -6
  9. package/dist/commands/fork.d.ts +9 -0
  10. package/dist/commands/fork.js +67 -0
  11. package/dist/commands/run-account-picker.d.ts +14 -0
  12. package/dist/commands/run-account-picker.js +131 -0
  13. package/dist/commands/setup-browser.d.ts +18 -0
  14. package/dist/commands/setup-browser.js +142 -0
  15. package/dist/commands/setup-computer.d.ts +19 -0
  16. package/dist/commands/setup-computer.js +133 -0
  17. package/dist/commands/setup-share.d.ts +17 -0
  18. package/dist/commands/setup-share.js +85 -0
  19. package/dist/commands/setup.d.ts +1 -1
  20. package/dist/commands/setup.js +58 -1
  21. package/dist/commands/share.d.ts +15 -0
  22. package/dist/commands/share.js +10 -4
  23. package/dist/commands/ssh.js +202 -9
  24. package/dist/commands/view.d.ts +4 -14
  25. package/dist/commands/view.js +32 -30
  26. package/dist/index.js +2 -1
  27. package/dist/lib/agents.d.ts +10 -0
  28. package/dist/lib/agents.js +32 -0
  29. package/dist/lib/auth-health.d.ts +141 -0
  30. package/dist/lib/auth-health.js +277 -0
  31. package/dist/lib/browser/chrome.d.ts +9 -0
  32. package/dist/lib/browser/chrome.js +22 -0
  33. package/dist/lib/computer/download.d.ts +54 -0
  34. package/dist/lib/computer/download.js +146 -0
  35. package/dist/lib/computer-rpc.js +9 -3
  36. package/dist/lib/daemon.js +38 -0
  37. package/dist/lib/devices/connect.d.ts +15 -0
  38. package/dist/lib/devices/connect.js +17 -5
  39. package/dist/lib/devices/health-report.d.ts +11 -0
  40. package/dist/lib/devices/health-report.js +73 -4
  41. package/dist/lib/devices/stats-cache.d.ts +36 -0
  42. package/dist/lib/devices/stats-cache.js +115 -0
  43. package/dist/lib/devices/terminfo.d.ts +44 -0
  44. package/dist/lib/devices/terminfo.js +167 -0
  45. package/dist/lib/rotate.d.ts +12 -8
  46. package/dist/lib/rotate.js +22 -23
  47. package/dist/lib/session/fork.d.ts +32 -0
  48. package/dist/lib/session/fork.js +101 -0
  49. package/dist/lib/startup/command-registry.d.ts +1 -0
  50. package/dist/lib/startup/command-registry.js +2 -0
  51. package/dist/lib/usage.d.ts +23 -0
  52. package/dist/lib/usage.js +84 -0
  53. package/package.json +1 -1
@@ -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, probeLocalStats, probeFleetStats, } from '../lib/devices/health.js';
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 probeFleetStats(probeable, { selfName: self });
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
- // Probe only reachable devices, in parallel, bounded by the per-probe
423
- // timeout a slow box degrades to "—", it never hangs the table.
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
- const spinner = isInteractiveTerminal()
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
- statsMap = await probeFleetStats(probeable, { selfName: self });
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 },
@@ -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
- * Text for the account (email) column. Prefers the real email; otherwise, for a
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;
@@ -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, accountOrgBadge, getAllCliStates, getAccountInfo, resolveAgentName, formatAgentError, agentLabel, colorAgent, } from '../lib/agents.js';
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
- // Shown in the email column for agents that are signed in but expose no email
30
- // address locally (Antigravity stores an opaque OAuth grant with no identity).
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: options?.refresh === true });
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: options?.refresh === true });
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);
@@ -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). */
@@ -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);