@phnx-labs/agents-cli 1.22.25 → 1.22.27

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 (178) hide show
  1. package/CHANGELOG.md +427 -0
  2. package/README.md +35 -3
  3. package/dist/bin/agents +0 -0
  4. package/dist/browser.js +14 -4
  5. package/dist/commands/apply.js +52 -8
  6. package/dist/commands/bench.d.ts +2 -0
  7. package/dist/commands/bench.js +101 -0
  8. package/dist/commands/browser.js +35 -0
  9. package/dist/commands/doctor.js +32 -10
  10. package/dist/commands/exec.js +17 -16
  11. package/dist/commands/insights.d.ts +25 -19
  12. package/dist/commands/insights.js +146 -38
  13. package/dist/commands/output.js +100 -25
  14. package/dist/commands/reconnect.d.ts +46 -0
  15. package/dist/commands/reconnect.js +109 -0
  16. package/dist/commands/resume.d.ts +11 -0
  17. package/dist/commands/resume.js +51 -0
  18. package/dist/commands/routines.js +2 -2
  19. package/dist/commands/secrets.d.ts +2 -8
  20. package/dist/commands/secrets.js +29 -105
  21. package/dist/commands/sessions-picker.js +12 -7
  22. package/dist/commands/sessions.d.ts +47 -1
  23. package/dist/commands/sessions.js +349 -37
  24. package/dist/commands/setup-secrets.d.ts +1 -0
  25. package/dist/commands/setup-secrets.js +1 -1
  26. package/dist/commands/setup.d.ts +26 -3
  27. package/dist/commands/setup.js +105 -46
  28. package/dist/commands/ssh.js +184 -11
  29. package/dist/commands/teams.d.ts +6 -0
  30. package/dist/commands/teams.js +46 -3
  31. package/dist/commands/trends.d.ts +8 -0
  32. package/dist/commands/trends.js +10 -156
  33. package/dist/index.js +17 -21
  34. package/dist/lib/agents.d.ts +11 -0
  35. package/dist/lib/agents.js +29 -2
  36. package/dist/lib/analytics/dashboard.d.ts +10 -6
  37. package/dist/lib/analytics/dashboard.js +6 -4
  38. package/dist/lib/analytics/mix-commands.d.ts +53 -0
  39. package/dist/lib/analytics/mix-commands.js +229 -0
  40. package/dist/lib/analytics/recipes.d.ts +19 -14
  41. package/dist/lib/analytics/recipes.js +4 -2
  42. package/dist/lib/auth-health.d.ts +47 -3
  43. package/dist/lib/auth-health.js +74 -15
  44. package/dist/lib/bench/index.d.ts +4 -0
  45. package/dist/lib/bench/index.js +4 -0
  46. package/dist/lib/bench/runner.d.ts +16 -0
  47. package/dist/lib/bench/runner.js +111 -0
  48. package/dist/lib/bench/schema.d.ts +5 -0
  49. package/dist/lib/bench/schema.js +91 -0
  50. package/dist/lib/bench/storage.d.ts +5 -0
  51. package/dist/lib/bench/storage.js +32 -0
  52. package/dist/lib/bench/types.d.ts +40 -0
  53. package/dist/lib/bench/types.js +1 -0
  54. package/dist/lib/browser/ipc.d.ts +26 -0
  55. package/dist/lib/browser/ipc.js +139 -24
  56. package/dist/lib/browser/profiles.d.ts +11 -0
  57. package/dist/lib/browser/profiles.js +1 -1
  58. package/dist/lib/browser/stream.d.ts +14 -0
  59. package/dist/lib/browser/stream.js +71 -0
  60. package/dist/lib/channels/owner-sink.d.ts +27 -0
  61. package/dist/lib/channels/owner-sink.js +93 -0
  62. package/dist/lib/claude-account-token.d.ts +2 -0
  63. package/dist/lib/claude-account-token.js +27 -1
  64. package/dist/lib/crabbox/cli.d.ts +2 -0
  65. package/dist/lib/crabbox/cli.js +2 -0
  66. package/dist/lib/crabbox/lease.js +7 -1
  67. package/dist/lib/daemon.js +20 -0
  68. package/dist/lib/devices/connect.d.ts +2 -0
  69. package/dist/lib/devices/connect.js +7 -0
  70. package/dist/lib/devices/doctor-findings.d.ts +10 -1
  71. package/dist/lib/devices/doctor-findings.js +47 -1
  72. package/dist/lib/devices/harness-inventory.d.ts +97 -0
  73. package/dist/lib/devices/harness-inventory.js +0 -0
  74. package/dist/lib/devices/registry.d.ts +2 -0
  75. package/dist/lib/devices/ssh-config.js +3 -0
  76. package/dist/lib/devices/windows-ssh-enrollment.d.ts +20 -0
  77. package/dist/lib/devices/windows-ssh-enrollment.js +98 -0
  78. package/dist/lib/exec.bench.d.ts +1 -0
  79. package/dist/lib/exec.bench.js +186 -0
  80. package/dist/lib/exec.js +18 -8
  81. package/dist/lib/fleet/apply.d.ts +59 -3
  82. package/dist/lib/fleet/apply.js +191 -12
  83. package/dist/lib/fleet/remote-login.d.ts +4 -3
  84. package/dist/lib/fleet/remote-login.js +11 -9
  85. package/dist/lib/fleet/types.d.ts +21 -2
  86. package/dist/lib/gemini-settings.d.ts +0 -1
  87. package/dist/lib/gemini-settings.js +12 -7
  88. package/dist/lib/hooks/cache.js +15 -0
  89. package/dist/lib/hooks.d.ts +8 -5
  90. package/dist/lib/hooks.js +10 -6
  91. package/dist/lib/hosts/dispatch.js +7 -3
  92. package/dist/lib/hosts/passthrough.d.ts +45 -0
  93. package/dist/lib/hosts/passthrough.js +59 -8
  94. package/dist/lib/hosts/progress.d.ts +2 -1
  95. package/dist/lib/hosts/progress.js +4 -3
  96. package/dist/lib/hosts/providers/devices.js +1 -0
  97. package/dist/lib/hosts/ready.d.ts +6 -2
  98. package/dist/lib/hosts/ready.js +33 -8
  99. package/dist/lib/hosts/reconcile.d.ts +1 -1
  100. package/dist/lib/hosts/reconcile.js +16 -7
  101. package/dist/lib/hosts/reconnect.d.ts +56 -21
  102. package/dist/lib/hosts/reconnect.js +128 -71
  103. package/dist/lib/hosts/registry.d.ts +2 -2
  104. package/dist/lib/hosts/registry.js +3 -5
  105. package/dist/lib/hosts/remote-cmd.d.ts +17 -0
  106. package/dist/lib/hosts/remote-cmd.js +29 -0
  107. package/dist/lib/hosts/tasks.d.ts +2 -0
  108. package/dist/lib/hosts/types.d.ts +1 -0
  109. package/dist/lib/hosts/types.js +3 -0
  110. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  111. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  112. package/dist/lib/menubar/install-menubar.d.ts +9 -6
  113. package/dist/lib/menubar/install-menubar.js +20 -9
  114. package/dist/lib/models.d.ts +30 -1
  115. package/dist/lib/models.js +42 -144
  116. package/dist/lib/pricing/cost.d.ts +9 -0
  117. package/dist/lib/pricing/cost.js +24 -0
  118. package/dist/lib/pricing/index.d.ts +1 -1
  119. package/dist/lib/pricing/index.js +1 -1
  120. package/dist/lib/redact.js +8 -3
  121. package/dist/lib/remote-agents-json.d.ts +32 -0
  122. package/dist/lib/remote-agents-json.js +47 -16
  123. package/dist/lib/resource-profiles.js +1 -2
  124. package/dist/lib/routine-notify-owner.d.ts +102 -0
  125. package/dist/lib/routine-notify-owner.js +232 -0
  126. package/dist/lib/routines.d.ts +10 -0
  127. package/dist/lib/routines.js +14 -2
  128. package/dist/lib/runner.d.ts +9 -4
  129. package/dist/lib/runner.js +182 -30
  130. package/dist/lib/sandbox.d.ts +0 -2
  131. package/dist/lib/sandbox.js +2 -19
  132. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  133. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  134. package/dist/lib/secrets/agent.js +3 -2
  135. package/dist/lib/secrets/lease.d.ts +25 -0
  136. package/dist/lib/secrets/lease.js +44 -0
  137. package/dist/lib/secrets/push.d.ts +94 -0
  138. package/dist/lib/secrets/push.js +145 -0
  139. package/dist/lib/secrets/reaper.d.ts +15 -1
  140. package/dist/lib/secrets/reaper.js +30 -3
  141. package/dist/lib/self-update.d.ts +20 -5
  142. package/dist/lib/self-update.js +93 -16
  143. package/dist/lib/session/db.d.ts +38 -3
  144. package/dist/lib/session/db.js +273 -16
  145. package/dist/lib/session/discover.d.ts +22 -1
  146. package/dist/lib/session/discover.js +239 -51
  147. package/dist/lib/session/insights.d.ts +37 -0
  148. package/dist/lib/session/insights.js +219 -9
  149. package/dist/lib/session/recovery.d.ts +17 -1
  150. package/dist/lib/session/recovery.js +111 -4
  151. package/dist/lib/session/remote-list.d.ts +10 -9
  152. package/dist/lib/session/remote-list.js +25 -23
  153. package/dist/lib/session/resume-owner.d.ts +55 -0
  154. package/dist/lib/session/resume-owner.js +69 -0
  155. package/dist/lib/session/team-filter.d.ts +65 -0
  156. package/dist/lib/session/team-filter.js +98 -3
  157. package/dist/lib/session/tool-index.js +133 -22
  158. package/dist/lib/session/tool-store.d.ts +26 -2
  159. package/dist/lib/session/tool-store.js +36 -17
  160. package/dist/lib/session/types.d.ts +33 -0
  161. package/dist/lib/smart-launch.d.ts +31 -5
  162. package/dist/lib/smart-launch.js +43 -7
  163. package/dist/lib/ssh-exec.d.ts +27 -0
  164. package/dist/lib/ssh-exec.js +42 -3
  165. package/dist/lib/ssh-tunnel.d.ts +3 -2
  166. package/dist/lib/ssh-tunnel.js +25 -16
  167. package/dist/lib/startup/command-registry.d.ts +2 -0
  168. package/dist/lib/startup/command-registry.js +6 -0
  169. package/dist/lib/teams/agents.d.ts +13 -0
  170. package/dist/lib/teams/agents.js +75 -7
  171. package/dist/lib/teams/placement-probe.d.ts +21 -0
  172. package/dist/lib/teams/placement-probe.js +135 -0
  173. package/dist/lib/teams/scheduler.d.ts +74 -1
  174. package/dist/lib/teams/scheduler.js +187 -10
  175. package/dist/lib/tmux/session.d.ts +8 -0
  176. package/dist/lib/tmux/session.js +22 -0
  177. package/dist/lib/types.d.ts +2 -0
  178. package/package.json +1 -1
@@ -26,6 +26,13 @@ import { registerSetupSecretsCommand } from './setup-secrets.js';
26
26
  import { registerSetupFleetCommand } from './setup-fleet.js';
27
27
  import { registerSetupWatchdogCommand, runWatchdogSetupWizard } from './setup-watchdog.js';
28
28
  import { runPreferencesStep } from './setup-preferences.js';
29
+ import { getConfiguredDefaultProfileName, getProfile, isProfileLaunchableHere, DEFAULT_BROWSER_PROFILE_NAME } from '../lib/browser/profiles.js';
30
+ import { listInstalledBrowsers } from '../lib/browser/chrome.js';
31
+ import { probeComputerTrust } from './computer.js';
32
+ import { readShareConfig } from '../lib/share/config.js';
33
+ import { loadDevices } from '../lib/devices/registry.js';
34
+ import { getConfigValue } from '../lib/device-config.js';
35
+ import { setupSecretsPrefsPath } from './setup-secrets.js';
29
36
  const HOME = os.homedir();
30
37
  /**
31
38
  * Import an existing unmanaged agent installation into agents-cli.
@@ -70,9 +77,9 @@ export async function runSetup(program, options = {}) {
70
77
  const agentsDir = getAgentsDir();
71
78
  const alreadyConfigured = isGitRepo(agentsDir);
72
79
  if (alreadyConfigured && !options.force) {
73
- console.log(chalk.gray('~/.agents/.system/ is already set up.'));
74
- console.log(chalk.gray('\nTo sync updates: agents repo pull system'));
75
- console.log(chalk.gray('To re-run setup: agents setup --force'));
80
+ if (options.suppressFooter)
81
+ return;
82
+ await (options.runHub ?? runSetupHub)();
76
83
  return;
77
84
  }
78
85
  // Detect existing installations BEFORE cloning (they won't exist after if we import)
@@ -193,11 +200,7 @@ export async function runSetup(program, options = {}) {
193
200
  // Fresh-machine hub: offer to set up the optional capabilities that need their
194
201
  // own guided flow. TTY-only and fully opt-in — a non-interactive `agents setup`
195
202
  // stops at the system-repo bootstrap above, unchanged.
196
- await runSetupHub();
197
- // Preferences step: the two questions that keep agents off the wrong machine —
198
- // which box you sit at (interactive host) and which browser agents drive here.
199
- // TTY-only, skippable, and writes the same keys as `agents devices …`.
200
- await runPreferencesStep();
203
+ await (options.runHub ?? runSetupHub)();
201
204
  console.log(chalk.bold('\nSetup complete. Try:'));
202
205
  console.log(chalk.cyan(' agents view ') + chalk.gray(' # see what\'s installed'));
203
206
  console.log(chalk.cyan(' agents run <agent> "hello" ') + chalk.gray(' # run an agent'));
@@ -229,42 +232,88 @@ export async function ensureInitialized(program) {
229
232
  }
230
233
  await runSetup(program, { suppressFooter: true });
231
234
  }
232
- /**
233
- * Interactive "what else do you want to set up?" menu shown after the bare
234
- * `agents setup` finishes on a TTY. Each pick runs that capability's guided
235
- * wizard. Never throws a cancel or an optional wizard's error just skips the
236
- * rest and lets core setup complete.
237
- */
238
- async function runSetupHub() {
239
- if (!isInteractiveTerminal())
235
+ export async function getSetupStatus() {
236
+ const configuredBrowserProfile = getConfiguredDefaultProfileName();
237
+ const browserProfile = await getProfile(configuredBrowserProfile ?? DEFAULT_BROWSER_PROFILE_NAME);
238
+ const browserReady = browserProfile !== null && isProfileLaunchableHere(browserProfile);
239
+ const installedBrowsers = listInstalledBrowsers();
240
+ const computerState = process.platform === 'darwin' ? (await probeComputerTrust() ? 'ready' : 'missing') : 'n/a';
241
+ const devices = await loadDevices();
242
+ const coreReady = isGitRepo(getAgentsDir());
243
+ const secretsReady = fs.existsSync(setupSecretsPrefsPath());
244
+ const shareConfig = readShareConfig();
245
+ const watchdogEnabled = getConfigValue('watchdog.enabled').value === true;
246
+ const interactiveHost = getConfigValue('interactive.host').value;
247
+ const defaultBrowser = getConfigValue('browser.profile').value;
248
+ return [
249
+ { phase: 'core', state: coreReady ? 'ready' : 'missing', detail: coreReady ? 'system repo ready' : 'system repo missing' },
250
+ { phase: 'browser', state: browserReady ? 'ready' : 'missing', detail: browserReady ? `profile ${browserProfile.name}` : browserProfile ? `profile ${browserProfile.name} cannot launch here` : installedBrowsers.length ? 'no default profile' : 'no supported browser found' },
251
+ { phase: 'computer', state: computerState, detail: computerState === 'ready' ? 'helper trusted' : computerState === 'n/a' ? 'macOS local setup only' : 'helper not running or not trusted' },
252
+ { phase: 'secrets', state: secretsReady ? 'ready' : 'missing', detail: secretsReady ? 'defaults chosen' : 'defaults not chosen' },
253
+ { phase: 'fleet', state: Object.keys(devices).length ? 'ready' : 'missing', detail: Object.keys(devices).length ? `${Object.keys(devices).length} device${Object.keys(devices).length === 1 ? '' : 's'} registered` : 'no devices registered' },
254
+ { phase: 'share', state: shareConfig ? 'ready' : 'missing', detail: shareConfig?.baseUrl ?? 'endpoint not configured' },
255
+ { phase: 'watchdog', state: watchdogEnabled ? 'ready' : 'missing', detail: watchdogEnabled ? 'enabled on this device' : 'disabled on this device' },
256
+ { phase: 'preferences', state: interactiveHost || defaultBrowser ? 'ready' : 'missing', detail: [interactiveHost && `host ${interactiveHost}`, defaultBrowser && `browser ${defaultBrowser}`].filter(Boolean).join(' · ') || 'interactive host and browser unset' },
257
+ ];
258
+ }
259
+ export function renderSetupStatus(rows) {
260
+ console.log(chalk.bold('\nagents setup — onboarding\n'));
261
+ for (const row of rows) {
262
+ const marker = row.state === 'ready' ? chalk.green('[x]') : row.state === 'n/a' ? chalk.gray('[-]') : chalk.yellow('[ ]');
263
+ const label = row.phase[0].toUpperCase() + row.phase.slice(1);
264
+ console.log(` ${marker} ${label.padEnd(12)} ${chalk.gray(row.detail)}`);
265
+ }
266
+ }
267
+ async function runSetupPhase(phase) {
268
+ if (phase === 'browser')
269
+ await runBrowserWizard();
270
+ else if (phase === 'computer')
271
+ await runComputerWizard();
272
+ else if (phase === 'share')
273
+ await runShareWizard();
274
+ else if (phase === 'secrets')
275
+ await import('./setup-secrets.js').then((m) => m.runSecretsSetupWizard());
276
+ else if (phase === 'fleet')
277
+ await import('./setup-fleet.js').then((m) => m.runFleetSetupWizard());
278
+ else if (phase === 'watchdog')
279
+ await runWatchdogSetupWizard();
280
+ else
281
+ await runPreferencesStep();
282
+ }
283
+ export async function runSetupHub(deps = {}) {
284
+ const interactive = deps.interactive ?? isInteractiveTerminal();
285
+ if (!interactive) {
286
+ const rows = await getSetupStatus();
287
+ renderSetupStatus(rows);
288
+ if (rows.some((row) => row.state === 'missing'))
289
+ process.exitCode = 1;
240
290
  return;
291
+ }
241
292
  try {
242
- const { checkbox } = await import('@inquirer/prompts');
243
- const picks = await checkbox({
244
- message: 'Set up optional capabilities now? (space to select, enter to confirm)',
245
- choices: [
246
- { name: 'browser — drive a real Chrome/Brave/Edge for web automation', value: 'browser' },
247
- { name: 'computer — control native macOS apps (screenshot, click, type)', value: 'computer' },
248
- { name: 'share — publish shareable links (Cloudflare R2 + Worker)', value: 'share' },
249
- { name: 'secrets — choose storage defaults and import existing credentials', value: 'secrets' },
250
- { name: 'fleet — discover Tailscale devices and configure SSH access', value: 'fleet' },
251
- { name: 'watchdog — resume stalled agent sessions on selected devices', value: 'watchdog' },
252
- ],
253
- });
254
- for (const pick of picks) {
293
+ while (true) {
294
+ const rows = await getSetupStatus();
295
+ renderSetupStatus(rows);
296
+ let pick;
297
+ if (deps.selectPhase) {
298
+ pick = await deps.selectPhase(rows);
299
+ }
300
+ else {
301
+ const { select } = await import('@inquirer/prompts');
302
+ pick = await select({
303
+ message: 'Choose a phase to configure',
304
+ choices: [
305
+ ...rows.filter((row) => row.phase !== 'core' && row.state !== 'n/a').map((row) => ({
306
+ name: `${row.state === 'ready' ? '[x]' : '[ ]'} ${row.phase.padEnd(12)} ${row.detail}`,
307
+ value: row.phase,
308
+ })),
309
+ { name: 'Exit setup', value: 'exit' },
310
+ ],
311
+ });
312
+ }
313
+ if (pick === 'exit')
314
+ return;
255
315
  console.log();
256
- if (pick === 'browser')
257
- await runBrowserWizard();
258
- else if (pick === 'computer')
259
- await runComputerWizard();
260
- else if (pick === 'share')
261
- await runShareWizard();
262
- else if (pick === 'secrets')
263
- await import('./setup-secrets.js').then((m) => m.runSecretsSetupWizard());
264
- else if (pick === 'fleet')
265
- await import('./setup-fleet.js').then((m) => m.runFleetSetupWizard());
266
- else if (pick === 'watchdog')
267
- await runWatchdogSetupWizard();
316
+ await (deps.runPhase ?? runSetupPhase)(pick);
268
317
  }
269
318
  }
270
319
  catch (err) {
@@ -277,7 +326,7 @@ async function runSetupHub() {
277
326
  export function registerSetupCommand(program) {
278
327
  const setupCmd = program
279
328
  .command('setup')
280
- .description('First-time setup. Clones a config repo and installs agent CLIs.')
329
+ .description('Set up agents-cli, or re-open the capability onboarding hub.')
281
330
  .option('-f, --force', 'Re-run setup even if ~/.agents/.system/ already exists (use with caution)')
282
331
  .option('--no-system-repo', 'Skip cloning the system repo (you must populate ~/.agents/.system/ yourself)');
283
332
  // Capability subcommands: `agents setup browser|computer|share|mine|secrets|fleet`.
@@ -288,6 +337,18 @@ export function registerSetupCommand(program) {
288
337
  registerSetupSecretsCommand(setupCmd);
289
338
  registerSetupFleetCommand(setupCmd);
290
339
  registerSetupWatchdogCommand(setupCmd);
340
+ setupCmd.command('status')
341
+ .description('Show setup readiness for core, browser, computer, secrets, fleet, share, watchdog, and preferences.')
342
+ .option('--json', 'print machine-readable JSON')
343
+ .action(async (options) => {
344
+ const rows = await getSetupStatus();
345
+ if (options.json)
346
+ console.log(JSON.stringify(rows, null, 2));
347
+ else
348
+ renderSetupStatus(rows);
349
+ if (rows.some((row) => row.state === 'missing'))
350
+ process.exitCode = 1;
351
+ });
291
352
  setHelpSections(setupCmd, {
292
353
  examples: `
293
354
  # First-time setup (clones the system repo into ~/.agents/.system/)
@@ -308,10 +369,8 @@ export function registerSetupCommand(program) {
308
369
  What it does:
309
370
  1. Clones the system repo into ~/.agents/.system/
310
371
  2. Imports any unmanaged agent installations it finds
311
- 3. On a TTY, offers to set up optional capabilities (browser/computer/share/secrets/fleet/watchdog)
312
- 4. On a TTY, asks preferences: which machine you sit at (interactive host)
313
- and which browser agents drive here — both skippable, both the same
314
- keys 'agents devices set-interactive' / 'browser profiles set-default' write
372
+ 3. Opens a re-runnable capability menu with live ready/missing status
373
+ 4. Delegates each selection to its existing setup wizard
315
374
 
316
375
  Capability setup can also be run any time on its own:
317
376
  agents setup browser # detect a browser + create the default profile
@@ -31,7 +31,7 @@ import { resolveDeviceTarget, splitUserHost } from '../lib/devices/resolve-targe
31
31
  import { clearPendingSentinel } from '../lib/devices/pending.js';
32
32
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
33
33
  import { hostNameFor, renderSshConfig } from '../lib/devices/ssh-config.js';
34
- import { ASKPASS_BUNDLE_ENV, ASKPASS_KEY_ENV, buildSshInvocation, fleetDialTarget, writeAskpassShim, } from '../lib/devices/connect.js';
34
+ import { ASKPASS_BUNDLE_ENV, ASKPASS_KEY_ENV, buildSshInvocation, deviceIdentityArgs, fleetDialTarget, writeAskpassShim, } from '../lib/devices/connect.js';
35
35
  import { ensureManagedKnownHostsDir, isHostPinned } from '../lib/devices/known-hosts.js';
36
36
  import { shouldSyncTerminfo, syncTerminfoToDevice, terminfoHostKey } from '../lib/devices/terminfo.js';
37
37
  import { fanOutDevices, fleetHealthSkip, planFleetTargets, remoteFleetTargets, runFleet, skipLabel, upgradeCommand, } from '../lib/devices/fleet.js';
@@ -44,6 +44,7 @@ import { checkAllClis } from '../lib/teams/agents.js';
44
44
  import { buildRemoteAgentsInvocation } from '../lib/hosts/remote-cmd.js';
45
45
  import { sshExecAsync } from '../lib/ssh-exec.js';
46
46
  import { ALL_AGENT_IDS } from '../lib/agents.js';
47
+ import { collectLocalHarnessInventory, groupByAccount, renderAccountsMatrix, renderHarnessMatrix, } from '../lib/devices/harness-inventory.js';
47
48
  import { crabboxList, crabboxFind, crabboxSshArgv } from '../lib/crabbox/cli.js';
48
49
  import { boxAddress, boxStatus, fmtIdleShort, fmtExpiresShort } from './lease.js';
49
50
  import { authCellColor, formatCheckedAge, isDeadVerdict, probeLocalFleetAuth, readAuthHealthCache, summarizeHostAuth, summarizeVerdicts, verdictColor, verdictLabel, writeFleetAuthRows, } from '../lib/auth-health.js';
@@ -351,7 +352,7 @@ async function probeRemoteFleetStatus(target) {
351
352
  const isWin = /^win/i.test((target.platform ?? '').trim());
352
353
  const env = isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' };
353
354
  const cmd = buildRemoteAgentsInvocation(['devices', 'status', '--local', '--json'], undefined, isWin ? 'windows' : undefined, env);
354
- const res = await sshExecAsync(target.dialTarget, cmd, { timeoutMs: 15000, multiplex: true });
355
+ const res = await sshExecAsync(target.dialTarget, cmd, { timeoutMs: 15000, multiplex: true, extraSshArgs: target.extraSshArgs });
355
356
  if (res.code !== 0) {
356
357
  throw new Error(res.timedOut ? 'timed out' : (res.stderr.trim() || `exit ${res.code ?? 'unknown'}`));
357
358
  }
@@ -361,10 +362,10 @@ async function probeRemoteHealth(target) {
361
362
  const isWin = /^win/i.test((target.platform ?? '').trim());
362
363
  const env = isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' };
363
364
  const versionCmd = buildRemoteAgentsInvocation(['--version'], undefined, isWin ? 'windows' : undefined, env);
364
- const versionRes = await sshExecAsync(target.dialTarget, versionCmd, { timeoutMs: 15000, multiplex: true });
365
+ const versionRes = await sshExecAsync(target.dialTarget, versionCmd, { timeoutMs: 15000, multiplex: true, extraSshArgs: target.extraSshArgs });
365
366
  const version = versionRes.code === 0 ? versionRes.stdout.trim().split(/\s+/)[0] || null : null;
366
367
  const doctorCmd = buildRemoteAgentsInvocation(['doctor', '--json'], undefined, isWin ? 'windows' : undefined, env);
367
- const doctorRes = await sshExecAsync(target.dialTarget, doctorCmd, { timeoutMs: 30000, multiplex: true });
368
+ const doctorRes = await sshExecAsync(target.dialTarget, doctorCmd, { timeoutMs: 30000, multiplex: true, extraSshArgs: target.extraSshArgs });
368
369
  if (doctorRes.code !== 0) {
369
370
  throw new Error(doctorRes.timedOut ? 'timed out' : (doctorRes.stderr.trim() || `exit ${doctorRes.code ?? 'unknown'}`));
370
371
  }
@@ -425,6 +426,7 @@ async function runFleetStatus(opts) {
425
426
  // this is trusted on the default path, not just under `--refresh`.
426
427
  skip: fleetHealthSkip(t.skip, statsMap.get(t.device.name)),
427
428
  dialTarget: fleetDialTarget(t.device),
429
+ extraSshArgs: deviceIdentityArgs(t.device),
428
430
  }));
429
431
  const remote = await fanOutDevices(remoteTargets, probeRemoteHealth);
430
432
  for (const result of remote) {
@@ -533,7 +535,7 @@ async function probeRemoteAuth(target) {
533
535
  const isWin = /^win/i.test((target.platform ?? '').trim());
534
536
  const env = isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' };
535
537
  const cmd = buildRemoteAgentsInvocation(['devices', 'ping', '--local', '--json'], undefined, isWin ? 'windows' : undefined, env);
536
- const res = await sshExecAsync(target.dialTarget, cmd, { timeoutMs: 15000, multiplex: true });
538
+ const res = await sshExecAsync(target.dialTarget, cmd, { timeoutMs: 15000, multiplex: true, extraSshArgs: target.extraSshArgs });
537
539
  if (res.code !== 0) {
538
540
  throw new Error(res.timedOut ? 'timed out' : (res.stderr.trim() || `exit ${res.code ?? 'unknown'}`));
539
541
  }
@@ -601,6 +603,7 @@ async function runFleetPing(opts) {
601
603
  platform: t.device.platform,
602
604
  skip: t.skip,
603
605
  dialTarget: fleetDialTarget(t.device),
606
+ extraSshArgs: deviceIdentityArgs(t.device),
604
607
  }));
605
608
  const probeable = remoteTargets.filter((t) => !t.skip).length;
606
609
  const spinner = isInteractiveTerminal() && !opts.json
@@ -645,6 +648,118 @@ async function runFleetPing(opts) {
645
648
  if (opts.strict && anyBad)
646
649
  process.exitCode = 1;
647
650
  }
651
+ /** SSH into a host and read its raw harness rows (the `--local --json` worker). */
652
+ async function probeRemoteHarnesses(target, refresh) {
653
+ const isWin = /^win/i.test((target.platform ?? '').trim());
654
+ const env = isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' };
655
+ const args = ['devices', 'harnesses', '--local', '--json'];
656
+ if (refresh)
657
+ args.push('--refresh');
658
+ const cmd = buildRemoteAgentsInvocation(args, undefined, isWin ? 'windows' : undefined, env);
659
+ const res = await sshExecAsync(target.dialTarget, cmd, { timeoutMs: 15000, multiplex: true, extraSshArgs: target.extraSshArgs });
660
+ if (res.code !== 0) {
661
+ throw new Error(res.timedOut ? 'timed out' : (res.stderr.trim() || `exit ${res.code ?? 'unknown'}`));
662
+ }
663
+ const parsed = JSON.parse(res.stdout);
664
+ return parsed.rows ?? [];
665
+ }
666
+ /**
667
+ * Gather harness rows across the fleet: THIS host in process, every reachable
668
+ * peer over SSH. Shared by both `harnesses` and `accounts` (they differ only in
669
+ * how the rows are rendered). Honors an optional `--device` allowlist on both
670
+ * the local and remote rows. Bounded by the same per-device + overall deadlines
671
+ * as `fleet ping`, so one unreachable box can never stall the glance.
672
+ */
673
+ async function collectFleetHarnesses(opts) {
674
+ const self = machineId();
675
+ const want = opts.devices?.length ? new Set(opts.devices) : null;
676
+ const results = [];
677
+ if (!want || want.has(self)) {
678
+ const localRows = await collectLocalHarnessInventory({ agents: opts.agents, refresh: opts.refresh });
679
+ results.push({ host: self, rows: localRows });
680
+ }
681
+ const reg = await loadDevices();
682
+ const planned = planFleetTargets(reg);
683
+ let remoteTargets = remoteFleetTargets(planned, self).map((t) => ({
684
+ name: t.device.name,
685
+ platform: t.device.platform,
686
+ skip: t.skip,
687
+ dialTarget: fleetDialTarget(t.device),
688
+ extraSshArgs: deviceIdentityArgs(t.device),
689
+ }));
690
+ if (want)
691
+ remoteTargets = remoteTargets.filter((t) => want.has(t.name));
692
+ if (remoteTargets.length > 0) {
693
+ const probeable = remoteTargets.filter((t) => !t.skip).length;
694
+ const spinner = isInteractiveTerminal() && !opts.json
695
+ ? ora(`Probing ${probeable} device${probeable === 1 ? '' : 's'}…`).start()
696
+ : undefined;
697
+ let remote;
698
+ try {
699
+ const fanOut = fanOutDevices(remoteTargets, (t) => probeRemoteHarnesses(t, !!opts.refresh), { perDeviceTimeoutMs: 15_000 });
700
+ remote = await raceFleetPingDeadline(fanOut, remoteTargets, 30_000);
701
+ }
702
+ finally {
703
+ spinner?.stop();
704
+ }
705
+ for (const r of remote) {
706
+ if (r.status === 'ok' && r.value) {
707
+ results.push({ host: r.name, rows: r.value });
708
+ }
709
+ else {
710
+ results.push({
711
+ host: r.name,
712
+ rows: [],
713
+ error: r.error,
714
+ skipped: r.reason ? String(r.reason) : undefined,
715
+ });
716
+ }
717
+ }
718
+ }
719
+ return results;
720
+ }
721
+ async function runDevicesHarnesses(opts) {
722
+ if (opts.local) {
723
+ const rows = await collectLocalHarnessInventory({ agents: opts.agents, refresh: opts.refresh });
724
+ if (opts.json)
725
+ console.log(JSON.stringify({ host: machineId(), rows }));
726
+ else
727
+ for (const line of renderHarnessMatrix([{ host: machineId(), rows }]))
728
+ console.log(line);
729
+ return;
730
+ }
731
+ const results = await collectFleetHarnesses(opts);
732
+ if (opts.json)
733
+ console.log(JSON.stringify(results, null, 2));
734
+ else
735
+ for (const line of renderHarnessMatrix(results))
736
+ console.log(line);
737
+ }
738
+ async function runDevicesAccounts(opts) {
739
+ if (opts.local) {
740
+ const rows = await collectLocalHarnessInventory({ agents: opts.agents, refresh: opts.refresh });
741
+ if (opts.json)
742
+ console.log(JSON.stringify({ host: machineId(), accounts: groupByAccount(rows) }));
743
+ else
744
+ for (const line of renderAccountsMatrix([{ host: machineId(), rows }]))
745
+ console.log(line);
746
+ return;
747
+ }
748
+ const results = await collectFleetHarnesses(opts);
749
+ if (opts.json) {
750
+ const grouped = results.map((r) => ({
751
+ host: r.host,
752
+ error: r.error,
753
+ skipped: r.skipped,
754
+ accounts: groupByAccount(r.rows),
755
+ }));
756
+ console.log(JSON.stringify(grouped, null, 2));
757
+ }
758
+ else {
759
+ for (const line of renderAccountsMatrix(results))
760
+ console.log(line);
761
+ }
762
+ }
648
763
  /** Resolve an {@link AuthCellColor} to a chalk painter. Single map for cells + labels. */
649
764
  const CELL_PAINT = {
650
765
  green: chalk.green,
@@ -777,6 +892,7 @@ Typical workflow:
777
892
  agents devices configure mac-mini --max-agents 4 --scheduler off
778
893
  agents devices note mac-mini "runs the releases — don't reboot"
779
894
  agents devices set win-mini --auth password --bundle muqsit
895
+ agents devices set worker --auth key --identity-file ~/.ssh/worker_ed25519
780
896
  agents devices render --write # write ~/.ssh/config.d/agents include
781
897
  agents fleet update # roll out latest agents-cli to every online device
782
898
  agents fleet run uname -a # run a command on every online device
@@ -1219,6 +1335,59 @@ Typical workflow:
1219
1335
  const verbose = opts.verbose ?? Boolean(cmd.optsWithGlobals().verbose);
1220
1336
  await runFleetPing({ ...opts, verbose });
1221
1337
  });
1338
+ const csvList = (s) => s ? s.split(',').map((x) => x.trim()).filter(Boolean) : undefined;
1339
+ const harnessInvOpts = (opts) => ({
1340
+ agents: csvList(opts.agents),
1341
+ devices: csvList(opts.device ?? opts.devices),
1342
+ refresh: opts.refresh || opts.live,
1343
+ json: opts.json,
1344
+ local: opts.local,
1345
+ });
1346
+ const harnessesCmd = devicesCmd
1347
+ .command('harnesses')
1348
+ .description('Per device, one row per installed agent@version: account, signed-in, quota, and a single ready verdict. SSH-probes each online box.')
1349
+ .option('--json', 'output machine-readable JSON (per-host rows)')
1350
+ .option('--agents <csv>', 'only these agents (comma-separated)')
1351
+ .option('--device <csv>', 'only these devices (comma-separated); default: every online box')
1352
+ .option('--refresh', 'fetch live quota instead of the cached snapshot (slower)')
1353
+ .option('--live', 'alias of --refresh')
1354
+ .option('--local', "this host only: emit THIS box's rows (the per-host worker the fan-out reads over ssh)")
1355
+ .action(async (opts) => {
1356
+ await runDevicesHarnesses(harnessInvOpts(opts));
1357
+ });
1358
+ setHelpSections(harnessesCmd, {
1359
+ examples: `
1360
+ agents devices harnesses # every box: agent@version · account · signed · quota · ready
1361
+ agents devices harnesses --agents claude,codex # just these harnesses
1362
+ agents devices harnesses --device zion # one box
1363
+ agents devices harnesses --refresh # live quota (bypass the cached snapshot)
1364
+ agents devices harnesses --json # machine-readable, per-host rows`,
1365
+ notes: `
1366
+ "ready" = signed in AND not rate-limited — usable for a run right now.
1367
+ Quota is the cached usage snapshot (the daemon warms it); --refresh fetches live.
1368
+ Use \`agents devices accounts\` for the same data grouped by account.`,
1369
+ });
1370
+ const accountsCmd = devicesCmd
1371
+ .command('accounts')
1372
+ .description('Per device, one row per account: which harnesses share it, signed-in, quota, and ready. The identity lens on `agents devices harnesses`.')
1373
+ .option('--json', 'output machine-readable JSON (per-host account groups)')
1374
+ .option('--agents <csv>', 'only these agents (comma-separated)')
1375
+ .option('--device <csv>', 'only these devices (comma-separated); default: every online box')
1376
+ .option('--refresh', 'fetch live quota instead of the cached snapshot (slower)')
1377
+ .option('--live', 'alias of --refresh')
1378
+ .option('--local', "this host only: emit THIS box's account groups")
1379
+ .action(async (opts) => {
1380
+ await runDevicesAccounts(harnessInvOpts(opts));
1381
+ });
1382
+ setHelpSections(accountsCmd, {
1383
+ examples: `
1384
+ agents devices accounts # every box: account · agents · signed · quota · ready
1385
+ agents devices accounts --device mac-mini
1386
+ agents devices accounts --json # machine-readable, per-host account groups`,
1387
+ notes: `
1388
+ Collapses the installs that share one account (e.g. five claude versions on one
1389
+ email) into a single row. Use \`agents devices harnesses\` for the per-install view.`,
1390
+ });
1222
1391
  devicesCmd
1223
1392
  .command('login')
1224
1393
  .description('Log agent CLIs into fleet boxes over SSH: drive each box\'s device-code OAuth, scrape the URL + code, and surface every pending login in one local browser page. Default drives all codes at once; --interactive walks one box at a time (codes requested just-in-time so they don\'t expire).')
@@ -1281,15 +1450,19 @@ Typical workflow:
1281
1450
  .option('--auth <method>', 'key | password')
1282
1451
  .option('--bundle <bundle>', 'secrets bundle holding the password (for --auth password)')
1283
1452
  .option('--bundle-key <key>', "key within the bundle (default 'password')")
1453
+ .option('--identity-file <path>', 'private-key path for --auth key')
1454
+ .option('--clear-identity-file', 'return key auth to ssh-agent/default-key discovery')
1284
1455
  .action(async (name, opts) => {
1285
1456
  try {
1286
1457
  const existing = await mustGetDevice(name);
1287
- const auth = opts.auth || opts.bundle || opts.bundleKey
1288
- ? {
1289
- method: opts.auth ?? existing.auth.method,
1290
- bundle: opts.bundle ?? existing.auth.bundle,
1291
- bundleKey: opts.bundleKey ?? existing.auth.bundleKey,
1292
- }
1458
+ const nextMethod = opts.auth ?? existing.auth.method;
1459
+ if (opts.identityFile && nextMethod !== 'key') {
1460
+ throw new Error('--identity-file requires key auth; pass --auth key in the same command.');
1461
+ }
1462
+ const auth = opts.auth || opts.bundle || opts.bundleKey || opts.identityFile || opts.clearIdentityFile
1463
+ ? nextMethod === 'key'
1464
+ ? { method: nextMethod, identityFile: opts.clearIdentityFile ? undefined : (opts.identityFile ?? existing.auth.identityFile) }
1465
+ : { method: nextMethod, bundle: opts.bundle ?? existing.auth.bundle, bundleKey: opts.bundleKey ?? existing.auth.bundleKey }
1293
1466
  : undefined;
1294
1467
  const d = await upsertDevice(name, {
1295
1468
  platform: opts.platform ?? undefined,
@@ -34,6 +34,12 @@ export interface TeamListAgentSnapshot {
34
34
  cloud_provider: string | null;
35
35
  pr_url: string | null;
36
36
  }
37
+ /**
38
+ * One-line operator nudge after `teams start`: teammates are briefed to post
39
+ * IMPORTANT milestones to the feed (RUSH-2250), and this is how to watch them.
40
+ * Print-only — no engine behavior.
41
+ */
42
+ export declare function printFeedHint(team: string): void;
37
43
  /** Where `teams message`/`teams resume` routes a follow-up, by teammate status. */
38
44
  export type TeamMessageRoute = {
39
45
  kind: 'steer';
@@ -17,7 +17,7 @@ import { createTeam, ensureTeam, getTeam, loadTeams, removeTeam, teamExists, } f
17
17
  import { setHelpSections } from '../lib/help.js';
18
18
  import { createWorktree, isGitRepo, hasUncommittedChanges, removeWorktree, } from '../lib/teams/worktree.js';
19
19
  import { resolveHost } from '../lib/hosts/registry.js';
20
- import { isDeviceAuto, resolveDeviceAffinity } from '../lib/smart-launch.js';
20
+ import { isDeviceAuto, resolveDeviceAuto } from '../lib/smart-launch.js';
21
21
  import { sshTargetFor } from '../lib/hosts/types.js';
22
22
  import { ensureHostReady } from '../lib/hosts/ready.js';
23
23
  import { remoteShellFor } from '../lib/hosts/remote-cmd.js';
@@ -153,6 +153,18 @@ function fullName(type, version) {
153
153
  const name = AGENT_NAMES[type];
154
154
  return version ? `${name} ${version}` : name;
155
155
  }
156
+ /**
157
+ * One-line operator nudge after `teams start`: teammates are briefed to post
158
+ * IMPORTANT milestones to the feed (RUSH-2250), and this is how to watch them.
159
+ * Print-only — no engine behavior.
160
+ */
161
+ export function printFeedHint(team) {
162
+ console.log(chalk.gray('Tip: teammates post IMPORTANT milestones to the feed (') +
163
+ chalk.cyan('agents feed timeline') +
164
+ chalk.gray('); watch team progress with ') +
165
+ chalk.cyan(`agents teams status ${team}`) +
166
+ chalk.gray('.'));
167
+ }
156
168
  /**
157
169
  * Resolve a teammate spec to its execution target.
158
170
  *
@@ -365,6 +377,8 @@ async function runOneWave(mgr, team, json) {
365
377
  console.log(` ${chalk.blue(h)} ${chalk.gray('after')} ${a.after.join(', ')}`);
366
378
  }
367
379
  }
380
+ if (launched.length > 0)
381
+ printFeedHint(team);
368
382
  }
369
383
  /**
370
384
  * Path to the pr-watch dedupe-state file for a team. Persists the set of
@@ -1340,12 +1354,12 @@ export function registerTeamsCommands(program) {
1340
1354
  }
1341
1355
  return h ?? d ?? null;
1342
1356
  })();
1343
- // `auto` is the same affinity sentinel `agents run --device auto` resolves
1357
+ // `auto` is the same live fleet sentinel `agents run --device auto` resolves
1344
1358
  // (RUSH-2185) — pick the concrete device name up front so the local-machine
1345
1359
  // check right below (and every dieFriction message further down) sees the
1346
1360
  // real target instead of the literal string "auto".
1347
1361
  if (explicitDevice && isDeviceAuto(explicitDevice)) {
1348
- const plan = resolveDeviceAffinity({});
1362
+ const plan = await resolveDeviceAuto(parseTeammate(teammate).agent);
1349
1363
  const picked = plan.host ?? machineId();
1350
1364
  process.stderr.write(chalk.gray(`[teams] device=auto → ${picked === machineId() ? 'local' : picked}\n`));
1351
1365
  explicitDevice = picked;
@@ -1766,11 +1780,40 @@ export function registerTeamsCommands(program) {
1766
1780
  await warnUnsignedTeammates(mgr, team);
1767
1781
  await warnThrottledTeammates(mgr, team);
1768
1782
  }
1783
+ // Health-/harness-aware pre-flight (RUSH-2002): if the team declares a
1784
+ // device pool and no device in it can run a pending teammate's agent, fail
1785
+ // loud here rather than launching a wave that strands teammates pending.
1786
+ // --force downgrades it to a warning so the user can still try.
1787
+ const placementError = await mgr.preflightPlacement(team);
1788
+ if (placementError) {
1789
+ if (isJsonMode(opts)) {
1790
+ console.log(JSON.stringify({
1791
+ team,
1792
+ error: 'no-viable-device',
1793
+ message: placementError.message,
1794
+ excluded: placementError.excluded,
1795
+ }));
1796
+ if (!opts.force) {
1797
+ process.exitCode = 1;
1798
+ return;
1799
+ }
1800
+ }
1801
+ else if (opts.force) {
1802
+ console.error(chalk.yellow(`⚠ ${placementError.message}\n Continuing anyway (--force).`));
1803
+ }
1804
+ else {
1805
+ console.error(chalk.red(placementError.message));
1806
+ process.exitCode = 1;
1807
+ return;
1808
+ }
1809
+ }
1769
1810
  emit('teams.start', { module: 'teams', team, watch: Boolean(opts.watch) });
1770
1811
  if (!opts.watch) {
1771
1812
  await runOneWave(mgr, team, Boolean(opts.json));
1772
1813
  return;
1773
1814
  }
1815
+ if (!isJsonMode(opts))
1816
+ printFeedHint(team);
1774
1817
  const intervalMs = Math.max(1000, Number.parseInt(opts.interval, 10) * 1000 || 8000);
1775
1818
  const maxWaves = Math.max(1, Number.parseInt(opts.maxWaves, 10) || 1000);
1776
1819
  const json = isJsonMode(opts);
@@ -1,2 +1,10 @@
1
+ /**
2
+ * Deprecated top-level spelling of the counter-mix tree.
3
+ *
4
+ * Implementation lives under `agents insights mix` (see
5
+ * `lib/analytics/mix-commands.ts`). This file only registers the thin alias so
6
+ * old scripts and agent briefs keep working while printing one deprecation line
7
+ * — no second recipe implementation.
8
+ */
1
9
  import type { Command } from 'commander';
2
10
  export declare function registerTrendsCommand(program: Command): void;