@phnx-labs/agents-cli 1.22.114 → 1.22.116

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 (243) hide show
  1. package/CHANGELOG.md +245 -0
  2. package/README.md +22 -102
  3. package/dist/browser.js +0 -0
  4. package/dist/cli/command-registry.js +0 -7
  5. package/dist/commands/accounts.d.ts +3 -77
  6. package/dist/commands/accounts.js +13 -366
  7. package/dist/commands/audit.js +1 -1
  8. package/dist/commands/auth.js +0 -2
  9. package/dist/commands/browser.d.ts +53 -0
  10. package/dist/commands/browser.js +210 -9
  11. package/dist/commands/doctor.d.ts +2 -3
  12. package/dist/commands/doctor.js +0 -11
  13. package/dist/commands/events.d.ts +1 -3
  14. package/dist/commands/events.js +4 -7
  15. package/dist/commands/exec.d.ts +0 -1
  16. package/dist/commands/exec.js +214 -148
  17. package/dist/commands/feed-watch.js +52 -8
  18. package/dist/commands/feed.js +65 -14
  19. package/dist/commands/focus.js +1 -1
  20. package/dist/commands/go.d.ts +0 -17
  21. package/dist/commands/go.js +2 -19
  22. package/dist/commands/logs.js +1 -1
  23. package/dist/commands/mcp.js +8 -83
  24. package/dist/commands/memory.js +4 -47
  25. package/dist/commands/message.js +4 -4
  26. package/dist/commands/plugins.js +1 -93
  27. package/dist/commands/repo.js +0 -44
  28. package/dist/commands/resume.js +4 -1
  29. package/dist/commands/secrets-passthrough.js +2 -2
  30. package/dist/commands/send.d.ts +4 -4
  31. package/dist/commands/send.js +6 -51
  32. package/dist/commands/sessions-backup-setup.js +1 -1
  33. package/dist/commands/sessions-picker.d.ts +11 -0
  34. package/dist/commands/sessions-picker.js +88 -7
  35. package/dist/commands/sessions-resume.js +0 -1
  36. package/dist/commands/sessions-share.d.ts +5 -7
  37. package/dist/commands/sessions-share.js +98 -49
  38. package/dist/commands/sessions.d.ts +21 -2
  39. package/dist/commands/sessions.js +158 -11
  40. package/dist/commands/setup-browser.js +18 -2
  41. package/dist/commands/setup-computer.js +20 -6
  42. package/dist/commands/setup-secrets.d.ts +2 -2
  43. package/dist/commands/setup-secrets.js +24 -4
  44. package/dist/commands/setup-term.d.ts +24 -0
  45. package/dist/commands/setup-term.js +70 -0
  46. package/dist/commands/setup-terminal.d.ts +3 -0
  47. package/dist/commands/setup-terminal.js +22 -0
  48. package/dist/commands/setup.d.ts +1 -1
  49. package/dist/commands/setup.js +29 -11
  50. package/dist/commands/skills.js +0 -8
  51. package/dist/commands/ssh.d.ts +6 -0
  52. package/dist/commands/ssh.js +93 -302
  53. package/dist/commands/sync.js +14 -5
  54. package/dist/commands/teams.js +1 -1
  55. package/dist/commands/traces.js +1 -1
  56. package/dist/lib/accounting/rotate.d.ts +63 -1
  57. package/dist/lib/accounting/rotate.js +56 -0
  58. package/dist/lib/accounts/add.d.ts +0 -5
  59. package/dist/lib/accounts/add.js +3 -9
  60. package/dist/lib/accounts/slots.js +32 -2
  61. package/dist/lib/answer-router.d.ts +11 -2
  62. package/dist/lib/answer-router.js +26 -2
  63. package/dist/lib/artifacts-client.d.ts +20 -0
  64. package/dist/lib/artifacts-client.js +46 -0
  65. package/dist/lib/auth-mint.d.ts +5 -4
  66. package/dist/lib/auth-mint.js +6 -5
  67. package/dist/lib/browser/drivers/arc.d.ts +1 -1
  68. package/dist/lib/browser/runtime-state.d.ts +55 -0
  69. package/dist/lib/browser/runtime-state.js +99 -18
  70. package/dist/lib/browser/service.d.ts +10 -0
  71. package/dist/lib/browser/service.js +229 -37
  72. package/dist/lib/browser/types.d.ts +18 -0
  73. package/dist/lib/cli-resources.js +3 -1
  74. package/dist/lib/cloud/dispatch.js +1 -1
  75. package/dist/lib/cloudflare/creds.d.ts +10 -0
  76. package/dist/lib/cloudflare/creds.js +46 -0
  77. package/dist/lib/cloudflare/provision.d.ts +35 -0
  78. package/dist/lib/cloudflare/provision.js +144 -0
  79. package/dist/lib/computer/sessions-list.d.ts +55 -0
  80. package/dist/lib/computer/sessions-list.js +168 -1
  81. package/dist/lib/config-keys.d.ts +1 -1
  82. package/dist/lib/config-keys.js +5 -0
  83. package/dist/lib/daemon/daemon.js +8 -1
  84. package/dist/lib/daemon/feed-stream-service.d.ts +23 -0
  85. package/dist/lib/daemon/feed-stream-service.js +40 -0
  86. package/dist/lib/daemon-services.d.ts +1 -1
  87. package/dist/lib/daemon-services.js +5 -0
  88. package/dist/lib/device-config.js +61 -0
  89. package/dist/lib/devices/connect.d.ts +49 -5
  90. package/dist/lib/devices/connect.js +169 -21
  91. package/dist/lib/devices/doctor-findings.js +2 -6
  92. package/dist/lib/feed/answer.d.ts +153 -4
  93. package/dist/lib/feed/answer.js +716 -105
  94. package/dist/lib/feed/envelope.d.ts +79 -0
  95. package/dist/lib/feed/envelope.js +23 -0
  96. package/dist/lib/feed/events.d.ts +6 -0
  97. package/dist/lib/feed/events.js +8 -0
  98. package/dist/lib/feed/feed.d.ts +61 -1
  99. package/dist/lib/feed/feed.js +226 -14
  100. package/dist/lib/feed/hub-server.d.ts +141 -0
  101. package/dist/lib/feed/hub-server.js +586 -0
  102. package/dist/lib/feed/hub.d.ts +95 -0
  103. package/dist/lib/feed/hub.js +255 -0
  104. package/dist/lib/feed/pr-status.d.ts +8 -0
  105. package/dist/lib/feed/pr-status.js +9 -1
  106. package/dist/lib/feed/tool-activity.d.ts +108 -0
  107. package/dist/lib/feed/tool-activity.js +313 -0
  108. package/dist/lib/feed/tools.d.ts +198 -0
  109. package/dist/lib/feed/tools.js +265 -0
  110. package/dist/lib/feed/watch.d.ts +50 -50
  111. package/dist/lib/feed/watch.js +147 -16
  112. package/dist/lib/feed-outcome.d.ts +1 -1
  113. package/dist/lib/feed-outcome.js +9 -2
  114. package/dist/lib/feed-policy.js +9 -3
  115. package/dist/lib/fleet/auth-sync.d.ts +2 -55
  116. package/dist/lib/fleet/auth-sync.js +2 -89
  117. package/dist/lib/format.d.ts +1 -1
  118. package/dist/lib/format.js +1 -1
  119. package/dist/lib/git.d.ts +0 -16
  120. package/dist/lib/git.js +0 -58
  121. package/dist/lib/harness-auth-capabilities.js +7 -2
  122. package/dist/lib/helper-versions.js +1 -1
  123. package/dist/lib/hosts/dispatch.d.ts +20 -1
  124. package/dist/lib/hosts/dispatch.js +52 -30
  125. package/dist/lib/hosts/remote-cmd.d.ts +72 -1
  126. package/dist/lib/hosts/remote-cmd.js +151 -10
  127. package/dist/lib/hosts/remote-cmd.test-fixture.d.ts +2 -0
  128. package/dist/lib/hosts/remote-cmd.test-fixture.js +22 -0
  129. package/dist/lib/mailbox.d.ts +12 -0
  130. package/dist/lib/mailbox.js +16 -2
  131. package/dist/lib/mcp.js +17 -11
  132. package/dist/lib/menubar/snapshot.d.ts +51 -0
  133. package/dist/lib/menubar/snapshot.js +42 -3
  134. package/dist/lib/open-url.js +2 -2
  135. package/dist/lib/probe.d.ts +4 -1
  136. package/dist/lib/probe.js +5 -2
  137. package/dist/lib/projects.d.ts +23 -0
  138. package/dist/lib/projects.js +78 -0
  139. package/dist/lib/pwsh.d.ts +33 -0
  140. package/dist/lib/pwsh.js +56 -0
  141. package/dist/lib/redact.d.ts +8 -0
  142. package/dist/lib/redact.js +11 -0
  143. package/dist/lib/refresh.d.ts +6 -2
  144. package/dist/lib/refresh.js +92 -72
  145. package/dist/lib/secrets-cli.d.ts +11 -0
  146. package/dist/lib/secrets-cli.js +30 -0
  147. package/dist/lib/secrets-client.js +3 -2
  148. package/dist/lib/session/active.d.ts +1 -0
  149. package/dist/lib/session/active.js +8 -0
  150. package/dist/lib/session/db.d.ts +67 -3
  151. package/dist/lib/session/db.js +381 -126
  152. package/dist/lib/session/detached.d.ts +7 -0
  153. package/dist/lib/session/detached.js +29 -0
  154. package/dist/lib/session/prompt.d.ts +23 -7
  155. package/dist/lib/session/prompt.js +46 -8
  156. package/dist/lib/session/remote/peer-stream.d.ts +24 -2
  157. package/dist/lib/session/remote/peer-stream.js +33 -6
  158. package/dist/lib/session/remote/remote-list.d.ts +20 -0
  159. package/dist/lib/session/remote/remote-list.js +22 -6
  160. package/dist/lib/session/remote/watch.d.ts +12 -0
  161. package/dist/lib/session/remote/watch.js +9 -0
  162. package/dist/lib/session/remote-preview-cache.d.ts +29 -0
  163. package/dist/lib/session/remote-preview-cache.js +373 -0
  164. package/dist/lib/session/sync/backend.d.ts +3 -3
  165. package/dist/lib/session/sync/backend.js +3 -3
  166. package/dist/lib/session/sync/provision.d.ts +1 -1
  167. package/dist/lib/session/sync/provision.js +2 -2
  168. package/dist/lib/session/tail.d.ts +50 -0
  169. package/dist/lib/session/tail.js +219 -0
  170. package/dist/lib/sessions-client.js +0 -3
  171. package/dist/lib/setup-tool-install.d.ts +3 -0
  172. package/dist/lib/setup-tool-install.js +27 -0
  173. package/dist/lib/setup-tool-status.d.ts +22 -0
  174. package/dist/lib/setup-tool-status.js +220 -0
  175. package/dist/lib/share-runtime.d.ts +11 -0
  176. package/dist/lib/share-runtime.js +63 -0
  177. package/dist/lib/signin-badge.d.ts +19 -4
  178. package/dist/lib/signin-badge.js +29 -11
  179. package/dist/lib/smart-launch.d.ts +1 -5
  180. package/dist/lib/smart-launch.js +3 -11
  181. package/dist/lib/ssh-exec.d.ts +44 -0
  182. package/dist/lib/ssh-exec.js +119 -0
  183. package/dist/lib/startup/command-registry.d.ts +6 -4
  184. package/dist/lib/startup/command-registry.js +10 -7
  185. package/dist/lib/state.js +2 -2
  186. package/dist/lib/storage/selection.d.ts +2 -2
  187. package/dist/lib/storage/selection.js +2 -2
  188. package/dist/lib/sync-umbrella.d.ts +5 -0
  189. package/dist/lib/sync-umbrella.js +18 -10
  190. package/dist/lib/term-driver.d.ts +24 -0
  191. package/dist/lib/term-driver.js +36 -0
  192. package/dist/lib/terminal/index.d.ts +1 -1
  193. package/dist/lib/terminal/index.js +1 -1
  194. package/dist/lib/terminal/inject.d.ts +38 -0
  195. package/dist/lib/terminal/inject.js +55 -9
  196. package/dist/lib/terminal/transport.d.ts +15 -5
  197. package/dist/lib/terminal/transport.js +61 -11
  198. package/dist/lib/traces/backend.d.ts +1 -2
  199. package/dist/lib/traces/backend.js +1 -2
  200. package/dist/lib/traces/provision.d.ts +1 -1
  201. package/dist/lib/traces/provision.js +2 -2
  202. package/dist/lib/types.d.ts +8 -6
  203. package/package.json +2 -3
  204. package/dist/commands/artifacts-setup.d.ts +0 -53
  205. package/dist/commands/artifacts-setup.js +0 -161
  206. package/dist/commands/artifacts.d.ts +0 -18
  207. package/dist/commands/artifacts.js +0 -58
  208. package/dist/commands/attach.d.ts +0 -12
  209. package/dist/commands/attach.js +0 -86
  210. package/dist/commands/auth-mint.d.ts +0 -12
  211. package/dist/commands/auth-mint.js +0 -108
  212. package/dist/commands/reconnect.d.ts +0 -46
  213. package/dist/commands/reconnect.js +0 -115
  214. package/dist/commands/share.d.ts +0 -293
  215. package/dist/commands/share.js +0 -1424
  216. package/dist/lib/fleet/remote-login.d.ts +0 -170
  217. package/dist/lib/fleet/remote-login.js +0 -568
  218. package/dist/lib/share/analytics.d.ts +0 -13
  219. package/dist/lib/share/analytics.js +0 -45
  220. package/dist/lib/share/backend.d.ts +0 -120
  221. package/dist/lib/share/backend.js +0 -176
  222. package/dist/lib/share/capture.d.ts +0 -31
  223. package/dist/lib/share/capture.js +0 -174
  224. package/dist/lib/share/config.d.ts +0 -72
  225. package/dist/lib/share/config.js +0 -211
  226. package/dist/lib/share/delete.d.ts +0 -123
  227. package/dist/lib/share/delete.js +0 -173
  228. package/dist/lib/share/html.d.ts +0 -20
  229. package/dist/lib/share/html.js +0 -88
  230. package/dist/lib/share/http-error.d.ts +0 -53
  231. package/dist/lib/share/http-error.js +0 -65
  232. package/dist/lib/share/og.d.ts +0 -26
  233. package/dist/lib/share/og.js +0 -84
  234. package/dist/lib/share/provision.d.ts +0 -127
  235. package/dist/lib/share/provision.js +0 -285
  236. package/dist/lib/share/publish.d.ts +0 -379
  237. package/dist/lib/share/publish.js +0 -818
  238. package/dist/lib/share/worker-template.d.ts +0 -27
  239. package/dist/lib/share/worker-template.js +0 -2424
  240. package/dist/lib/storage/index.d.ts +0 -14
  241. package/dist/lib/storage/index.js +0 -14
  242. package/dist/lib/storage/visibility.d.ts +0 -82
  243. package/dist/lib/storage/visibility.js +0 -99
@@ -20,9 +20,9 @@ import { ensureShimCurrent, switchHomeFileSymlinks, isShimsInPath, addShimsToPat
20
20
  import { setHelpSections } from '../lib/help.js';
21
21
  import { registerSetupBrowserCommand, runBrowserWizard } from './setup-browser.js';
22
22
  import { registerSetupComputerCommand, runComputerWizard } from './setup-computer.js';
23
- import { runShareWizard } from './artifacts-setup.js';
24
23
  import { registerSetupMineCommand } from './setup-mine.js';
25
24
  import { registerSetupSecretsCommand } from './setup-secrets.js';
25
+ import { registerSetupTermCommand, runTermWizard, isTermCliInstalled } from './setup-term.js';
26
26
  import { registerSetupFleetCommand } from './setup-fleet.js';
27
27
  import { registerSetupAccountsCommand, runAccountsSetupWizard } from './setup-accounts.js';
28
28
  import { registerSetupWatchdogCommand, runWatchdogSetupWizard } from './setup-watchdog.js';
@@ -34,10 +34,10 @@ import { runPreferencesStep } from './setup-preferences.js';
34
34
  import { getConfiguredDefaultProfileName, getProfile, getAutoDetectedProfile, isProfileLaunchableHere } from '../lib/browser/profiles.js';
35
35
  import { listInstalledBrowsers } from '../lib/browser/chrome.js';
36
36
  import { probeComputerTrust } from './computer.js';
37
- import { readShareConfig } from '../lib/share/config.js';
38
37
  import { loadDevices } from '../lib/devices/registry.js';
39
38
  import { getConfigValue } from '../lib/device-config.js';
40
39
  import { setupSecretsPrefsPath } from './setup-secrets.js';
40
+ import { SETUP_TOOLS, getCachedToolSetup, refreshToolSetup } from '../lib/setup-tool-status.js';
41
41
  const HOME = os.homedir();
42
42
  /**
43
43
  * Import an existing unmanaged agent installation into agents-cli.
@@ -279,8 +279,8 @@ export async function getSetupStatus() {
279
279
  const devices = await loadDevices();
280
280
  const coreReady = isGitRepo(getAgentsDir());
281
281
  const secretsReady = fs.existsSync(setupSecretsPrefsPath());
282
+ const termReady = isTermCliInstalled();
282
283
  const minted = hasMintedSetupToken();
283
- const shareConfig = readShareConfig();
284
284
  const watchdogEnabled = getConfigValue('watchdog.enabled').value === true;
285
285
  const interactiveHost = getConfigValue('interactive.host').value;
286
286
  const defaultBrowser = getConfigValue('browser.profile').value;
@@ -289,9 +289,9 @@ export async function getSetupStatus() {
289
289
  { 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' },
290
290
  { phase: 'computer', state: computerState, detail: computerState === 'ready' ? 'helper trusted' : computerState === 'n/a' ? 'macOS local setup only' : 'helper not running or not trusted' },
291
291
  { phase: 'secrets', state: secretsReady ? 'ready' : 'missing', detail: secretsReady ? 'defaults chosen' : 'defaults not chosen' },
292
+ { phase: 'term', state: termReady ? 'ready' : 'missing', detail: termReady ? 'installed' : 'not installed (fleet login / auth mint spawn it)' },
292
293
  { phase: 'accounts', state: minted.ready ? 'ready' : 'missing', detail: minted.detail },
293
294
  { 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' },
294
- { phase: 'share', state: shareConfig ? 'ready' : 'missing', detail: shareConfig?.baseUrl ?? 'endpoint not configured' },
295
295
  { phase: 'watchdog', state: watchdogEnabled ? 'ready' : 'missing', detail: watchdogEnabled ? 'enabled on this device' : 'disabled on this device' },
296
296
  { phase: 'preferences', state: interactiveHost || defaultBrowser ? 'ready' : 'missing', detail: [interactiveHost && `host ${interactiveHost}`, defaultBrowser && `browser ${defaultBrowser}`].filter(Boolean).join(' · ') || 'interactive host and browser unset' },
297
297
  ];
@@ -309,10 +309,10 @@ async function runSetupPhase(phase) {
309
309
  await runBrowserWizard();
310
310
  else if (phase === 'computer')
311
311
  await runComputerWizard();
312
- else if (phase === 'share')
313
- await runShareWizard();
314
312
  else if (phase === 'secrets')
315
313
  await import('./setup-secrets.js').then((m) => m.runSecretsSetupWizard());
314
+ else if (phase === 'term')
315
+ await runTermWizard();
316
316
  else if (phase === 'accounts')
317
317
  await runAccountsSetupWizard();
318
318
  else if (phase === 'fleet')
@@ -371,11 +371,12 @@ export function registerSetupCommand(program) {
371
371
  .description('Set up agents-cli, or re-open the capability onboarding hub.')
372
372
  .option('-f, --force', 'Re-run setup even if ~/.agents/.system/ already exists (use with caution)')
373
373
  .option('--no-system-repo', 'Skip cloning the system repo (you must populate ~/.agents/.system/ yourself)');
374
- // Capability subcommands: `agents setup browser|computer|mine|secrets|accounts|fleet|alias|beta`.
375
- // Share/artifact publishing is set up by `agents artifacts setup` (RUSH-2580);
376
- // the hub below still offers it as a phase via runShareWizard.
374
+ // Capability subcommands: `agents setup browser|computer|term|mine|secrets|accounts|fleet|alias|beta`.
375
+ // Artifact publishing is no longer set up here — it lives in the standalone
376
+ // `artifacts` CLI (`artifacts share setup`/`join`, PHNX-3992).
377
377
  registerSetupBrowserCommand(setupCmd);
378
378
  registerSetupComputerCommand(setupCmd);
379
+ registerSetupTermCommand(setupCmd);
379
380
  registerSetupMineCommand(setupCmd);
380
381
  registerSetupSecretsCommand(setupCmd);
381
382
  registerSetupAccountsCommand(setupCmd);
@@ -404,9 +405,25 @@ export function registerSetupCommand(program) {
404
405
  `,
405
406
  });
406
407
  setupCmd.command('status')
407
- .description('Show setup readiness for core, browser, computer, secrets, accounts, fleet, share, watchdog, and preferences.')
408
+ .description('Show setup readiness for core, browser, computer, secrets, term, accounts, fleet, watchdog, and preferences.')
408
409
  .option('--json', 'print machine-readable JSON')
410
+ .option('--tool <name>', 'Read cached standalone tool status: browser, computer, secrets, term, or all')
411
+ .option('--refresh', 'Explicitly refresh only the selected standalone tool health checks')
409
412
  .action(async (options) => {
413
+ if (options.tool || options.refresh) {
414
+ const selected = options.tool ?? 'all';
415
+ if (selected !== 'all' && !SETUP_TOOLS.includes(selected))
416
+ throw new Error('Unknown tool. Choose browser, computer, secrets, term, or all.');
417
+ const rows = options.refresh
418
+ ? await refreshToolSetup(selected)
419
+ : getCachedToolSetup().filter((row) => selected === 'all' || row.tool === selected);
420
+ if (options.json)
421
+ console.log(JSON.stringify(rows));
422
+ else
423
+ for (const row of rows)
424
+ console.log(`${row.tool}: ${row.installed === false ? 'not installed' : row.readiness} · ${row.detail}`);
425
+ return;
426
+ }
410
427
  const rows = await getSetupStatus();
411
428
  if (options.json)
412
429
  console.log(JSON.stringify(rows, null, 2));
@@ -427,6 +444,7 @@ export function registerSetupCommand(program) {
427
444
  agents setup browser
428
445
  agents setup computer
429
446
  agents setup secrets
447
+ agents setup term
430
448
  agents setup accounts
431
449
  agents setup fleet
432
450
  agents setup watchdog
@@ -443,8 +461,8 @@ export function registerSetupCommand(program) {
443
461
  Capability setup can also be run any time on its own:
444
462
  agents setup browser # detect a browser + create the default profile
445
463
  agents setup computer # install the signed macOS helper + grant permissions
446
- agents artifacts setup # provision or join a Cloudflare share endpoint
447
464
  agents setup secrets # choose secrets backend/policy defaults + import
465
+ agents setup term # install the standalone term CLI (PTY engine for fleet login / auth mint)
448
466
  agents setup accounts # mint a Claude setup-token for unattended usage/probe
449
467
  agents setup fleet # discover Tailscale devices + configure SSH access
450
468
  agents setup watchdog # choose which devices run the daemon watchdog pass
@@ -511,14 +511,6 @@ Examples:
511
511
  const output = lines.join('\n');
512
512
  printWithPager(output, lines.length);
513
513
  });
514
- // Deprecated alias for 'view'
515
- skillsCmd
516
- .command('info [name]', { hidden: true })
517
- .action(async (name) => {
518
- console.log(chalk.yellow('Deprecated: Use "agents skills view" instead of "agents skills info"\n'));
519
- // Re-execute view command logic
520
- await skillsCmd.commands.find((c) => c.name() === 'view')?.parseAsync(['view', ...(name ? [name] : [])], { from: 'user' });
521
- });
522
514
  }
523
515
  /**
524
516
  * Build the row data for `agents skills list`. Each row = one central skill
@@ -83,5 +83,11 @@ export interface HarnessInventoryOpts {
83
83
  */
84
84
  export declare function collectFleetHarnesses(opts: HarnessInventoryOpts): Promise<HostHarnessResult[]>;
85
85
  export declare function runDevicesAccounts(opts: HarnessInventoryOpts): Promise<void>;
86
+ /**
87
+ * Parse `--argv`'s JSON array, or report exactly what was wrong and return
88
+ * undefined. Fails loud rather than coercing: a caller that meant to pass argv
89
+ * and typo'd the JSON must not silently get shell-string semantics instead.
90
+ */
91
+ export declare function parseArgvJson(raw: string, log?: (line: string) => void): string[] | undefined;
86
92
  /** Register both `agents ssh` and `agents devices`. */
87
93
  export declare function registerSshCommands(program: Command): void;
@@ -58,7 +58,6 @@ import { crabboxList, crabboxFind, crabboxSshArgv } from '../lib/crabbox/cli.js'
58
58
  import { boxAddress, boxStatus, fmtIdleShort, fmtExpiresShort, registerLeaseCommand } from './lease.js';
59
59
  import { registerSnapshotCommand } from './snapshot.js';
60
60
  import { authCellColor, formatCheckedAge, isDeadVerdict, readAuthHealthCache, summarizeHostAuth, summarizeVerdicts, verdictColor, verdictLabel, writeFleetAuthRows, } from '../lib/auth-health.js';
61
- import { runFleetLogin } from '../lib/fleet/remote-login.js';
62
61
  import { getConfigValue, listConfig, setConfigValue, unsetConfigValue, configKeySpec, autoPoolMode, configuredDeviceRole, listConfiguredDeviceRoles, setConfiguredDeviceRole, } from '../lib/device-config.js';
63
62
  import { filterAutoPool, listWorkerDevices } from '../lib/devices/pool.js';
64
63
  import { registerCommandGroups, setHelpSections } from '../lib/help.js';
@@ -1055,46 +1054,6 @@ function renderAuthMatrix(results, opts) {
1055
1054
  }
1056
1055
  return lines;
1057
1056
  }
1058
- /** Render the final `fleet login` result summary in the house auth-matrix style. */
1059
- function renderLoginMatrix(results) {
1060
- const lines = [chalk.bold('Fleet login')];
1061
- if (results.length === 0) {
1062
- lines.push(chalk.gray(' Nothing pending — every requested account is already logged in.'));
1063
- return lines;
1064
- }
1065
- const nameW = Math.max(6, ...results.map((r) => `${r.agent}@${r.device}`.length));
1066
- for (const r of results) {
1067
- const who = `${r.agent}@${r.device}`.padEnd(nameW);
1068
- let badge;
1069
- switch (r.state) {
1070
- case 'authorized':
1071
- badge = chalk.green('authorized');
1072
- break;
1073
- case 'ready':
1074
- badge = chalk.cyan('code ready');
1075
- break;
1076
- case 'driving':
1077
- badge = chalk.yellow('driving');
1078
- break;
1079
- case 'skipped':
1080
- badge = chalk.gray('not remotable');
1081
- break;
1082
- case 'error':
1083
- badge = chalk.red('error');
1084
- break;
1085
- default:
1086
- badge = chalk.gray(r.state);
1087
- break;
1088
- }
1089
- const note = r.detail ? chalk.dim(` ${r.detail}`) : (r.reason && !r.remotable ? chalk.dim(` ${r.reason}`) : '');
1090
- lines.push(` ${who} ${badge}${note}`);
1091
- }
1092
- const auth = results.filter((r) => r.state === 'authorized').length;
1093
- const remotable = results.filter((r) => r.remotable).length;
1094
- lines.push('');
1095
- lines.push(chalk.gray(` ${auth}/${remotable} remotable logins authorized`));
1096
- return lines;
1097
- }
1098
1057
  /** Register the `agents devices` command tree (also aliased as `fleet`). */
1099
1058
  function registerDevicesCommands(program) {
1100
1059
  const devicesCmd = program
@@ -1122,7 +1081,8 @@ function registerDevicesCommands(program) {
1122
1081
  agents devices config mac-mini notes "runs the releases"
1123
1082
  agents devices config win-mini ssh.auth password
1124
1083
  agents devices config worker ssh.identity-file ~/.ssh/worker_ed25519
1125
- agents devices config mac-mini auto-launch.enabled off
1084
+ agents devices disable mac-mini # leave the auto-placement pool (auto-launch.enabled off)
1085
+ agents devices enable mac-mini # rejoin it
1126
1086
  agents devices describe mark-1 "gpu box — cuda 12.4" # one-line purpose, shown in the list
1127
1087
  agents devices config mac-mini interactive.host zion # where agents show YOU artifacts
1128
1088
  agents devices render --write # write ~/.ssh/config.d/agents include
@@ -1140,7 +1100,7 @@ function registerDevicesCommands(program) {
1140
1100
  { title: 'Inspect', names: ['list', 'show', 'status', 'ping', 'harnesses', 'accounts', 'snapshot'] },
1141
1101
  { title: 'Disposable devices', names: ['lease'] },
1142
1102
  { title: 'Configure a device', names: ['config', 'describe', 'render'] },
1143
- { title: 'Fleet operations', names: ['update', 'run', 'login', 'capture', 'apply', 'worktrees'] },
1103
+ { title: 'Fleet operations', names: ['update', 'run', 'capture', 'apply', 'worktrees'] },
1144
1104
  ]);
1145
1105
  devicesCmd
1146
1106
  .command('sync')
@@ -1286,11 +1246,9 @@ function registerDevicesCommands(program) {
1286
1246
  // ─── devices config (unified settings surface) ────────────────────────────
1287
1247
  //
1288
1248
  // ONE command for every per-device setting: `agents devices config <name>
1289
- // [key] [value] [--unset] [--json]`. The retired subcommands (configure,
1290
- // note, set-interactive, set, enable/disable/prefer/unprefer) are hidden
1291
- // tombstones below — each prints a deprecation notice on STDERR (so a --json
1292
- // consumer's stdout stays parseable) and delegates to this same engine,
1293
- // preserving its old output shape and exit codes.
1249
+ // [key] [value] [--unset] [--json]`. configure, note, set-interactive, set,
1250
+ // prefer, and unprefer are deleted (PHNX-4051) — use this command directly.
1251
+ // enable/disable stay as first-class sugar over auto-launch.enabled.
1294
1252
  /** Parse a raw CLI string into a config key's typed value (bool/int pass validation, strings verbatim). */
1295
1253
  const parseConfigValueInput = (spec, raw) => {
1296
1254
  switch (spec.type) {
@@ -1685,8 +1643,10 @@ function registerDevicesCommands(program) {
1685
1643
  the device itself; --fleet still writes a fleet-wide default those boxes
1686
1644
  inherit until they override.
1687
1645
 
1688
- The retired subcommands still work and forward here: configure, note,
1689
- set, set-interactive, enable, disable, prefer, unprefer.
1646
+ configure, note, set, set-interactive, prefer, and unprefer are deleted;
1647
+ use this command directly (auto-launch.preferred, interactive.host, or
1648
+ any other key). enable/disable stay as first-class sugar over
1649
+ auto-launch.enabled.
1690
1650
  `,
1691
1651
  });
1692
1652
  const roleCmd = devicesCmd
@@ -1765,182 +1725,6 @@ function registerDevicesCommands(program) {
1765
1725
  long-form scratch use 'agents devices config <name> notes "…"'.
1766
1726
  `,
1767
1727
  });
1768
- /** Deprecation notice for a retired subcommand — STDERR only, so a --json consumer's stdout stays parseable. */
1769
- const configTombstoneNotice = (retired, replacement) => {
1770
- console.error(chalk.yellow(`Deprecated: "agents devices ${retired}" is now "agents devices ${replacement}". Running that for you.\n`));
1771
- };
1772
- devicesCmd
1773
- .command('enable <name>', { hidden: true })
1774
- .action(async (name) => {
1775
- try {
1776
- configTombstoneNotice('enable <name>', 'config <name> auto-launch.enabled on');
1777
- await mustGetDevice(name);
1778
- // Back to the default (enabled) = remove the key.
1779
- await runDevicesConfig(name, 'auto-launch.enabled', [], { unset: true, quiet: true });
1780
- console.log(chalk.green(`Enabled '${name}'`) + chalk.gray(' for AGI EXT auto-launch.'));
1781
- }
1782
- catch (err) {
1783
- console.error(chalk.red(err.message));
1784
- process.exit(1);
1785
- }
1786
- });
1787
- devicesCmd
1788
- .command('disable <name>', { hidden: true })
1789
- .action(async (name) => {
1790
- try {
1791
- configTombstoneNotice('disable <name>', 'config <name> auto-launch.enabled off');
1792
- await mustGetDevice(name);
1793
- await runDevicesConfig(name, 'auto-launch.enabled', ['off'], { quiet: true });
1794
- console.log(chalk.green(`Disabled '${name}'`) + chalk.gray(' for AGI EXT auto-launch.'));
1795
- }
1796
- catch (err) {
1797
- console.error(chalk.red(err.message));
1798
- process.exit(1);
1799
- }
1800
- });
1801
- devicesCmd
1802
- .command('prefer <name>', { hidden: true })
1803
- .action(async (name) => {
1804
- try {
1805
- configTombstoneNotice('prefer <name>', 'config <name> auto-launch.preferred on');
1806
- await mustGetDevice(name);
1807
- await runDevicesConfig(name, 'auto-launch.preferred', ['on'], { quiet: true });
1808
- console.log(chalk.green(`Preferred '${name}'`) + chalk.gray(' for AGI EXT auto-launch.'));
1809
- }
1810
- catch (err) {
1811
- console.error(chalk.red(err.message));
1812
- process.exit(1);
1813
- }
1814
- });
1815
- devicesCmd
1816
- .command('unprefer <name>', { hidden: true })
1817
- .action(async (name) => {
1818
- try {
1819
- configTombstoneNotice('unprefer <name>', 'config <name> auto-launch.preferred off');
1820
- await mustGetDevice(name);
1821
- // Back to the default (not preferred) = remove the key.
1822
- await runDevicesConfig(name, 'auto-launch.preferred', [], { unset: true, quiet: true });
1823
- console.log(chalk.green(`No longer preferring '${name}'`) + chalk.gray(' for AGI EXT auto-launch.'));
1824
- }
1825
- catch (err) {
1826
- console.error(chalk.red(err.message));
1827
- process.exit(1);
1828
- }
1829
- });
1830
- devicesCmd
1831
- .command('set-interactive [name]', { hidden: true })
1832
- .option('--unset', 'clear the interactive host')
1833
- .option('--json', 'output machine-readable JSON')
1834
- .action(async (name, opts) => {
1835
- try {
1836
- configTombstoneNotice('set-interactive [name]', 'config <name> interactive.host <name>');
1837
- if (opts.unset) {
1838
- unsetConfigValue('interactive.host');
1839
- if (opts.json)
1840
- writeJson({ interactiveHost: null });
1841
- else
1842
- console.log(chalk.green('Cleared the interactive host.'));
1843
- return;
1844
- }
1845
- if (name) {
1846
- await mustGetDevice(name);
1847
- setConfigValue('interactive.host', name);
1848
- if (opts.json)
1849
- writeJson({ interactiveHost: name });
1850
- else
1851
- console.log(chalk.green(`Interactive host: '${name}'`) + chalk.gray(' — agents show you artifacts there. Clear with --unset.'));
1852
- return;
1853
- }
1854
- const current = getConfigValue('interactive.host').value;
1855
- if (opts.json) {
1856
- writeJson({ interactiveHost: current ?? null });
1857
- }
1858
- else if (current) {
1859
- console.log(`${chalk.bold('Interactive host:')} ${chalk.cyan(current)}`);
1860
- }
1861
- else {
1862
- console.log(chalk.gray("No interactive host set. Set one with 'agents devices config <name> interactive.host <name>'."));
1863
- }
1864
- }
1865
- catch (err) {
1866
- console.error(chalk.red(err.message));
1867
- process.exit(1);
1868
- }
1869
- });
1870
- devicesCmd
1871
- .command('configure <name>', { hidden: true })
1872
- .option('--max-agents <n>', 'cap concurrent agents')
1873
- .option('--scheduler <on|off>', 'allow the routines scheduler (daemon) to fire on this device')
1874
- .option('--json', 'output machine-readable JSON')
1875
- .action(async (name, opts) => {
1876
- try {
1877
- configTombstoneNotice('configure <name> [--max-agents N] [--scheduler on|off]', 'config <name> <key> <value>');
1878
- await mustGetDevice(name);
1879
- const writes = [];
1880
- if (opts.maxAgents !== undefined)
1881
- writes.push(['agents.max-concurrent', opts.maxAgents]);
1882
- if (opts.scheduler !== undefined) {
1883
- if (opts.scheduler !== 'on' && opts.scheduler !== 'off') {
1884
- throw new Error(`--scheduler expects 'on' or 'off', got '${opts.scheduler}'.`);
1885
- }
1886
- writes.push(['scheduler.enabled', opts.scheduler]);
1887
- }
1888
- for (const [key, value] of writes) {
1889
- await runDevicesConfig(name, key, [value], { quiet: Boolean(opts.json) });
1890
- }
1891
- if (opts.json)
1892
- printDevicesConfig(name, true);
1893
- else if (writes.length === 0)
1894
- printDevicesConfig(name, false);
1895
- }
1896
- catch (err) {
1897
- console.error(chalk.red(err.message));
1898
- process.exit(1);
1899
- }
1900
- });
1901
- devicesCmd
1902
- .command('note <name> [text...]', { hidden: true })
1903
- .option('--clear', 'remove all notes from the device')
1904
- .option('--json', 'output machine-readable JSON')
1905
- .action(async (name, text, opts) => {
1906
- try {
1907
- configTombstoneNotice('note <name> [text...]', 'config <name> notes <text>');
1908
- await mustGetDevice(name);
1909
- if (opts.clear) {
1910
- await runDevicesConfig(name, 'notes', [], { unset: true, quiet: true });
1911
- if (opts.json)
1912
- writeJson({ device: name, notes: [] });
1913
- else
1914
- console.log(chalk.green(`Cleared notes on '${name}'.`));
1915
- return;
1916
- }
1917
- if (text.length > 0) {
1918
- await runDevicesConfig(name, 'notes', text, { quiet: true });
1919
- const notes = getConfigValue('notes', { device: name }).value ?? [];
1920
- if (opts.json)
1921
- writeJson({ device: name, notes });
1922
- else
1923
- console.log(chalk.green(`Noted on '${name}':`) + ` ${text.join(' ')}`);
1924
- return;
1925
- }
1926
- const notes = getConfigValue('notes', { device: name }).value ?? [];
1927
- if (opts.json) {
1928
- writeJson({ device: name, notes });
1929
- }
1930
- else if (notes.length > 0) {
1931
- console.log(chalk.bold(`Notes for '${name}'`));
1932
- for (const n of notes)
1933
- console.log(` ${chalk.gray('•')} ${n}`);
1934
- }
1935
- else {
1936
- console.log(chalk.gray(`No notes on '${name}'. Add one with 'agents devices config ${name} notes "..."'.`));
1937
- }
1938
- }
1939
- catch (err) {
1940
- console.error(chalk.red(err.message));
1941
- process.exit(1);
1942
- }
1943
- });
1944
1728
  const runList = async (opts = {}) => {
1945
1729
  const reg = await loadDevices();
1946
1730
  const names = Object.keys(reg).sort();
@@ -2027,6 +1811,34 @@ function registerDevicesCommands(program) {
2027
1811
  }
2028
1812
  };
2029
1813
  devicesCmd.action(runList);
1814
+ devicesCmd
1815
+ .command('enable <name>')
1816
+ .description('Put a device back in the automatic-placement pool (clears auto-launch.enabled)')
1817
+ .action(async (name) => {
1818
+ try {
1819
+ await mustGetDevice(name);
1820
+ await runDevicesConfig(name, 'auto-launch.enabled', [], { unset: true, quiet: true });
1821
+ console.log(chalk.green(`Enabled '${name}'`) + chalk.gray(' for automatic placement.'));
1822
+ }
1823
+ catch (err) {
1824
+ console.error(chalk.red(err.message));
1825
+ process.exit(1);
1826
+ }
1827
+ });
1828
+ devicesCmd
1829
+ .command('disable <name>')
1830
+ .description('Drop a device from every automatic-placement path (sets auto-launch.enabled off)')
1831
+ .action(async (name) => {
1832
+ try {
1833
+ await mustGetDevice(name);
1834
+ await runDevicesConfig(name, 'auto-launch.enabled', ['off'], { quiet: true });
1835
+ console.log(chalk.green(`Disabled '${name}'`) + chalk.gray(' for automatic placement.'));
1836
+ }
1837
+ catch (err) {
1838
+ console.error(chalk.red(err.message));
1839
+ process.exit(1);
1840
+ }
1841
+ });
2030
1842
  devicesCmd
2031
1843
  .command('list')
2032
1844
  .alias('ls')
@@ -2155,33 +1967,6 @@ agents devices accounts --json # machine-readable, per-host account gr
2155
1967
  Collapses the installs that share one account (e.g. five claude versions on one
2156
1968
  email) into a single row. Use \`agents devices harnesses\` for the per-install view.`,
2157
1969
  });
2158
- devicesCmd
2159
- .command('login')
2160
- .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).')
2161
- .option('--agents <csv>', 'only these agents (comma-separated); default: every agent with a device-code flow')
2162
- .option('--devices <csv>', 'only these devices (comma-separated); default: every online box')
2163
- .option('--all', 'target every device-code pair regardless of cached login state (cold cache / forced re-login)')
2164
- .option('--interactive', 'guided one-box-at-a-time wizard (codes requested just-in-time)')
2165
- .option('--json', 'output the final result matrix as JSON')
2166
- .action(async (opts) => {
2167
- const csv = (s) => (s ? s.split(',').map((x) => x.trim()).filter(Boolean) : undefined);
2168
- const results = await runFleetLogin({
2169
- agents: csv(opts.agents),
2170
- devices: csv(opts.devices),
2171
- all: opts.all,
2172
- interactive: opts.interactive,
2173
- json: opts.json,
2174
- });
2175
- if (opts.json) {
2176
- console.log(JSON.stringify(results, null, 2));
2177
- }
2178
- else {
2179
- for (const line of renderLoginMatrix(results))
2180
- console.log(line);
2181
- }
2182
- if (results.some((r) => r.remotable && r.state !== 'authorized'))
2183
- process.exitCode = 1;
2184
- });
2185
1970
  devicesCmd
2186
1971
  .command('show <name>')
2187
1972
  .description('Show the full profile for one device.')
@@ -2214,53 +1999,6 @@ email) into a single row. Use \`agents devices harnesses\` for the per-install v
2214
1999
  process.exit(1);
2215
2000
  }
2216
2001
  });
2217
- devicesCmd
2218
- .command('set <name>', { hidden: true })
2219
- .option('--platform <platform>', 'windows | linux | macos')
2220
- .option('--user <user>', 'login user')
2221
- .option('--auth <method>', 'key | password')
2222
- .option('--bundle <bundle>', 'secrets bundle holding the password (for --auth password)')
2223
- .option('--bundle-key <key>', "key within the bundle (default 'password')")
2224
- .option('--identity-file <path>', 'private-key path for --auth key')
2225
- .option('--clear-identity-file', 'return key auth to ssh-agent/default-key discovery')
2226
- .action(async (name, opts) => {
2227
- try {
2228
- configTombstoneNotice('set <name> [--platform|--user|--auth|--bundle|--bundle-key|--identity-file …]', 'config <name> <ssh.*|platform> <value>');
2229
- const existing = await mustGetDevice(name);
2230
- const nextMethod = opts.auth ?? resolveDeviceProfile(existing).auth.method;
2231
- if (opts.identityFile && nextMethod !== 'key') {
2232
- throw new Error('--identity-file requires key auth; pass --auth key in the same command.');
2233
- }
2234
- const writes = [];
2235
- if (opts.platform)
2236
- writes.push({ key: 'platform', value: opts.platform });
2237
- if (opts.user)
2238
- writes.push({ key: 'ssh.user', value: opts.user });
2239
- if (opts.auth)
2240
- writes.push({ key: 'ssh.auth', value: opts.auth });
2241
- if (opts.bundle)
2242
- writes.push({ key: 'ssh.bundle', value: opts.bundle });
2243
- if (opts.bundleKey)
2244
- writes.push({ key: 'ssh.bundle-key', value: opts.bundleKey });
2245
- if (opts.identityFile)
2246
- writes.push({ key: 'ssh.identity-file', value: opts.identityFile });
2247
- if (opts.clearIdentityFile)
2248
- writes.push({ key: 'ssh.identity-file' }); // unset
2249
- if (writes.length === 0) {
2250
- printDevicesConfig(name, false);
2251
- return;
2252
- }
2253
- for (const w of writes) {
2254
- await runDevicesConfig(name, w.key, w.value !== undefined ? [w.value] : [], { unset: w.value === undefined, quiet: true });
2255
- }
2256
- const d = resolveDeviceProfile((await mustGetDevice(name)));
2257
- console.log(chalk.green(`Updated device '${name}'`) + chalk.gray(` (auth: ${d.auth.method}${d.auth.bundle ? ` via ${d.auth.bundle}` : ''})`));
2258
- }
2259
- catch (err) {
2260
- console.error(chalk.red(err.message));
2261
- process.exit(1);
2262
- }
2263
- });
2264
2002
  devicesCmd
2265
2003
  .command('remove <name>')
2266
2004
  .alias('rm')
@@ -2633,12 +2371,40 @@ async function doDeviceTaskStop(ref) {
2633
2371
  process.exitCode = 1;
2634
2372
  }
2635
2373
  }
2374
+ /**
2375
+ * Parse `--argv`'s JSON array, or report exactly what was wrong and return
2376
+ * undefined. Fails loud rather than coercing: a caller that meant to pass argv
2377
+ * and typo'd the JSON must not silently get shell-string semantics instead.
2378
+ */
2379
+ export function parseArgvJson(raw, log = (line) => console.error(line)) {
2380
+ let parsed;
2381
+ try {
2382
+ parsed = JSON.parse(raw);
2383
+ }
2384
+ catch (error) {
2385
+ log(`--argv is not valid JSON: ${error.message}`);
2386
+ log(`Expected a JSON array of strings, e.g. --argv '["uptime","-p"]'`);
2387
+ return undefined;
2388
+ }
2389
+ if (!Array.isArray(parsed)) {
2390
+ log('--argv must be a JSON ARRAY of strings.');
2391
+ return undefined;
2392
+ }
2393
+ const bad = parsed.findIndex((token) => typeof token !== 'string');
2394
+ if (bad >= 0) {
2395
+ log(`--argv element ${bad} is ${typeof parsed[bad]}, not a string.`);
2396
+ log('Every element is one argv token; numbers and objects have no argv meaning.');
2397
+ return undefined;
2398
+ }
2399
+ return parsed;
2400
+ }
2636
2401
  /** Register the `agents ssh` smart wrapper. */
2637
2402
  function registerSshWrapper(program) {
2638
2403
  const sshCmd = program
2639
2404
  .command('ssh <name> [cmd...]')
2640
2405
  .description('Connect to a registered device. Preflights reachability, picks the right shell, and authenticates (key or password-from-bundle).')
2641
2406
  .allowUnknownOption()
2407
+ .option('--argv <json>', 'run a JSON array of argv tokens with exact per-token fidelity (no shell splitting)')
2642
2408
  .addHelpText('after', `
2643
2409
  Examples:
2644
2410
  agents ssh yosemite-s0 # interactive login (mirrors your project dir)
@@ -2646,6 +2412,8 @@ Examples:
2646
2412
  agents ssh win-mini hostname # run a command (PowerShell on Windows)
2647
2413
  agents ssh yosemite-s0 uptime # run a command (POSIX)
2648
2414
  agents ssh auto # affinity-pick a device (same engine as 'agents run --device auto')
2415
+ agents ssh box --argv '["agents","feed","post","--title","two words","a & b"]'
2416
+ # exact tokens: spaces and metacharacters survive
2649
2417
 
2650
2418
  Devices come from 'agents devices'. Password auth pulls the secret from a
2651
2419
  secrets bundle via an askpass shim — the password never touches argv.
@@ -2660,8 +2428,31 @@ the target when it exists, else the remote home. Same portable-cwd rule as
2660
2428
  An 'agents browser …', 'ag browser …', or standalone 'browser …' command is
2661
2429
  stamped AGENTS_FLEET_REMOTE so the target's browser.remote-control consent
2662
2430
  gate applies, same as 'agents browser <verb> --device <name>'.
2431
+
2432
+ --argv takes a JSON array of strings and delivers each element as exactly ONE
2433
+ token on the peer, so a token containing a space, '&', '|', '$' or a quote
2434
+ arrives intact. The positional form keeps its existing semantics — the remote
2435
+ shell parses it — so the two are mutually exclusive rather than interchangeable.
2663
2436
  `)
2664
- .action(async (name, cmd) => {
2437
+ .action(async (name, cmd, opts) => {
2438
+ // `--argv` is a DIFFERENT delivery contract, not a spelling of the
2439
+ // positional form, so accepting both would have to silently pick one.
2440
+ if (opts.argv !== undefined && cmd.length > 0) {
2441
+ console.error(chalk.red('Pass either --argv <json> or a positional command, not both.'));
2442
+ console.error(chalk.gray('--argv delivers exact tokens; the positional form is parsed by the remote shell.'));
2443
+ process.exit(1);
2444
+ }
2445
+ let argvTokens;
2446
+ if (opts.argv !== undefined) {
2447
+ argvTokens = parseArgvJson(opts.argv);
2448
+ if (!argvTokens)
2449
+ process.exit(1);
2450
+ if (argvTokens.length === 0) {
2451
+ console.error(chalk.red('--argv needs at least one token (the program to run).'));
2452
+ process.exit(1);
2453
+ }
2454
+ cmd = argvTokens;
2455
+ }
2665
2456
  // Hidden askpass bridge: ssh execs the shim, which re-invokes us here.
2666
2457
  if (name === '__askpass') {
2667
2458
  await runAskpass();
@@ -2734,7 +2525,7 @@ gate applies, same as 'agents browser <verb> --device <name>'.
2734
2525
  // `agents run --device` (deriveMirroredCwd). Best-effort — a missing dir
2735
2526
  // falls back to the remote home. An explicit `cmd` keeps its cwd (RUSH-2412).
2736
2527
  const mirrorCwd = cmd.length === 0 ? deriveMirroredCwd(process.cwd()) : undefined;
2737
- const { args, env } = buildSshInvocation(device, cmd, shim, { pinned }, { interactiveCwd: mirrorCwd });
2528
+ const { args, env } = buildSshInvocation(device, cmd, shim, { pinned }, { interactiveCwd: mirrorCwd, ...(argvTokens ? { argv: true } : {}) });
2738
2529
  // Interactive login: make the local terminal's terminfo (e.g.
2739
2530
  // xterm-ghostty) available on the remote so backspace/colors/clear work.
2740
2531
  // Best-effort + cached per host — never blocks the login (see terminfo.ts).