@phnx-labs/agents-cli 1.20.73 → 1.20.76

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 (209) hide show
  1. package/CHANGELOG.md +989 -0
  2. package/README.md +36 -10
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/apply.js +21 -3
  5. package/dist/commands/attach.d.ts +10 -0
  6. package/dist/commands/attach.js +41 -0
  7. package/dist/commands/computer.js +3 -3
  8. package/dist/commands/detach-core.d.ts +46 -0
  9. package/dist/commands/detach-core.js +61 -0
  10. package/dist/commands/detach.d.ts +5 -0
  11. package/dist/commands/detach.js +171 -0
  12. package/dist/commands/doctor.js +31 -1
  13. package/dist/commands/exec.js +31 -3
  14. package/dist/commands/export.d.ts +11 -0
  15. package/dist/commands/export.js +215 -0
  16. package/dist/commands/feed.d.ts +7 -1
  17. package/dist/commands/feed.js +47 -4
  18. package/dist/commands/go.d.ts +9 -2
  19. package/dist/commands/go.js +16 -6
  20. package/dist/commands/harness.d.ts +14 -0
  21. package/dist/commands/harness.js +145 -0
  22. package/dist/commands/import.js +87 -9
  23. package/dist/commands/inspect.js +5 -2
  24. package/dist/commands/profiles.d.ts +20 -0
  25. package/dist/commands/profiles.js +68 -21
  26. package/dist/commands/repo.js +14 -0
  27. package/dist/commands/routines.js +15 -2
  28. package/dist/commands/secrets.js +44 -21
  29. package/dist/commands/sessions-browser.js +1 -1
  30. package/dist/commands/sessions-export.js +19 -5
  31. package/dist/commands/sessions-migrate.d.ts +29 -0
  32. package/dist/commands/sessions-migrate.js +596 -0
  33. package/dist/commands/sessions-picker.js +16 -1
  34. package/dist/commands/sessions.d.ts +36 -13
  35. package/dist/commands/sessions.js +210 -40
  36. package/dist/commands/setup-computer.js +2 -2
  37. package/dist/commands/setup.js +6 -1
  38. package/dist/commands/ssh.js +71 -35
  39. package/dist/commands/status.js +10 -2
  40. package/dist/commands/versions.js +51 -11
  41. package/dist/commands/view.d.ts +5 -0
  42. package/dist/commands/view.js +99 -31
  43. package/dist/commands/watchdog.d.ts +7 -2
  44. package/dist/commands/watchdog.js +73 -57
  45. package/dist/index.js +59 -7
  46. package/dist/lib/activity.d.ts +14 -4
  47. package/dist/lib/activity.js +17 -5
  48. package/dist/lib/actor.d.ts +50 -0
  49. package/dist/lib/actor.js +166 -0
  50. package/dist/lib/agent-spec/provider.js +2 -1
  51. package/dist/lib/agent-spec/resolve.js +19 -5
  52. package/dist/lib/agent-spec/types.d.ts +9 -1
  53. package/dist/lib/agents.d.ts +56 -0
  54. package/dist/lib/agents.js +133 -7
  55. package/dist/lib/cloud/codex.d.ts +2 -0
  56. package/dist/lib/cloud/codex.js +14 -3
  57. package/dist/lib/cloud/session-index.d.ts +32 -0
  58. package/dist/lib/cloud/session-index.js +58 -0
  59. package/dist/lib/cloud/store.d.ts +7 -0
  60. package/dist/lib/cloud/store.js +25 -0
  61. package/dist/lib/config-transfer.d.ts +16 -0
  62. package/dist/lib/config-transfer.js +60 -0
  63. package/dist/lib/daemon.js +18 -2
  64. package/dist/lib/devices/connect.d.ts +15 -1
  65. package/dist/lib/devices/connect.js +15 -1
  66. package/dist/lib/devices/fleet.d.ts +21 -0
  67. package/dist/lib/devices/fleet.js +26 -0
  68. package/dist/lib/devices/health-report.d.ts +38 -0
  69. package/dist/lib/devices/health-report.js +214 -0
  70. package/dist/lib/devices/health.js +4 -1
  71. package/dist/lib/devices/reachability.d.ts +31 -0
  72. package/dist/lib/devices/reachability.js +40 -0
  73. package/dist/lib/devices/registry.d.ts +33 -0
  74. package/dist/lib/devices/registry.js +37 -0
  75. package/dist/lib/devices/resolve-target.d.ts +15 -27
  76. package/dist/lib/devices/resolve-target.js +63 -102
  77. package/dist/lib/devices/sync.d.ts +18 -0
  78. package/dist/lib/devices/sync.js +23 -1
  79. package/dist/lib/devices/tailscale.d.ts +3 -0
  80. package/dist/lib/devices/tailscale.js +1 -0
  81. package/dist/lib/events.d.ts +1 -1
  82. package/dist/lib/exec.d.ts +66 -0
  83. package/dist/lib/exec.js +195 -6
  84. package/dist/lib/export.d.ts +72 -0
  85. package/dist/lib/export.js +269 -0
  86. package/dist/lib/feed-post.d.ts +63 -0
  87. package/dist/lib/feed-post.js +204 -0
  88. package/dist/lib/fleet/apply.d.ts +32 -2
  89. package/dist/lib/fleet/apply.js +97 -10
  90. package/dist/lib/fleet/types.d.ts +11 -0
  91. package/dist/lib/fs-walk.d.ts +13 -0
  92. package/dist/lib/fs-walk.js +16 -7
  93. package/dist/lib/hooks.d.ts +1 -0
  94. package/dist/lib/hooks.js +156 -0
  95. package/dist/lib/hosts/dispatch.d.ts +7 -0
  96. package/dist/lib/hosts/dispatch.js +2 -0
  97. package/dist/lib/hosts/passthrough.js +2 -0
  98. package/dist/lib/hosts/reconnect.d.ts +78 -0
  99. package/dist/lib/hosts/reconnect.js +127 -0
  100. package/dist/lib/hosts/registry.d.ts +75 -14
  101. package/dist/lib/hosts/registry.js +205 -30
  102. package/dist/lib/hosts/remote-cmd.js +1 -0
  103. package/dist/lib/hosts/run-target.d.ts +13 -5
  104. package/dist/lib/hosts/run-target.js +26 -10
  105. package/dist/lib/hosts/session-index.d.ts +18 -1
  106. package/dist/lib/hosts/session-index.js +37 -2
  107. package/dist/lib/hosts/session-marker.d.ts +33 -0
  108. package/dist/lib/hosts/session-marker.js +51 -0
  109. package/dist/lib/hosts/tasks.d.ts +8 -4
  110. package/dist/lib/import.d.ts +25 -0
  111. package/dist/lib/import.js +107 -2
  112. package/dist/lib/isolation-boundary-report.d.ts +10 -0
  113. package/dist/lib/isolation-boundary-report.js +35 -0
  114. package/dist/lib/menubar/install-menubar.d.ts +20 -0
  115. package/dist/lib/menubar/install-menubar.js +45 -1
  116. package/dist/lib/migrate.d.ts +16 -0
  117. package/dist/lib/migrate.js +36 -0
  118. package/dist/lib/models.d.ts +27 -0
  119. package/dist/lib/models.js +54 -1
  120. package/dist/lib/overdue.d.ts +7 -2
  121. package/dist/lib/overdue.js +18 -11
  122. package/dist/lib/picker.js +4 -1
  123. package/dist/lib/platform/process.d.ts +17 -0
  124. package/dist/lib/platform/process.js +70 -0
  125. package/dist/lib/profiles-presets.js +9 -7
  126. package/dist/lib/profiles.d.ts +31 -0
  127. package/dist/lib/profiles.js +70 -0
  128. package/dist/lib/project-resources.js +20 -5
  129. package/dist/lib/pty-server.d.ts +2 -10
  130. package/dist/lib/pty-server.js +4 -38
  131. package/dist/lib/registry.d.ts +1 -1
  132. package/dist/lib/registry.js +48 -8
  133. package/dist/lib/rotate.d.ts +18 -0
  134. package/dist/lib/rotate.js +28 -0
  135. package/dist/lib/routines.d.ts +26 -0
  136. package/dist/lib/routines.js +19 -2
  137. package/dist/lib/runner.js +104 -7
  138. package/dist/lib/secrets/agent.d.ts +66 -1
  139. package/dist/lib/secrets/agent.js +154 -90
  140. package/dist/lib/secrets/bundles.d.ts +34 -0
  141. package/dist/lib/secrets/bundles.js +179 -19
  142. package/dist/lib/secrets/index.d.ts +15 -1
  143. package/dist/lib/secrets/index.js +21 -3
  144. package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
  145. package/dist/lib/secrets/rc-hygiene.js +154 -0
  146. package/dist/lib/secrets/remote.d.ts +7 -4
  147. package/dist/lib/secrets/remote.js +7 -4
  148. package/dist/lib/secrets/sync-commands.d.ts +21 -0
  149. package/dist/lib/secrets/sync-commands.js +21 -0
  150. package/dist/lib/self-update.d.ts +23 -2
  151. package/dist/lib/self-update.js +86 -5
  152. package/dist/lib/session/active.d.ts +114 -7
  153. package/dist/lib/session/active.js +323 -52
  154. package/dist/lib/session/cloud.js +2 -1
  155. package/dist/lib/session/db.d.ts +48 -0
  156. package/dist/lib/session/db.js +238 -14
  157. package/dist/lib/session/detached.d.ts +30 -0
  158. package/dist/lib/session/detached.js +92 -0
  159. package/dist/lib/session/discover.d.ts +465 -2
  160. package/dist/lib/session/discover.js +1648 -468
  161. package/dist/lib/session/fork.js +2 -1
  162. package/dist/lib/session/hook-sessions.d.ts +43 -0
  163. package/dist/lib/session/hook-sessions.js +135 -0
  164. package/dist/lib/session/linear.d.ts +6 -0
  165. package/dist/lib/session/linear.js +55 -0
  166. package/dist/lib/session/migrate-targets.d.ts +65 -0
  167. package/dist/lib/session/migrate-targets.js +94 -0
  168. package/dist/lib/session/migrations.d.ts +37 -0
  169. package/dist/lib/session/migrations.js +60 -0
  170. package/dist/lib/session/parse.d.ts +18 -0
  171. package/dist/lib/session/parse.js +130 -30
  172. package/dist/lib/session/pid-registry.d.ts +34 -2
  173. package/dist/lib/session/pid-registry.js +49 -2
  174. package/dist/lib/session/render.d.ts +7 -2
  175. package/dist/lib/session/render.js +22 -10
  176. package/dist/lib/session/short-id.d.ts +17 -0
  177. package/dist/lib/session/short-id.js +20 -0
  178. package/dist/lib/session/state.js +6 -2
  179. package/dist/lib/shims.d.ts +27 -1
  180. package/dist/lib/shims.js +88 -6
  181. package/dist/lib/sqlite.d.ts +3 -2
  182. package/dist/lib/sqlite.js +27 -4
  183. package/dist/lib/staleness/detectors/commands.js +1 -1
  184. package/dist/lib/staleness/detectors/workflows.js +13 -2
  185. package/dist/lib/staleness/writers/commands.js +7 -7
  186. package/dist/lib/staleness/writers/workflows.d.ts +4 -2
  187. package/dist/lib/startup/command-registry.d.ts +2 -0
  188. package/dist/lib/startup/command-registry.js +5 -0
  189. package/dist/lib/state.d.ts +1 -1
  190. package/dist/lib/state.js +15 -36
  191. package/dist/lib/teams/agents.d.ts +19 -10
  192. package/dist/lib/teams/agents.js +40 -39
  193. package/dist/lib/types.d.ts +48 -2
  194. package/dist/lib/uninstall.js +1 -45
  195. package/dist/lib/usage.d.ts +33 -1
  196. package/dist/lib/usage.js +172 -12
  197. package/dist/lib/versions.d.ts +22 -2
  198. package/dist/lib/versions.js +98 -10
  199. package/dist/lib/watchdog/log.d.ts +43 -0
  200. package/dist/lib/watchdog/log.js +69 -0
  201. package/dist/lib/watchdog/routine.d.ts +44 -0
  202. package/dist/lib/watchdog/routine.js +69 -0
  203. package/dist/lib/watchdog/runner.d.ts +51 -7
  204. package/dist/lib/watchdog/runner.js +239 -64
  205. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  206. package/dist/lib/watchdog/watchdog.js +31 -16
  207. package/dist/lib/workflows.d.ts +16 -0
  208. package/dist/lib/workflows.js +110 -1
  209. package/package.json +1 -1
@@ -20,21 +20,22 @@ import { readAndResolveBundleEnv, isHeadlessSecretsContext } from '../lib/secret
20
20
  import { machineId } from '../lib/session/sync/config.js';
21
21
  import { registerFleetCaptureCommand } from './fleet-capture.js';
22
22
  import { registerFleetApplyAlias } from './apply.js';
23
- import { addIgnored, getDevice, loadDevices, loadIgnored, removeDevice, removeIgnored, upsertDevice, } from '../lib/devices/registry.js';
23
+ import { addIgnored, getDevice, loadDevices, loadIgnored, removeDevice, removeIgnored, upsertDevice, writeReachability, } from '../lib/devices/registry.js';
24
+ import { collectReachabilityWriteBacks, deviceOnlineState } from '../lib/devices/reachability.js';
24
25
  import { addControlToken } from '../lib/serve/token.js';
25
26
  import { DEFAULT_SERVE_PORT } from '../lib/serve/server.js';
26
27
  import { nodeToDeviceInput, parseTailscaleStatus, tailscaleStatusJson, } from '../lib/devices/tailscale.js';
27
- import { localLoginUser, planDeviceReconciliation, runDeviceSync, withDefaultUser } from '../lib/devices/sync.js';
28
+ import { defaultPickerChecked, localLoginUser, planDeviceReconciliation, runDeviceSync, withDefaultUser } from '../lib/devices/sync.js';
28
29
  import { resolveDeviceTarget, splitUserHost } from '../lib/devices/resolve-target.js';
29
30
  import { clearPendingSentinel } from '../lib/devices/pending.js';
30
31
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
31
32
  import { hostNameFor, renderSshConfig } from '../lib/devices/ssh-config.js';
32
- import { ASKPASS_BUNDLE_ENV, ASKPASS_KEY_ENV, buildSshInvocation, fleetDialTarget, writeAskpassShim, } from '../lib/devices/connect.js';
33
+ import { ASKPASS_BUNDLE_ENV, ASKPASS_KEY_ENV, ASKPASS_AGENT_ONLY_ENV, buildSshInvocation, fleetDialTarget, writeAskpassShim, } from '../lib/devices/connect.js';
33
34
  import { ensureManagedKnownHostsDir, isHostPinned } from '../lib/devices/known-hosts.js';
34
35
  import { shouldSyncTerminfo, syncTerminfoToDevice, terminfoHostKey } from '../lib/devices/terminfo.js';
35
- import { fanOutDevices, planFleetTargets, remoteFleetTargets, runFleet, skipLabel, upgradeCommand, } from '../lib/devices/fleet.js';
36
+ import { fanOutDevices, fleetHealthSkip, planFleetTargets, remoteFleetTargets, runFleet, skipLabel, upgradeCommand, } from '../lib/devices/fleet.js';
36
37
  import { fleetCapacity, fmtBytes, headroom, } from '../lib/devices/health.js';
37
- import { buildFleetHealthReport, renderFleetMatrix, renderFleetWarnings, } from '../lib/devices/health-report.js';
38
+ import { buildFleetHealthReport, renderFleetMatrix, renderFleetSummary, renderFleetWarnings, } from '../lib/devices/health-report.js';
38
39
  import { loadFleetStats, readStatsCache } from '../lib/devices/stats-cache.js';
39
40
  import { checkSyncStatus, countOrphans } from '../lib/drift.js';
40
41
  import { checkAllClis } from '../lib/teams/agents.js';
@@ -47,14 +48,17 @@ import { authCellColor, formatCheckedAge, isDeadVerdict, probeLocalFleetAuth, re
47
48
  import { runFleetLogin } from '../lib/fleet/remote-login.js';
48
49
  /** One-line summary of a device for `list`. `isSelf` marks the machine this
49
50
  * command is running on so it stands out from the rest of the tailnet. */
50
- function deviceSummary(d, isSelf = false) {
51
+ function deviceSummary(d, isSelf = false, stats) {
51
52
  const addr = hostNameFor(d) ?? chalk.gray('no address');
52
- const online = d.tailscale
53
- ? d.tailscale.online
54
- ? chalk.green('online')
55
- : chalk.gray('offline')
56
- : chalk.gray('unknown');
57
- const reach = d.tailscale?.online && !d.tailscale.direct ? chalk.yellow(' (relayed)') : '';
53
+ // Prefer a fresh live verdict (this run's probe, else the written-back
54
+ // reachability) over the stale tailscale.online snapshot (RUSH-1965).
55
+ const state = deviceOnlineState(d, stats);
56
+ const online = state === 'online'
57
+ ? chalk.green('online')
58
+ : state === 'offline'
59
+ ? chalk.gray('offline')
60
+ : chalk.gray('unknown');
61
+ const reach = state === 'online' && d.tailscale && !d.tailscale.direct ? chalk.yellow(' (relayed)') : '';
58
62
  const marker = isSelf ? chalk.cyan('▸ ') : ' ';
59
63
  const name = isSelf ? chalk.bold.cyan(d.name.padEnd(16)) : chalk.bold(d.name.padEnd(16));
60
64
  const here = isSelf ? chalk.cyan(' ← this machine') : '';
@@ -106,8 +110,11 @@ function renderDeviceTable(reg, names, self, statsMap, full = false) {
106
110
  const marker = isSelf ? chalk.cyan('▸ ') : ' ';
107
111
  const label = isSelf ? chalk.bold.cyan(name.padEnd(16)) : chalk.bold(name.padEnd(16));
108
112
  const plat = String(d.platform).padEnd(8);
109
- const offline = d.tailscale && !d.tailscale.online;
110
113
  const stats = statsMap.get(name);
114
+ // Prefer this run's live probe, then the written-back verdict, over the
115
+ // stale tailscale.online snapshot — so a reachable box never renders
116
+ // "offline" while its live load/mem sit one column over (RUSH-1965).
117
+ const offline = deviceOnlineState(d, stats) === 'offline';
111
118
  if (offline) {
112
119
  lines.push(`${marker}${label}${plat} ${chalk.gray('offline')}`);
113
120
  continue;
@@ -243,13 +250,16 @@ async function runInteractiveDeviceSync() {
243
250
  // Everything not already dismissed starts checked, so pressing Enter keeps
244
251
  // the fleet as-is (matching what auto-sync would register). Unchecking a
245
252
  // device removes it AND dismisses it so auto-sync never re-adds it.
253
+ // Sharee nodes (shared in by another user) start unchecked unless already
254
+ // registered — registering one must be a deliberate check, never the
255
+ // default Enter.
246
256
  message: 'Your fleet — uncheck a device to remove and stop suggesting it:',
247
257
  pageSize: Math.min(nodes.length, 20),
248
258
  choices: nodes.map((n) => {
249
- const flags = [n.platform, n.online ? undefined : 'offline', ignored.has(n.name) ? 'ignored' : undefined]
259
+ const flags = [n.platform, n.online ? undefined : 'offline', n.sharee ? 'shared' : undefined, ignored.has(n.name) ? 'ignored' : undefined]
250
260
  .filter(Boolean)
251
261
  .join(', ');
252
- return { value: n.name, name: `${n.name} ${chalk.gray(`(${flags})`)}`, checked: !ignored.has(n.name) };
262
+ return { value: n.name, name: `${n.name} ${chalk.gray(`(${flags})`)}`, checked: defaultPickerChecked(n, registered, ignored) };
253
263
  }),
254
264
  });
255
265
  }
@@ -347,26 +357,23 @@ async function runFleetStatus(opts) {
347
357
  const statsMap = opts.stats === false
348
358
  ? new Map()
349
359
  : (await loadFleetStats(probeable, { forceRefresh, selfName: self })).stats;
360
+ // Persist the live probe's reachability verdict so the online/offline word is
361
+ // read from a fresh probe, not a stale tailscale snapshot (RUSH-1965).
362
+ // Best-effort: a registry write must never break the status render.
363
+ await writeReachability(collectReachabilityWriteBacks(reg, statsMap)).catch(() => { });
350
364
  const rows = [localHealthRow(self, statsMap.get(self))];
351
365
  const remoteTargets = remoteFleetTargets(planned, self)
352
366
  .map((t) => ({
353
367
  name: t.device.name,
354
368
  platform: t.device.platform,
355
- skip: t.skip,
369
+ // Fail fast: gate the expensive version+doctor dials on the reachability
370
+ // verdict the cheap stats probe already computed one step earlier. A box
371
+ // it found unreachable skips straight to an `unreachable` row instead of
372
+ // burning 15s+30s per box — so one genuinely-offline device can't stall
373
+ // the matrix for ~60s (RUSH-1964). See {@link fleetHealthSkip} for why
374
+ // this is trusted on the default path, not just under `--refresh`.
375
+ skip: fleetHealthSkip(t.skip, statsMap.get(t.device.name)),
356
376
  dialTarget: fleetDialTarget(t.device),
357
- // Fail fast: the stats probe already tried this box with the same
358
- // (registry) address. If it came back unreachable, don't spend another
359
- // 15s+30s on version+doctor — skip straight to an unreachable row so one
360
- // genuinely-offline device can't stall the whole matrix.
361
- //
362
- // Only trust this on a FRESH probe (`--refresh`/`--live`). loadFleetStats
363
- // is cache-first (daemon-warmed ~3min), so a box that came back online
364
- // since the last warm would otherwise be skipped to "unreachable" without
365
- // a live attempt — a false negative. On a default run we still do the live
366
- // probe (fast now that it dials the registry address).
367
- ...(forceRefresh && statsMap.get(t.device.name)?.reachable === false && !t.skip
368
- ? { skip: 'unreachable' }
369
- : {}),
370
377
  }));
371
378
  const remote = await fanOutDevices(remoteTargets, probeRemoteHealth);
372
379
  for (const result of remote) {
@@ -400,18 +407,32 @@ async function runFleetStatus(opts) {
400
407
  for (const row of rows) {
401
408
  if (!row.auth)
402
409
  row.auth = summarizeHostAuth(authCache, row.name);
410
+ // Resolve the online/offline verdict and last-seen once (RUSH-1966) so the
411
+ // summary view reads one truth — the same `deviceOnlineState` ordering the
412
+ // registry write-back uses — instead of re-deriving it from error/skipped.
413
+ const profile = reg[row.name];
414
+ if (profile) {
415
+ row.online = deviceOnlineState(profile, statsMap.get(row.name));
416
+ row.lastSeen = profile.tailscale?.lastSeen ?? profile.reachability?.checkedAt;
417
+ }
403
418
  }
404
419
  const report = buildFleetHealthReport(rows);
405
420
  if (opts.json) {
406
421
  console.log(JSON.stringify(report, null, 2));
407
422
  }
408
- else {
423
+ else if (opts.verbose) {
424
+ // Full grid: the auth/CLI/sync/version columns and the warnings rollup.
409
425
  for (const line of renderFleetWarnings(report))
410
426
  console.log(line);
411
427
  console.log();
412
428
  for (const line of renderFleetMatrix(report))
413
429
  console.log(line);
414
430
  }
431
+ else {
432
+ // Default: rollup + NEEDS ATTENTION + OS-grouped quiet rows + footer.
433
+ for (const line of renderFleetSummary(report, { self }))
434
+ console.log(line);
435
+ }
415
436
  if (opts.strict && report.hasWarnings)
416
437
  process.exitCode = 1;
417
438
  }
@@ -737,6 +758,10 @@ Typical workflow:
737
758
  finally {
738
759
  spinner?.stop();
739
760
  }
761
+ // Write the live verdict back so this and every other consumer read one
762
+ // reachability truth instead of the stale tailscale snapshot (RUSH-1965).
763
+ if (statsMap)
764
+ await writeReachability(collectReachabilityWriteBacks(reg, statsMap)).catch(() => { });
740
765
  }
741
766
  console.log(chalk.bold(`Devices (${names.length})`));
742
767
  for (const line of renderDeviceTable(reg, names, self, statsMap, opts.full))
@@ -766,14 +791,20 @@ Typical workflow:
766
791
  .action(runList);
767
792
  devicesCmd
768
793
  .command('status')
769
- .description('Show fleet health: warnings rollup, per-device sync drift, CLI readiness, version skew, and resource headroom.')
794
+ .description('Fleet health at a glance: online/offline rollup, a NEEDS ATTENTION list (each with its fix command), and quiet per-device rows grouped by OS. Use --verbose for the full auth/CLI/sync grid.')
770
795
  .option('--json', 'output machine-readable JSON')
771
796
  .option('--strict', 'exit non-zero when any device has drift or is unreachable')
772
797
  .option('--no-stats', 'skip the live resource probe')
773
798
  .option('--refresh', 'force a live probe of every device, bypassing the cache')
774
799
  .option('--live', 'alias of --refresh (shorter to type)')
775
- .action(async (opts) => {
776
- await runFleetStatus(opts);
800
+ .option('--verbose', 'show the full per-device auth/CLI/sync/version grid instead of the summary')
801
+ .action(async (opts, cmd) => {
802
+ // The root program also defines a global `--verbose`; commander binds a
803
+ // shared long flag to the program, not the leaf. Read the effective value
804
+ // from the merged globals so `fleet status --verbose` works at either level
805
+ // (same pattern as `fleet ping --verbose`).
806
+ const verbose = opts.verbose ?? Boolean(cmd.optsWithGlobals().verbose);
807
+ await runFleetStatus({ ...opts, verbose });
777
808
  });
778
809
  devicesCmd
779
810
  .command('ping')
@@ -1014,7 +1045,7 @@ secrets bundle via an askpass shim — the password never touches argv.
1014
1045
  // `user@device` (same device, login user overridden — dialed via its
1015
1046
  // Tailscale route, not LAN DNS), or an ad-hoc `user@host`/`host` literal.
1016
1047
  // A bare unregistered alias still errors as "Unknown device".
1017
- const device = resolveDeviceTarget(name, await loadDevices());
1048
+ const device = await resolveDeviceTarget(name);
1018
1049
  if (!device) {
1019
1050
  // Not a registered device — it may be a leased crabbox box slug. ssh into
1020
1051
  // it directly (crabbox@<tailnet|ip>:2222) before giving up.
@@ -1076,8 +1107,13 @@ async function runAskpass() {
1076
1107
  console.error(`askpass: ${ASKPASS_BUNDLE_ENV} not set`);
1077
1108
  process.exit(1);
1078
1109
  }
1110
+ // A read-only stats probe sets ASKPASS_AGENT_ONLY_ENV to force a broker-only
1111
+ // resolve even under a TTY — so `agents devices` never pops Touch ID just to
1112
+ // render load/mem for an uncached password-auth device (RUSH-1970). Otherwise
1113
+ // fall back to the headless-context heuristic.
1114
+ const agentOnly = process.env[ASKPASS_AGENT_ONLY_ENV] === '1' || isHeadlessSecretsContext();
1079
1115
  try {
1080
- const { env } = readAndResolveBundleEnv(bundle, { caller: 'agents ssh', keys: [key], keyMode: 'storage', agentOnly: isHeadlessSecretsContext() });
1116
+ const { env } = readAndResolveBundleEnv(bundle, { caller: 'agents ssh', keys: [key], keyMode: 'storage', agentOnly });
1081
1117
  const value = env[key];
1082
1118
  if (value === undefined) {
1083
1119
  console.error(`askpass: key '${key}' not found in bundle '${bundle}'`);
@@ -13,6 +13,7 @@ import { AGENTS } from '../lib/agents.js';
13
13
  import { setHelpSections } from '../lib/help.js';
14
14
  import { computeSyncStatus } from '../lib/sync-status.js';
15
15
  import { promptDriftSync } from '../lib/drift-sync.js';
16
+ import { resolveConfiguredModel } from '../lib/models.js';
16
17
  import { resolveSurface } from './utils.js';
17
18
  const agentName = (id) => AGENTS[id]?.name ?? id;
18
19
  function versionSummary(v) {
@@ -75,8 +76,15 @@ export function registerStatusCommand(program) {
75
76
  console.log(chalk.gray(' (no installed agent versions)'));
76
77
  }
77
78
  for (const v of status.agents) {
78
- const label = `${agentName(v.agent)}@${v.version}${v.isDefault ? chalk.gray(' (default)') : ''}`;
79
- console.log(` ${label.padEnd(28)} ${versionSummary(v)}`);
79
+ // Model sits right beside the version, same priority (no label).
80
+ const model = resolveConfiguredModel(v.agent, v.version)?.model;
81
+ const defaultTag = v.isDefault ? ' (default)' : '';
82
+ const plain = `${agentName(v.agent)}@${v.version}${model ? ` · ${model}` : ''}${defaultTag}`;
83
+ const shown = `${agentName(v.agent)}@${v.version}`
84
+ + (model ? ` ${chalk.gray('·')} ${chalk.yellow(model)}` : '')
85
+ + (v.isDefault ? chalk.gray(' (default)') : '');
86
+ const pad = ' '.repeat(Math.max(1, 34 - plain.length));
87
+ console.log(` ${shown}${pad}${versionSummary(v)}`);
80
88
  }
81
89
  if (status.totals.orphan > 0) {
82
90
  console.log(chalk.gray(` (${status.totals.orphan} orphan${status.totals.orphan === 1 ? '' : 's'} — run \`agents prune cleanup\`)`));
@@ -7,10 +7,11 @@ import { select, confirm, checkbox } from '@inquirer/prompts';
7
7
  import { AGENTS, ALL_AGENT_IDS, accountOrgBadge, getAccountEmail, getAccountInfo, agentLabel, warnAgentDeprecated, isSelfUpdatingAgent, } from '../lib/agents.js';
8
8
  import { formatUsageSummary, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
9
9
  import { viewAction } from './view.js';
10
+ import { resolveConfiguredModel, formatAgentIdentity } from '../lib/models.js';
10
11
  import { readManifest, writeManifest, createDefaultManifest } from '../lib/manifest.js';
11
- import { installVersion, removeVersion, listInstalledVersions, isVersionInstalled, isLatestInstalled, isOldestInstalled, getGlobalDefault, setGlobalDefault, markVersionIsolated, isVersionIsolated, getVersionHomePath, getVersionDir, syncResourcesToVersion, parseAgentSpec, promptResourceSelection, promptNewResourceSelection, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, printTrashFooter, } from '../lib/versions.js';
12
+ import { installVersion, removeVersion, listInstalledVersions, isVersionInstalled, isLatestInstalled, isOldestInstalled, getGlobalDefault, setGlobalDefault, markVersionIsolated, setIsolatedDefault, isVersionIsolated, getVersionHomePath, getVersionDir, syncResourcesToVersion, parseAgentSpec, promptResourceSelection, promptNewResourceSelection, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, printTrashFooter, } from '../lib/versions.js';
12
13
  import { carryForwardSettings } from '../lib/settings-manifest.js';
13
- import { createShim, createVersionedAlias, supportsIsolatedInstall, CONFIG_ENV_ISOLATED_AGENTS, removeShim, shimExists, getShimsDir, getShimPath, getPathShadowingExecutable, isShimsInPath, getPathSetupInstructions, addShimsToPath, switchConfigSymlink, switchHomeFileSymlinks, } from '../lib/shims.js';
14
+ import { createShim, createVersionedAlias, supportsIsolatedInstall, isIsolationProtected, CONFIG_ENV_ISOLATED_AGENTS, removeShim, shimExists, getShimsDir, getShimPath, getPathShadowingExecutable, isShimsInPath, getPathSetupInstructions, addShimsToPath, switchConfigSymlink, switchHomeFileSymlinks, } from '../lib/shims.js';
14
15
  import { isInteractiveTerminal, isPromptCancelled, requireInteractiveSelection } from './utils.js';
15
16
  import { tryAutoPull } from '../lib/git.js';
16
17
  import { getAgentsDir, getTrashVersionsDir } from '../lib/state.js';
@@ -342,6 +343,20 @@ export function registerVersionsCommands(program) {
342
343
  // from the user's real ~/.<agent>. Agents without one isolate only by
343
344
  // adopting ~/.<agent> (which --isolated skips), so an isolated copy
344
345
  // would silently read/write the real config. Refuse rather than lie.
346
+ // A normal install of a currently isolated-only agent would adopt it: set the
347
+ // global default, create the bare shim, and repoint the real ~/.<agent>. The
348
+ // primitives refuse that outright, so catch it here — before spending a
349
+ // network install on something that cannot finish — and say what to do.
350
+ if (!isIsolated && isIsolationProtected(agent)) {
351
+ console.log(chalk.red(`${agentLabel(agentConfig.id)} is installed only as isolated copies.`));
352
+ console.log(chalk.gray(` A normal install would adopt ${agentConfig.configDir} and your ${agentConfig.cliCommand} launcher.`));
353
+ console.log(chalk.gray(` Keep the sandbox: agents add ${agent}@${version ?? 'latest'} --isolated`));
354
+ console.log(chalk.gray(' Or manage it normally by removing the isolated copies first:'));
355
+ for (const v of listInstalledVersions(agent)) {
356
+ console.log(chalk.gray(` agents remove ${agent}@${v} --isolated`));
357
+ }
358
+ continue;
359
+ }
345
360
  if (isIsolated && !supportsIsolatedInstall(agent)) {
346
361
  console.log(chalk.red(`${agentLabel(agentConfig.id)} does not support --isolated installs.`));
347
362
  console.log(chalk.gray(` It has no config-directory env var, so it can only isolate by adopting ${agentConfig.configDir} — which --isolated deliberately avoids.`));
@@ -396,7 +411,10 @@ export function registerVersionsCommands(program) {
396
411
  spinner.text = msg;
397
412
  });
398
413
  if (result.success) {
399
- spinner.succeed(`Installed ${agentLabel(agentConfig.id)}@${result.installedVersion}`);
414
+ const installedVer = result.installedVersion || version;
415
+ const installedModel = resolveConfiguredModel(agentConfig.id, installedVer)?.model;
416
+ const installedIdentity = formatAgentIdentity(`${agentLabel(agentConfig.id)}@${result.installedVersion}`, installedModel ? chalk.yellow(installedModel) : null);
417
+ spinner.succeed(`Installed ${installedIdentity}`);
400
418
  const installedVersion = result.installedVersion || version;
401
419
  // Track the concrete version so a `--project` pin records it instead
402
420
  // of the `latest`/`oldest` alias.
@@ -705,10 +723,27 @@ export function registerVersionsCommands(program) {
705
723
  // Refuse for both the explicit `use <agent>@<isolated>` path (the picker
706
724
  // above already filters isolated versions out of the interactive path).
707
725
  // Isolated copies are launched explicitly via `agents run <agent>@<v>`.
726
+ // ...so `use` is scoped to the sandbox rather than refused. Setting the
727
+ // ISOLATED default records which copy a bare `agents run <agent>` should
728
+ // reach, and touches none of the five adopting side effects above. The
729
+ // pointer lives in `meta.isolatedAgents`, never in `meta.agents`, so
730
+ // `getGlobalDefault` still cannot return an isolated version.
708
731
  if (isVersionIsolated(agentId, finalVersion)) {
709
- console.log(chalk.red(`${agentLabel(agentConfig.id)}@${finalVersion} is an isolated install and can't be set as your active version.`));
710
- console.log(chalk.gray(`Isolated copies stay walled off from your real ${agentConfig.configDir}. Run it directly: agents run ${agentId}@${finalVersion}`));
711
- console.log(chalk.gray(`To install this version normally: agents add ${agentId}@${finalVersion}`));
732
+ if (options.project) {
733
+ console.log(chalk.yellow(`${agentLabel(agentConfig.id)}@${finalVersion} is an isolated install; --project pins are for shared versions.`));
734
+ console.log(chalk.gray(`Run it directly instead: agents run ${agentId}@${finalVersion}`));
735
+ return;
736
+ }
737
+ setIsolatedDefault(agentId, finalVersion);
738
+ console.log(chalk.green(`Set ${agentLabel(agentConfig.id)}@${finalVersion} as your default ISOLATED copy.`));
739
+ console.log(chalk.gray(` agents run ${agentId} now reaches it (no @version needed).`));
740
+ const globalDefault = getGlobalDefault(agentId);
741
+ if (globalDefault) {
742
+ console.log(chalk.gray(` Your default ${agentConfig.cliCommand} is still ${globalDefault}; ${agentConfig.configDir} is untouched.`));
743
+ }
744
+ else {
745
+ console.log(chalk.gray(` ${agentConfig.configDir} and your ${agentConfig.cliCommand} launcher are untouched.`));
746
+ }
712
747
  return;
713
748
  }
714
749
  if (options.project) {
@@ -725,8 +760,9 @@ export function registerVersionsCommands(program) {
725
760
  manifest.agents[agentId] = finalVersion;
726
761
  writeManifest(process.cwd(), manifest);
727
762
  const projEmail = await getAccountEmail(agentId, getVersionHomePath(agentId, finalVersion));
728
- const projEmailStr = projEmail ? chalk.cyan(` (${projEmail})`) : '';
729
- console.log(chalk.green(`Set ${agentLabel(agentConfig.id)}@${finalVersion} for this project`) + projEmailStr);
763
+ const projModel = resolveConfiguredModel(agentId, finalVersion)?.model;
764
+ const projIdentity = formatAgentIdentity(chalk.green(`${agentLabel(agentConfig.id)}@${finalVersion}`), projModel ? chalk.yellow(projModel) : null, projEmail ? chalk.cyan(projEmail) : null);
765
+ console.log(`Set ${projIdentity} for this project`);
730
766
  }
731
767
  else {
732
768
  // Smart resource detection: compare available vs ACTUALLY synced (source of truth: files, not tracking)
@@ -864,15 +900,19 @@ export function registerVersionsCommands(program) {
864
900
  switchHomeFileSymlinks(agentId, finalVersion);
865
901
  warnIfShimShadowed(agentId);
866
902
  const useEmail = await getAccountEmail(agentId, getVersionHomePath(agentId, finalVersion));
867
- const useEmailStr = useEmail ? chalk.cyan(` (${useEmail})`) : '';
903
+ const useModel = resolveConfiguredModel(agentId, finalVersion)?.model;
904
+ const useModelStr = useModel ? chalk.yellow(useModel) : null;
905
+ const useAcctStr = useEmail ? chalk.cyan(useEmail) : null;
868
906
  // Self-updating agents are one binary; `use` only swaps the config
869
907
  // symlink (the profile), it does not change which binary runs — so say
870
908
  // "profile", not "version", to match what actually happened.
871
909
  if (isSelfUpdatingAgent(agentId)) {
872
- console.log(chalk.green(`Switched ${agentLabel(agentConfig.id)} to config profile ${finalVersion}`) + useEmailStr);
910
+ const identity = formatAgentIdentity(chalk.green(agentLabel(agentConfig.id)), useModelStr, useAcctStr);
911
+ console.log(`Switched ${identity} to config profile ${chalk.green(finalVersion)}`);
873
912
  }
874
913
  else {
875
- console.log(chalk.green(`Set ${agentLabel(agentConfig.id)}@${finalVersion} as global default`) + useEmailStr);
914
+ const identity = formatAgentIdentity(chalk.green(`${agentLabel(agentConfig.id)}@${finalVersion}`), useModelStr, useAcctStr);
915
+ console.log(`Set ${identity} as global default`);
876
916
  }
877
917
  }
878
918
  }
@@ -10,6 +10,7 @@ import type { Command } from 'commander';
10
10
  import { accountDisplayLabel } from '../lib/agents.js';
11
11
  import type { AccountInfo } from '../lib/agents.js';
12
12
  import type { AgentId } from '../lib/types.js';
13
+ import { type ConfiguredModelSource } from '../lib/models.js';
13
14
  import { type ProfileSummary } from '../lib/profiles.js';
14
15
  /** Shared account identity formatter, re-exported for the view-specific tests. */
15
16
  export declare const accountColumnLabel: typeof accountDisplayLabel;
@@ -51,6 +52,10 @@ export interface ViewJsonVersion {
51
52
  }>;
52
53
  lastActive: string | null;
53
54
  path: string;
55
+ configuredModel?: {
56
+ model: string;
57
+ source: ConfiguredModelSource;
58
+ } | null;
54
59
  /** Present only when --resources / --detailed (or --json with a section
55
60
  * flag) is passed: this version's resource inventory with git sync-state. */
56
61
  resources?: VersionResourcesJson;