@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
@@ -505,6 +505,67 @@ export const CONFIG_KEYS = [
505
505
  defaultValue: true,
506
506
  description: 'AGI Menu: show the open pull requests section for each project.',
507
507
  },
508
+ {
509
+ name: 'menubar.menu.sessionUpdates',
510
+ yamlKey: 'menubarMenuSessionUpdates',
511
+ scope: 'user',
512
+ type: 'bool',
513
+ defaultValue: true,
514
+ description: 'AGI Menu: show the session-updates section on Home. Off leaves Home to requests and progress ' +
515
+ 'only; the full list stays on the Sessions tab either way.',
516
+ },
517
+ {
518
+ name: 'menubar.menu.deviceSort',
519
+ yamlKey: 'menubarMenuDeviceSort',
520
+ scope: 'user',
521
+ type: 'string',
522
+ defaultValue: 'name',
523
+ description: 'AGI Menu: how the device list is ordered — name, role, load, memory, or disk. The spec orders read ' +
524
+ 'the same cached fleet stats the rows render, so a device with no observation sorts last rather than ' +
525
+ 'as zero.',
526
+ validate: oneOf('menubar.menu.deviceSort', ['name', 'role', 'load', 'memory', 'disk']),
527
+ },
528
+ {
529
+ name: 'menubar.menu.headlessAgent',
530
+ yamlKey: 'menubarMenuHeadlessAgent',
531
+ scope: 'user',
532
+ type: 'string',
533
+ description: 'AGI Menu: the harness a Headless run dispatches to by default (an agent id, e.g. claude). Unset ' +
534
+ 'leaves the choice to the menu\'s own default — this key never invents a primary harness.',
535
+ validate: (v) => isAgentId(v) ? null : `menubar.menu.headlessAgent must be an agent id (e.g. claude, codex), got ${JSON.stringify(v)}.`,
536
+ },
537
+ {
538
+ name: 'menubar.menu.headlessFallbackAgent',
539
+ yamlKey: 'menubarMenuHeadlessFallbackAgent',
540
+ scope: 'user',
541
+ type: 'string',
542
+ description: 'AGI Menu: the permitted alternate harness for a Headless run, dispatched as `agents run --fallback ' +
543
+ '<agent>`. It is reached only after every usable account of the primary harness is exhausted (the ' +
544
+ 'run\'s own same-agent account failover runs first). Unset sends no --fallback.',
545
+ validate: (v) => isAgentId(v) ? null : `menubar.menu.headlessFallbackAgent must be an agent id (e.g. codex, grok), got ${JSON.stringify(v)}.`,
546
+ },
547
+ {
548
+ name: 'menubar.menu.headlessPlacement',
549
+ yamlKey: 'menubarMenuHeadlessPlacement',
550
+ scope: 'user',
551
+ type: 'string',
552
+ defaultValue: 'auto',
553
+ description: 'AGI Menu: where a Headless run executes — `auto` (the CLI picks an eligible worker; `agents run ' +
554
+ '--device auto` never picks a personal or desktop box), `local` (this machine, no --device), ' +
555
+ '`interactive` (the box pinned as interactive.host), or a device name to pin one explicitly.',
556
+ validate: (v) => {
557
+ const value = v;
558
+ if (value === 'auto' || value === 'local' || value === 'interactive')
559
+ return null;
560
+ try {
561
+ assertValidDeviceName(value);
562
+ return null;
563
+ }
564
+ catch {
565
+ return 'menubar.menu.headlessPlacement must be auto | local | interactive | a device name.';
566
+ }
567
+ },
568
+ },
508
569
  ];
509
570
  /** Look up a key spec by CLI dotted name, or throw listing the known keys. */
510
571
  /**
@@ -30,13 +30,43 @@ export declare function sshTargetFor(device: DeviceProfile): string;
30
30
  */
31
31
  export declare function fleetDialTarget(device: DeviceProfile): string;
32
32
  /**
33
- * Wrap a remote command for the device's shell. Windows devices speak
34
- * PowerShell, so a bare command is run through `powershell -NoProfile
35
- * -EncodedCommand`; POSIX devices get the command verbatim (the remote login
36
- * shell parses it). Returns undefined when no command was given (interactive
33
+ * Render `cmd` as the single command string ssh sends to the peer.
34
+ *
35
+ * Windows devices speak PowerShell, so the result is run through
36
+ * `powershell -NoProfile -EncodedCommand`; POSIX devices get it as the remote
37
+ * login shell sees it. Returns undefined when no command was given (interactive
37
38
  * login).
39
+ *
40
+ * TWO MODES, and the difference is load-bearing rather than a convenience.
41
+ *
42
+ * The default joins the tokens RAW. That is not a bug to be tidied away: every
43
+ * existing caller of `agents ssh` relies on the remote shell interpreting what it
44
+ * is handed — `agents ssh box 'bash -lc "cd x && make"'` arrives as ONE token
45
+ * whose pipeline, globs and redirections the peer's shell must expand. Quoting
46
+ * that would ship the whole line as a literal argument and break it.
47
+ *
48
+ * `{ argv: true }` is for a caller that genuinely holds an argv ARRAY and needs
49
+ * each element delivered as exactly one token. Joining those raw destroys any
50
+ * token containing a space or a metacharacter — `['--title', 'two words']`
51
+ * arrives as three tokens, and `'a & b'` arrives as a backgrounded command — which
52
+ * is what a native client hitting this path actually hit.
53
+ *
54
+ * So fidelity is opt-in at the call site that knows which shape it has, and the
55
+ * quoting itself reuses the canonical helpers (`shellQuote`, {@link pwshQuote})
56
+ * rather than introducing a third escaping scheme.
38
57
  */
39
- export declare function wrapRemoteCommand(device: DeviceProfile, cmd: string[]): string | undefined;
58
+ export declare function wrapRemoteCommand(device: DeviceProfile, cmd: string[], opts?: {
59
+ argv?: boolean;
60
+ prelude?: string[];
61
+ }): string | undefined;
62
+ /**
63
+ * Single-quote one token for PowerShell's OWN parser. Inside a single-quoted pwsh
64
+ * string the only special character is `'`, escaped by doubling.
65
+ *
66
+ * This is correct for a value PowerShell itself consumes, and NOT sufficient for
67
+ * an argument handed on to a native program — see the Win32 note below.
68
+ */
69
+ export declare function pwshQuote(token: string): string;
40
70
  /**
41
71
  * True when `cmd` is a browser drive: `agents browser …`, `ag browser …`, or
42
72
  * the standalone `browser` binary (`cli/package.json` `bin.browser` →
@@ -65,6 +95,19 @@ export declare function isAgentsBrowserDrive(cmd: string[]): boolean;
65
95
  * fan-out stamps the marker before {@link buildSshInvocation}) is left unchanged
66
96
  * so nothing is doubled.
67
97
  */
98
+ /**
99
+ * The provenance prefix as READY SHELL SYNTAX for the device's shell.
100
+ *
101
+ * These tokens are already quoted/escaped for their target shell — a POSIX
102
+ * `K=V` pair is `shellQuote`d here, and a PowerShell assignment is a complete
103
+ * statement with its own doubled quotes. That matters because argv mode quotes
104
+ * every token it is handed: quoting THESE again turns
105
+ * `'AGENTS_ACTOR=Some Name'` into `''\''AGENTS_ACTOR=Some Name'\'''` and turns a
106
+ * pwsh assignment into an inert string literal. So the prelude is composed
107
+ * SEPARATELY from the caller's argv rather than concatenated into it, and this is
108
+ * the single definition both paths use.
109
+ */
110
+ export declare function fleetRemotePrelude(device: Pick<DeviceProfile, 'shell'>, provenanceEnv?: Record<string, string>): string[];
68
111
  export declare function markFleetRemote(cmd: string[], device: Pick<DeviceProfile, 'shell'>, provenanceEnv?: Record<string, string>): string[];
69
112
  /**
70
113
  * Build the remote command that starts an INTERACTIVE LOGIN shell inside a
@@ -136,6 +179,7 @@ export declare function deviceIdentityArgs(device: DeviceProfile): string[];
136
179
  export declare function buildSshInvocation(device: DeviceProfile, cmd: string[], askpassShimPath: string, hostKey?: SshHostKeyOptions, opts?: {
137
180
  agentOnly?: boolean;
138
181
  interactiveCwd?: string;
182
+ argv?: boolean;
139
183
  }): {
140
184
  args: string[];
141
185
  env: Record<string, string>;
@@ -18,12 +18,14 @@ import * as path from 'path';
18
18
  import { assertValidSshTarget, shellQuote } from '../ssh-exec.js';
19
19
  import { resolveActor, actorEnv } from '../actor.js';
20
20
  import { getCliLaunch } from '../cli-entry.js';
21
- import { encodePwshBase64 } from '../pwsh.js';
21
+ import { encodePwshBase64, pwshLiteral, pwshNativeExecStatements } from '../pwsh.js';
22
+ import { quoteWin32ExecArg } from '../platform/exec.js';
22
23
  import { homeRemainder, remoteCdPrefix } from '../project-root.js';
23
24
  import { getCacheDir } from '../state.js';
24
25
  import { hostKeyCheckingOpts } from './known-hosts.js';
25
26
  import { hostNameFor } from './ssh-config.js';
26
27
  import { resolveDeviceProfile } from './resolve-profile.js';
28
+ import { renderPowershellCommand, windowsAgentsInvocation } from '../hosts/remote-cmd.js';
27
29
  /** Env var the askpass shim reads to know which bundle holds the password. */
28
30
  export const ASKPASS_BUNDLE_ENV = 'AGENTS_SSH_BUNDLE';
29
31
  /** Env var the askpass shim reads to know which key in the bundle is the password. */
@@ -72,20 +74,139 @@ export function fleetDialTarget(device) {
72
74
  }
73
75
  }
74
76
  /**
75
- * Wrap a remote command for the device's shell. Windows devices speak
76
- * PowerShell, so a bare command is run through `powershell -NoProfile
77
- * -EncodedCommand`; POSIX devices get the command verbatim (the remote login
78
- * shell parses it). Returns undefined when no command was given (interactive
77
+ * Render `cmd` as the single command string ssh sends to the peer.
78
+ *
79
+ * Windows devices speak PowerShell, so the result is run through
80
+ * `powershell -NoProfile -EncodedCommand`; POSIX devices get it as the remote
81
+ * login shell sees it. Returns undefined when no command was given (interactive
79
82
  * login).
83
+ *
84
+ * TWO MODES, and the difference is load-bearing rather than a convenience.
85
+ *
86
+ * The default joins the tokens RAW. That is not a bug to be tidied away: every
87
+ * existing caller of `agents ssh` relies on the remote shell interpreting what it
88
+ * is handed — `agents ssh box 'bash -lc "cd x && make"'` arrives as ONE token
89
+ * whose pipeline, globs and redirections the peer's shell must expand. Quoting
90
+ * that would ship the whole line as a literal argument and break it.
91
+ *
92
+ * `{ argv: true }` is for a caller that genuinely holds an argv ARRAY and needs
93
+ * each element delivered as exactly one token. Joining those raw destroys any
94
+ * token containing a space or a metacharacter — `['--title', 'two words']`
95
+ * arrives as three tokens, and `'a & b'` arrives as a backgrounded command — which
96
+ * is what a native client hitting this path actually hit.
97
+ *
98
+ * So fidelity is opt-in at the call site that knows which shape it has, and the
99
+ * quoting itself reuses the canonical helpers (`shellQuote`, {@link pwshQuote})
100
+ * rather than introducing a third escaping scheme.
80
101
  */
81
- export function wrapRemoteCommand(device, cmd) {
102
+ export function wrapRemoteCommand(device, cmd, opts = {}) {
82
103
  if (cmd.length === 0)
83
104
  return undefined;
84
- const joined = cmd.join(' ');
105
+ const prelude = opts.prelude ?? [];
106
+ let script;
107
+ if (opts.argv) {
108
+ // Quote EACH caller token so the peer receives it byte-for-byte, then prefix
109
+ // the prelude VERBATIM — it is already shell syntax and re-quoting it would
110
+ // break it (see `fleetRemotePrelude`).
111
+ if (device.shell === 'powershell') {
112
+ script = pwshExactArgvScript(cmd, prelude);
113
+ }
114
+ else {
115
+ script = [...prelude, ...cmd.map(shellQuote)].join(' ');
116
+ }
117
+ }
118
+ else {
119
+ // The default joins raw, which is what lets a caller hand the remote shell
120
+ // something to interpret. See the docblock above for why both must exist.
121
+ script = [...prelude, ...cmd].join(' ');
122
+ }
85
123
  if (device.shell === 'powershell') {
86
- return `powershell -NoProfile -EncodedCommand ${encodePwshBase64(joined)}`;
124
+ // Same renderer the Windows `agents` launcher uses, so a long script gets the
125
+ // compressed representation here too rather than only on that path. The
126
+ // interactive login route (`buildInteractiveShellCommand`, -NoExit) is
127
+ // deliberately left alone: it must stay an interactive session.
128
+ return renderPowershellCommand(script);
87
129
  }
88
- return joined;
130
+ return script;
131
+ }
132
+ /**
133
+ * Single-quote one token for PowerShell's OWN parser. Inside a single-quoted pwsh
134
+ * string the only special character is `'`, escaped by doubling.
135
+ *
136
+ * This is correct for a value PowerShell itself consumes, and NOT sufficient for
137
+ * an argument handed on to a native program — see the Win32 note below.
138
+ */
139
+ export function pwshQuote(token) {
140
+ return `'${token.replace(/'/g, "''")}'`;
141
+ }
142
+ /**
143
+ * Emit a PowerShell script that runs `cmd` with EXACT argv, for either kind of
144
+ * target a Windows peer can name.
145
+ *
146
+ * Windows has no argv array: a process receives ONE string and splits it itself.
147
+ * PowerShell 5.1 rebuilds that string when it invokes a native program, and its
148
+ * serializer is lossy — measured on a real peer, an EMPTY argument is dropped and
149
+ * an embedded `"` is discarded, so the callee's argv silently shifts.
150
+ *
151
+ * The obvious fix, the `--%` stop-parsing token, is NOT used, because measurement
152
+ * killed it three ways:
153
+ * - it only applies to a NATIVE command, and `agents` on Windows resolves to
154
+ * `agents.ps1`, so a script target received `--%` as a literal argument and
155
+ * the whole remainder as one string;
156
+ * - a token containing a NEWLINE terminates the directive, producing a parser
157
+ * error;
158
+ * - it performs cmd-style `%VAR%` expansion, so a literal `%PATH%` became six
159
+ * arguments — the exact opposite of exact argv.
160
+ *
161
+ * So the script branches on what the peer's own command discovery finds:
162
+ *
163
+ * - **native executable** — launched through `System.Diagnostics.Process` with a
164
+ * pre-built `Arguments` string escaped by {@link quoteWin32ExecArg}. .NET hands
165
+ * that string to `CreateProcess` essentially verbatim, so the child's
166
+ * `CommandLineToArgvW` reconstructs the tokens exactly; no shell sees it, so
167
+ * no `%VAR%` expansion and no newline sensitivity. `UseShellExecute = $false`
168
+ * with no redirection leaves the child on the inherited handles, which is what
169
+ * lets a binary stdout stream through unchanged.
170
+ * - **anything else** (a `.ps1`/`.cmd` launcher, a function, a cmdlet, an alias)
171
+ * — invoked with a splatted PowerShell array. That is an in-process call, so
172
+ * the native serializer is never involved and every token survives as itself.
173
+ *
174
+ * The exit code is propagated in both branches; a script launcher that sets no
175
+ * `$LASTEXITCODE` is left alone rather than forced to 0.
176
+ */
177
+ function pwshExactArgvScript(cmd, prelude) {
178
+ // The Agents CLI gets the canonical launcher, not the generic dispatch. On
179
+ // Windows `agents` is an npm `agents.ps1` whose own body splats `$args` into
180
+ // native node.exe — the PowerShell 5.1 lossy path — so even a perfectly
181
+ // splatted call into that script loses an embedded quote one layer deeper.
182
+ // `windowsAgentsInvocation` resolves the package's declared entry and runs it
183
+ // directly, which is the only way the real Agents parser sees exact tokens.
184
+ const bin = cmd[0];
185
+ if (bin === 'agents' || bin === 'ag') {
186
+ // `windowsAgentsInvocation` leaves the child's code in `$zq`.
187
+ return [...prelude, windowsAgentsInvocation(cmd.slice(1), bin), 'exit $zq'].join('\n');
188
+ }
189
+ const program = pwshQuote(cmd[0]);
190
+ const rest = cmd.slice(1);
191
+ const splat = rest.length > 0 ? `@(${rest.map(pwshQuote).join(', ')})` : '@()';
192
+ return [
193
+ ...prelude,
194
+ `$ErrorActionPreference='Stop'`,
195
+ `$__c = Get-Command -Name ${program} -ErrorAction Stop`,
196
+ `if ($__c.CommandType -eq 'Application') {`,
197
+ // One emitter for the .NET native-exec block, shared with the Windows
198
+ // `agents` launcher in `hosts/remote-cmd.ts`; a second copy would drift.
199
+ ...pwshNativeExecStatements('$__c.Source', pwshLiteral(rest.map(quoteWin32ExecArg).join(' '))).map((line) => ` ${line}`),
200
+ // `pwshNativeExecStatements` names the process handle `$zp`.
201
+ ` exit $zp.ExitCode`,
202
+ `}`,
203
+ // `@__a` SPLATS the array into separate arguments. `& $__c @(...)` on an
204
+ // array LITERAL does not splat — it passes one array-valued argument, which
205
+ // a real peer reported back as every token collapsed into one.
206
+ `$__a = ${splat}`,
207
+ `& $__c @__a`,
208
+ `if ($null -ne $LASTEXITCODE) { exit $LASTEXITCODE }`,
209
+ ].join('\n');
89
210
  }
90
211
  /**
91
212
  * True when `cmd` is a browser drive: `agents browser …`, `ag browser …`, or
@@ -123,22 +244,43 @@ export function isAgentsBrowserDrive(cmd) {
123
244
  * fan-out stamps the marker before {@link buildSshInvocation}) is left unchanged
124
245
  * so nothing is doubled.
125
246
  */
126
- export function markFleetRemote(cmd, device, provenanceEnv = actorEnv(resolveActor())) {
247
+ /**
248
+ * The provenance prefix as READY SHELL SYNTAX for the device's shell.
249
+ *
250
+ * These tokens are already quoted/escaped for their target shell — a POSIX
251
+ * `K=V` pair is `shellQuote`d here, and a PowerShell assignment is a complete
252
+ * statement with its own doubled quotes. That matters because argv mode quotes
253
+ * every token it is handed: quoting THESE again turns
254
+ * `'AGENTS_ACTOR=Some Name'` into `''\''AGENTS_ACTOR=Some Name'\'''` and turns a
255
+ * pwsh assignment into an inert string literal. So the prelude is composed
256
+ * SEPARATELY from the caller's argv rather than concatenated into it, and this is
257
+ * the single definition both paths use.
258
+ */
259
+ export function fleetRemotePrelude(device, provenanceEnv = actorEnv(resolveActor())) {
127
260
  if (device.shell === 'powershell') {
128
- // Exact-match guard, symmetric with the POSIX branch below: the marker token
129
- // is always this literal, so `startsWith` would only loosen it for no gain.
130
- if (cmd[0] === `$env:AGENTS_FLEET_REMOTE='1';`)
131
- return cmd;
132
- const prelude = [
261
+ return [
133
262
  `$env:AGENTS_FLEET_REMOTE='1';`,
134
263
  ...Object.entries(provenanceEnv).map(([k, v]) => `$env:${k}='${v.replace(/'/g, "''")}';`),
135
264
  ];
136
- return [...prelude, ...cmd];
137
265
  }
138
- if (cmd[0] === 'env' && cmd[1] === 'AGENTS_FLEET_REMOTE=1')
266
+ return [
267
+ 'env',
268
+ 'AGENTS_FLEET_REMOTE=1',
269
+ ...Object.entries(provenanceEnv).map(([k, v]) => shellQuote(`${k}=${v}`)),
270
+ ];
271
+ }
272
+ /** True when `cmd` already carries the prelude {@link fleetRemotePrelude} emits. */
273
+ function alreadyMarked(cmd, device) {
274
+ if (device.shell === 'powershell')
275
+ return cmd[0] === `$env:AGENTS_FLEET_REMOTE='1';`;
276
+ return cmd[0] === 'env' && cmd[1] === 'AGENTS_FLEET_REMOTE=1';
277
+ }
278
+ export function markFleetRemote(cmd, device, provenanceEnv = actorEnv(resolveActor())) {
279
+ // Exact-match guard: the marker token is always this literal, so `startsWith`
280
+ // would only loosen it for no gain.
281
+ if (alreadyMarked(cmd, device))
139
282
  return cmd;
140
- const actorTokens = Object.entries(provenanceEnv).map(([k, v]) => shellQuote(`${k}=${v}`));
141
- return ['env', 'AGENTS_FLEET_REMOTE=1', ...actorTokens, ...cmd];
283
+ return [...fleetRemotePrelude(device, provenanceEnv), ...cmd];
142
284
  }
143
285
  /**
144
286
  * Build the remote command that starts an INTERACTIVE LOGIN shell inside a
@@ -229,10 +371,16 @@ export function buildSshInvocation(device, cmd, askpassShimPath, hostKey = {}, o
229
371
  // Stamp the consent marker on the REMOTE command, not the local ssh env:
230
372
  // SSH_ASKPASS lives on this side; AGENTS_FLEET_REMOTE must be visible to the
231
373
  // process that runs on the peer.
232
- const remoteCmd = !interactive && isAgentsBrowserDrive(cmd) ? markFleetRemote(cmd, device) : cmd;
374
+ // Provenance is composed as a PRELUDE, not prepended to the argv array. In argv
375
+ // mode every token handed to `wrapRemoteCommand` is quoted, and the prelude is
376
+ // already shell syntax — mixing them in meant the actor pairs were quoted twice
377
+ // (breaking any value with a space or a quote) and the pwsh assignments became
378
+ // inert string literals.
379
+ const needsProvenance = !interactive && isAgentsBrowserDrive(cmd) && !alreadyMarked(cmd, device);
380
+ const prelude = needsProvenance ? fleetRemotePrelude(device) : [];
233
381
  const remote = interactive
234
382
  ? buildInteractiveShellCommand(device, opts.interactiveCwd)
235
- : wrapRemoteCommand(device, remoteCmd);
383
+ : wrapRemoteCommand(device, cmd, { ...(opts.argv ? { argv: true } : {}), ...(prelude.length > 0 ? { prelude } : {}) });
236
384
  const env = {};
237
385
  const args = [
238
386
  ...hostKeyCheckingOpts(hostKey.pinned ?? false, hostKey.knownHostsFile),
@@ -41,7 +41,7 @@ import chalk from 'chalk';
41
41
  import * as path from 'path';
42
42
  import { AGENTS, ALL_AGENT_IDS, supportsAccountInspection } from '../agents.js';
43
43
  import { blocksLocalScripts } from '../platform/winpath.js';
44
- import { loginHint } from '../signin-badge.js';
44
+ import { loginHint, loginSubcommand, SUBCOMMAND_LOGIN_AGENTS } from '../signin-badge.js';
45
45
  import { CONFIG_ENV_ISOLATED_AGENTS } from '../installations/shims.js';
46
46
  import { padToWidth, stringWidth } from '../text/width.js';
47
47
  import { windowsSshEnrollmentProblem } from './windows-ssh-enrollment.js';
@@ -78,11 +78,7 @@ const NO_PER_VERSION_LOGIN = new Set(ALL_AGENT_IDS.filter((a) => !ISOLATED_LOGIN
78
78
  * `subcommand` — `<cli> login` / `<cli> auth login`, runnable via `--`
79
79
  * `in-tui` — claude only: a `/login` slash command inside its own TUI
80
80
  * `on-launch` — the device/oauth flow starts when the agent launches */
81
- const LOGIN_SUBCOMMAND = {
82
- codex: 'login',
83
- grok: 'login',
84
- opencode: 'auth login',
85
- };
81
+ const LOGIN_SUBCOMMAND = Object.fromEntries(SUBCOMMAND_LOGIN_AGENTS.map((agent) => [agent, loginSubcommand(agent)]));
86
82
  /** Kinds whose fix is inherently PER VERSION, so a row collapsed across versions
87
83
  * could not carry a correct remediation. A login is the whole set: there is no
88
84
  * `@all` selector for it, and dropping the version falls back to the bare native
@@ -1,14 +1,133 @@
1
1
  import { type ActiveSession } from '../session/active.js';
2
2
  import { type MessageReceipt } from './feed.js';
3
+ /**
4
+ * Total budget for one answer. Sits below the AGI Menu's 30s abandon so the
5
+ * operator always gets a typed verdict instead of a timeout.
6
+ */
7
+ export declare const ANSWER_DEADLINE_MS = 20000;
8
+ /** Slice of the budget the optional PR-review enrichment may spend. */
9
+ export declare const PR_ENRICHMENT_BUDGET_MS = 4000;
10
+ /** Budget for the forwarded leg — the remote repeats the local work under its own deadline. */
11
+ export declare const REMOTE_ANSWER_TIMEOUT_MS = 25000;
12
+ /**
13
+ * A claim older than this with no receipt was stranded by a kill, not left in
14
+ * flight: it exceeds a full local deadline plus the remote leg, so no live
15
+ * delivery can still be running behind it.
16
+ */
17
+ export declare const STRANDED_CLAIM_MS = 60000;
18
+ /**
19
+ * How long to watch a `resume` child before giving up on a verdict. A resume
20
+ * runs the agent's whole next turn — minutes — so waiting for exit would blow the
21
+ * operator deadline on every headless answer. Only a clean early exit is booked;
22
+ * a non-zero exit or a still-running child is unknown, because neither proves
23
+ * the agent did or did not accept the prompt.
24
+ */
25
+ export declare const RESUME_SETTLE_MS = 2000;
26
+ /**
27
+ * How long a caller that LOST the claim waits for the holder's receipt before
28
+ * reporting unknown. The holder is mid-delivery — a double-clicked answer is the
29
+ * common case — so a short wait turns "I can't tell" into the real receipt,
30
+ * without ever delivering a second copy.
31
+ */
32
+ export declare const HOLDER_RECEIPT_WAIT_MS = 400;
33
+ /** Why an answer could not be delivered — the discriminant the operator UI branches on. */
34
+ export type AnswerFailureCode = 'malformed_key' | 'no_session' | 'stale' | 'unverified' | 'unauthorized' | 'unknown_choice' | 'empty_answer' | 'refused' | 'rail_failed' | 'timeout' | 'remote_failed';
35
+ /** A CONFIRMED failure: nothing reached a rail, the claim is released, retry is safe. */
36
+ export declare class AnswerError extends Error {
37
+ readonly code: AnswerFailureCode;
38
+ constructor(message: string, code: AnswerFailureCode);
39
+ }
40
+ /**
41
+ * An UNCONFIRMED outcome: something may have been delivered. The claim is kept
42
+ * so a retry cannot double-send, and the operator is offered a delivery check
43
+ * rather than a resend.
44
+ */
45
+ export declare class AnswerUnknownError extends Error {
46
+ constructor(message: string);
47
+ }
48
+ /**
49
+ * delivered — this call handed the answer to a rail.
50
+ * already_answered — another claim owns it; the reported evidence is that claim's.
51
+ * unknown — something may have landed; do NOT resend, check delivery.
52
+ * failed — confirmed: nothing was delivered.
53
+ */
54
+ export type AnswerStatus = 'delivered' | 'already_answered' | 'unknown' | 'failed';
55
+ /**
56
+ * receipt — a real {@link MessageReceipt} exists on the block.
57
+ * unconfirmed — no receipt evidence either way. The card must NOT be cleared.
58
+ * failed — confirmed failure, including a `dropped`/`expired` receipt.
59
+ */
60
+ export type AnswerDelivery = 'receipt' | 'unconfirmed' | 'failed';
61
+ export interface FeedAnswerResult {
62
+ status: AnswerStatus;
63
+ /** How much the CLI can actually vouch for. */
64
+ delivery: AnswerDelivery;
65
+ /** The block's real receipt. Absent means no rail ever reported one. */
66
+ receipt?: MessageReceipt;
67
+ /**
68
+ * The AGENT's own evidence that it received the answer — a `consumed` or
69
+ * `continued` receipt. A `queued` receipt is delivery, never resolution, so it
70
+ * leaves this false. Removal of the item from the feed resolves the card on
71
+ * its own; this flag only ever adds evidence, it never withholds it.
72
+ */
73
+ resolved: boolean;
74
+ /** Human explanation for a failure, an unknown, or an unconfirmed delivery. */
75
+ reason?: string;
76
+ /** Set when `status` is `failed`. */
77
+ code?: AnswerFailureCode;
78
+ attentionKey: string;
79
+ blockId?: string;
80
+ /** The device that owns the item — the exact target a delivery check re-queries. */
81
+ host?: string;
82
+ /**
83
+ * Stable identity of the delivery attempt (the claim timestamp). A delivery
84
+ * check correlates its read-only answer with the attempt it is checking, so
85
+ * "still unconfirmed" is distinguishable from "a newer attempt replaced it".
86
+ */
87
+ attempt?: string;
88
+ }
3
89
  interface VerifiedOperator {
4
90
  id?: string;
5
91
  verified: boolean;
6
92
  label?: string;
7
93
  }
8
- interface FeedAnswerResult {
9
- status: 'delivered' | 'already_answered';
10
- receipt: MessageReceipt;
94
+ export interface ParsedAttentionKey {
95
+ host: string;
96
+ sessionId: string;
97
+ generation: string;
11
98
  }
99
+ /**
100
+ * Split `<host>/<session>/<generation>` (attention.ts `attentionKey`). Fails
101
+ * loud on a malformed key: the old `slice(0, indexOf('/'))` returned a truncated
102
+ * host for a key with no separator, which then routed the answer at random.
103
+ */
104
+ export declare function parseAttentionKey(key: string): ParsedAttentionKey;
105
+ /**
106
+ * Whether THIS machine owns the item. The exact local/remote choice: the key's
107
+ * host is compared through the same {@link normalizeHost} the machine id is
108
+ * minted with, so `Yosemite-M4.local` and `yosemite-m4` are one machine.
109
+ */
110
+ export declare function answerOwnerIsLocal(host: string, self?: string): boolean;
111
+ /**
112
+ * What a stored receipt proves. The lifecycle vocabulary is fixed by
113
+ * `MessageReceipt`, and each member means exactly one thing here:
114
+ * queued — a rail took the answer. Delivery, never resolution.
115
+ * consumed/continued— the agent itself acknowledged it. This resolves the item.
116
+ * dropped/expired — terminal failure; it is the newest truth for its message
117
+ * (the write rank puts it top precisely so it cannot be
118
+ * regressed), which is exactly why it must not read as success.
119
+ */
120
+ export declare function classifyReceipt(receipt: MessageReceipt): {
121
+ delivery: AnswerDelivery;
122
+ resolved: boolean;
123
+ };
124
+ /**
125
+ * Read-only reconciliation of an attention item's delivery state — what powers
126
+ * "Check delivery" on an unconfirmed answer. It NEVER claims, routes, adopts or
127
+ * resends; it reports the stored claim and the block's real receipt so an
128
+ * operator can tell "still unconfirmed" from "the agent has it".
129
+ */
130
+ export declare function checkAnswerDelivery(attentionKey: string, feedRoot?: string, expectedAttempt?: string): FeedAnswerResult;
12
131
  /** Atomically claim the first answer, then route it over the session's recorded reply rail. */
13
132
  export declare function claimAndRouteAttentionAnswer(input: {
14
133
  attentionKey: string;
@@ -18,13 +137,43 @@ export declare function claimAndRouteAttentionAnswer(input: {
18
137
  feedRoot?: string;
19
138
  mailboxRoot?: string;
20
139
  sessions?: ActiveSession[];
140
+ deadlineMs?: number;
21
141
  }): Promise<FeedAnswerResult>;
22
- /** Forward a fleet attention answer to the device that owns its scope. */
142
+ /**
143
+ * The remote `agents feed answer` argv for a forwarded answer. Exported so the
144
+ * exact tokens that cross the hop are asserted directly.
145
+ */
146
+ export declare function remoteAnswerArgv(input: {
147
+ attentionKey: string;
148
+ choiceId?: string;
149
+ text?: string;
150
+ operatorId?: string;
151
+ check?: boolean;
152
+ attempt?: string;
153
+ }): string[];
154
+ /**
155
+ * Forward a fleet attention answer to the device that owns its scope.
156
+ *
157
+ * Every token is POSIX-quoted with the canonical {@link shellQuote} before the
158
+ * remote login shell parses it, so a multiline answer or one carrying `$`, `"`,
159
+ * `` ` ``, `;` or a newline arrives byte-identical. The transport is the
160
+ * canonical bounded {@link sshExecAsync} — it disables ControlMaster whenever a
161
+ * timeout is set, so the bound actually tears the remote command down instead of
162
+ * orphaning it behind a control socket.
163
+ *
164
+ * A timeout is reported UNKNOWN: the remote may well have delivered before the
165
+ * link was cut, so the operator is offered a delivery check, never an implicit
166
+ * resend. The returned receipt is verified to be about the key that was asked
167
+ * for, so a mismatched or truncated remote reply cannot be trusted as one.
168
+ */
23
169
  export declare function forwardFeedAnswer(input: {
24
170
  host: string;
25
171
  attentionKey: string;
26
172
  choiceId?: string;
27
173
  text?: string;
28
174
  operatorId?: string;
175
+ check?: boolean;
176
+ attempt?: string;
177
+ timeoutMs?: number;
29
178
  }): Promise<FeedAnswerResult>;
30
179
  export {};