@phnx-labs/agents-cli 1.20.34 → 1.20.36

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 (291) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/LICENSE +185 -21
  3. package/README.md +13 -5
  4. package/dist/commands/audit.d.ts +14 -0
  5. package/dist/commands/audit.js +68 -0
  6. package/dist/commands/browser.js +82 -8
  7. package/dist/commands/check.d.ts +15 -0
  8. package/dist/commands/check.js +84 -0
  9. package/dist/commands/cloud.js +143 -3
  10. package/dist/commands/computer-actions.d.ts +15 -0
  11. package/dist/commands/computer-actions.js +120 -22
  12. package/dist/commands/computer.js +81 -0
  13. package/dist/commands/cost.js +17 -12
  14. package/dist/commands/daemon.js +4 -1
  15. package/dist/commands/doctor.d.ts +1 -0
  16. package/dist/commands/doctor.js +54 -97
  17. package/dist/commands/events.d.ts +16 -0
  18. package/dist/commands/events.js +173 -0
  19. package/dist/commands/exec.d.ts +19 -0
  20. package/dist/commands/exec.js +283 -12
  21. package/dist/commands/hosts.js +37 -29
  22. package/dist/commands/inspect.d.ts +2 -0
  23. package/dist/commands/inspect.js +62 -16
  24. package/dist/commands/lock.d.ts +12 -0
  25. package/dist/commands/lock.js +70 -0
  26. package/dist/commands/logs.d.ts +17 -0
  27. package/dist/commands/logs.js +139 -0
  28. package/dist/commands/message.d.ts +15 -0
  29. package/dist/commands/message.js +56 -0
  30. package/dist/commands/models.d.ts +3 -0
  31. package/dist/commands/models.js +27 -8
  32. package/dist/commands/permissions.js +9 -2
  33. package/dist/commands/repo.d.ts +34 -0
  34. package/dist/commands/repo.js +243 -65
  35. package/dist/commands/resource-view.d.ts +20 -0
  36. package/dist/commands/resource-view.js +90 -28
  37. package/dist/commands/routines.js +101 -5
  38. package/dist/commands/secrets-migrate.js +106 -57
  39. package/dist/commands/secrets.d.ts +31 -18
  40. package/dist/commands/secrets.js +196 -91
  41. package/dist/commands/serve.d.ts +10 -0
  42. package/dist/commands/serve.js +37 -0
  43. package/dist/commands/sessions-inject.d.ts +14 -0
  44. package/dist/commands/sessions-inject.js +111 -0
  45. package/dist/commands/sessions-picker.d.ts +2 -0
  46. package/dist/commands/sessions-picker.js +24 -3
  47. package/dist/commands/sessions-resume.d.ts +2 -0
  48. package/dist/commands/sessions-resume.js +221 -0
  49. package/dist/commands/sessions-tail.d.ts +10 -0
  50. package/dist/commands/sessions-tail.js +11 -0
  51. package/dist/commands/sessions.d.ts +143 -1
  52. package/dist/commands/sessions.js +628 -80
  53. package/dist/commands/setup.js +4 -2
  54. package/dist/commands/sync.d.ts +3 -1
  55. package/dist/commands/sync.js +156 -4
  56. package/dist/commands/teams.js +217 -0
  57. package/dist/commands/versions.js +2 -4
  58. package/dist/commands/view.d.ts +3 -0
  59. package/dist/commands/view.js +19 -8
  60. package/dist/commands/wallet.d.ts +6 -0
  61. package/dist/commands/wallet.js +22 -5
  62. package/dist/commands/watchdog.d.ts +18 -0
  63. package/dist/commands/watchdog.js +238 -0
  64. package/dist/index.js +82 -32
  65. package/dist/lib/agents.js +30 -9
  66. package/dist/lib/audit/log.d.ts +92 -0
  67. package/dist/lib/audit/log.js +177 -0
  68. package/dist/lib/auto-pull.js +2 -1
  69. package/dist/lib/browser/cdp.js +5 -1
  70. package/dist/lib/browser/chrome.d.ts +10 -0
  71. package/dist/lib/browser/chrome.js +18 -7
  72. package/dist/lib/browser/drivers/ssh.d.ts +8 -0
  73. package/dist/lib/browser/drivers/ssh.js +77 -16
  74. package/dist/lib/browser/har.d.ts +84 -0
  75. package/dist/lib/browser/har.js +77 -0
  76. package/dist/lib/browser/ipc.d.ts +3 -0
  77. package/dist/lib/browser/ipc.js +37 -12
  78. package/dist/lib/browser/profiles.d.ts +1 -1
  79. package/dist/lib/browser/profiles.js +8 -10
  80. package/dist/lib/browser/refs.d.ts +65 -0
  81. package/dist/lib/browser/refs.js +73 -1
  82. package/dist/lib/browser/runtime-state.js +1 -0
  83. package/dist/lib/browser/service.d.ts +38 -2
  84. package/dist/lib/browser/service.js +183 -23
  85. package/dist/lib/browser/types.d.ts +14 -1
  86. package/dist/lib/budget/live-cloud.d.ts +42 -0
  87. package/dist/lib/budget/live-cloud.js +79 -0
  88. package/dist/lib/budget/live-team.d.ts +31 -0
  89. package/dist/lib/budget/live-team.js +115 -0
  90. package/dist/lib/cloud/codex.js +4 -0
  91. package/dist/lib/cloud/rush.d.ts +12 -1
  92. package/dist/lib/cloud/rush.js +13 -3
  93. package/dist/lib/cloud/types.d.ts +9 -0
  94. package/dist/lib/computer/dispatch.d.ts +8 -0
  95. package/dist/lib/computer/dispatch.js +125 -0
  96. package/dist/lib/computer/loop.d.ts +62 -0
  97. package/dist/lib/computer/loop.js +98 -0
  98. package/dist/lib/computer/model.d.ts +44 -0
  99. package/dist/lib/computer/model.js +157 -0
  100. package/dist/lib/concurrency.d.ts +19 -0
  101. package/dist/lib/concurrency.js +33 -0
  102. package/dist/lib/daemon.d.ts +92 -0
  103. package/dist/lib/daemon.js +222 -18
  104. package/dist/lib/devices/connect.d.ts +3 -2
  105. package/dist/lib/devices/connect.js +5 -3
  106. package/dist/lib/devices/registry.d.ts +7 -0
  107. package/dist/lib/devices/registry.js +24 -0
  108. package/dist/lib/devices/tailscale.js +1 -1
  109. package/dist/lib/drift.d.ts +52 -0
  110. package/dist/lib/drift.js +112 -0
  111. package/dist/lib/events.d.ts +10 -3
  112. package/dist/lib/events.js +101 -24
  113. package/dist/lib/exec.d.ts +17 -0
  114. package/dist/lib/exec.js +100 -12
  115. package/dist/lib/git.d.ts +27 -0
  116. package/dist/lib/git.js +56 -1
  117. package/dist/lib/hooks/cache.d.ts +6 -0
  118. package/dist/lib/hooks/cache.js +54 -12
  119. package/dist/lib/hooks.d.ts +27 -0
  120. package/dist/lib/hooks.js +135 -10
  121. package/dist/lib/hosts/dispatch.d.ts +15 -0
  122. package/dist/lib/hosts/dispatch.js +45 -13
  123. package/dist/lib/hosts/logs.d.ts +16 -0
  124. package/dist/lib/hosts/logs.js +74 -0
  125. package/dist/lib/hosts/option.js +1 -1
  126. package/dist/lib/hosts/passthrough.js +3 -1
  127. package/dist/lib/hosts/progress.d.ts +66 -0
  128. package/dist/lib/hosts/progress.js +125 -17
  129. package/dist/lib/hosts/ready.d.ts +51 -7
  130. package/dist/lib/hosts/ready.js +98 -25
  131. package/dist/lib/hosts/reconcile.d.ts +53 -0
  132. package/dist/lib/hosts/reconcile.js +81 -0
  133. package/dist/lib/hosts/registry.d.ts +19 -2
  134. package/dist/lib/hosts/registry.js +58 -2
  135. package/dist/lib/hosts/remote-cmd.d.ts +62 -1
  136. package/dist/lib/hosts/remote-cmd.js +70 -1
  137. package/dist/lib/hosts/remote-os.d.ts +17 -0
  138. package/dist/lib/hosts/remote-os.js +30 -0
  139. package/dist/lib/hosts/session-index.d.ts +34 -0
  140. package/dist/lib/hosts/session-index.js +56 -0
  141. package/dist/lib/hosts/tasks.d.ts +22 -0
  142. package/dist/lib/hosts/tasks.js +29 -0
  143. package/dist/lib/lock.d.ts +93 -0
  144. package/dist/lib/lock.js +207 -0
  145. package/dist/lib/loop.js +16 -1
  146. package/dist/lib/machine-id.d.ts +21 -0
  147. package/dist/lib/machine-id.js +26 -0
  148. package/dist/lib/mailbox-target.d.ts +36 -0
  149. package/dist/lib/mailbox-target.js +45 -0
  150. package/dist/lib/mailbox.d.ts +47 -0
  151. package/dist/lib/mailbox.js +194 -0
  152. package/dist/lib/mcp.d.ts +5 -0
  153. package/dist/lib/mcp.js +24 -8
  154. package/dist/lib/migrate.d.ts +19 -0
  155. package/dist/lib/migrate.js +134 -26
  156. package/dist/lib/overdue.js +3 -0
  157. package/dist/lib/permissions.d.ts +4 -0
  158. package/dist/lib/permissions.js +35 -0
  159. package/dist/lib/picker.d.ts +26 -0
  160. package/dist/lib/picker.js +127 -0
  161. package/dist/lib/platform/exec.d.ts +46 -0
  162. package/dist/lib/platform/exec.js +74 -0
  163. package/dist/lib/platform/process.d.ts +31 -0
  164. package/dist/lib/platform/process.js +34 -1
  165. package/dist/lib/platform/winpath.js +2 -0
  166. package/dist/lib/plugin-marketplace.d.ts +30 -0
  167. package/dist/lib/plugin-marketplace.js +215 -2
  168. package/dist/lib/plugins.d.ts +5 -0
  169. package/dist/lib/plugins.js +61 -10
  170. package/dist/lib/profiles.d.ts +25 -0
  171. package/dist/lib/profiles.js +22 -6
  172. package/dist/lib/pty-client.js +2 -1
  173. package/dist/lib/rotate.d.ts +61 -0
  174. package/dist/lib/rotate.js +52 -0
  175. package/dist/lib/routines.d.ts +40 -2
  176. package/dist/lib/routines.js +66 -8
  177. package/dist/lib/runner.d.ts +11 -2
  178. package/dist/lib/runner.js +49 -7
  179. package/dist/lib/scheduler.js +6 -1
  180. package/dist/lib/secrets/bundles.d.ts +60 -4
  181. package/dist/lib/secrets/bundles.js +131 -12
  182. package/dist/lib/secrets/filestore.d.ts +3 -0
  183. package/dist/lib/secrets/filestore.js +42 -16
  184. package/dist/lib/secrets/index.d.ts +43 -2
  185. package/dist/lib/secrets/index.js +102 -3
  186. package/dist/lib/secrets/mcp.d.ts +93 -0
  187. package/dist/lib/secrets/mcp.js +205 -0
  188. package/dist/lib/secrets/remote.js +12 -5
  189. package/dist/lib/secrets/sync.js +83 -4
  190. package/dist/lib/secrets/windows.js +14 -3
  191. package/dist/lib/serve/data.d.ts +81 -0
  192. package/dist/lib/serve/data.js +91 -0
  193. package/dist/lib/serve/page.d.ts +7 -0
  194. package/dist/lib/serve/page.js +140 -0
  195. package/dist/lib/serve/server.d.ts +46 -0
  196. package/dist/lib/serve/server.js +115 -0
  197. package/dist/lib/session/active.d.ts +61 -0
  198. package/dist/lib/session/active.js +196 -17
  199. package/dist/lib/session/discover.d.ts +37 -0
  200. package/dist/lib/session/discover.js +204 -27
  201. package/dist/lib/session/inject.d.ts +18 -0
  202. package/dist/lib/session/inject.js +21 -0
  203. package/dist/lib/session/parse.d.ts +6 -0
  204. package/dist/lib/session/parse.js +286 -2
  205. package/dist/lib/session/pid-registry.d.ts +22 -0
  206. package/dist/lib/session/pid-registry.js +104 -0
  207. package/dist/lib/session/provenance.d.ts +14 -2
  208. package/dist/lib/session/provenance.js +39 -8
  209. package/dist/lib/session/remote-active.d.ts +26 -0
  210. package/dist/lib/session/remote-active.js +153 -0
  211. package/dist/lib/session/remote-list.d.ts +51 -0
  212. package/dist/lib/session/remote-list.js +213 -0
  213. package/dist/lib/session/remote.d.ts +7 -1
  214. package/dist/lib/session/remote.js +18 -8
  215. package/dist/lib/session/render.js +1 -1
  216. package/dist/lib/session/sync/config.d.ts +1 -7
  217. package/dist/lib/session/sync/config.js +4 -11
  218. package/dist/lib/session/types.d.ts +18 -1
  219. package/dist/lib/session/types.js +1 -1
  220. package/dist/lib/shims.d.ts +44 -6
  221. package/dist/lib/shims.js +143 -57
  222. package/dist/lib/ssh-exec.d.ts +26 -3
  223. package/dist/lib/ssh-exec.js +47 -3
  224. package/dist/lib/ssh-tunnel.d.ts +24 -5
  225. package/dist/lib/ssh-tunnel.js +62 -63
  226. package/dist/lib/staleness/writers/hooks.js +1 -1
  227. package/dist/lib/startup/command-registry.d.ts +8 -0
  228. package/dist/lib/startup/command-registry.js +17 -1
  229. package/dist/lib/state.d.ts +13 -0
  230. package/dist/lib/state.js +103 -9
  231. package/dist/lib/sync-umbrella.d.ts +14 -7
  232. package/dist/lib/sync-umbrella.js +17 -9
  233. package/dist/lib/teams/forEach.d.ts +110 -0
  234. package/dist/lib/teams/forEach.js +186 -0
  235. package/dist/lib/teams/index.d.ts +1 -0
  236. package/dist/lib/teams/index.js +1 -0
  237. package/dist/lib/teams/pr-watch.d.ts +226 -0
  238. package/dist/lib/teams/pr-watch.js +371 -0
  239. package/dist/lib/teams/registry.js +25 -9
  240. package/dist/lib/teams/supervisor.d.ts +14 -1
  241. package/dist/lib/teams/supervisor.js +19 -0
  242. package/dist/lib/teams/worktree.d.ts +9 -0
  243. package/dist/lib/teams/worktree.js +32 -0
  244. package/dist/lib/terminal/backends/ghostty.d.ts +6 -0
  245. package/dist/lib/terminal/backends/ghostty.js +69 -0
  246. package/dist/lib/terminal/backends/index.d.ts +19 -0
  247. package/dist/lib/terminal/backends/index.js +31 -0
  248. package/dist/lib/terminal/backends/iterm.d.ts +6 -0
  249. package/dist/lib/terminal/backends/iterm.js +62 -0
  250. package/dist/lib/terminal/backends/tmux.d.ts +14 -0
  251. package/dist/lib/terminal/backends/tmux.js +23 -0
  252. package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
  253. package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
  254. package/dist/lib/terminal/engine.d.ts +39 -0
  255. package/dist/lib/terminal/engine.js +54 -0
  256. package/dist/lib/terminal/index.d.ts +17 -0
  257. package/dist/lib/terminal/index.js +8 -0
  258. package/dist/lib/terminal/inject.d.ts +204 -0
  259. package/dist/lib/terminal/inject.js +247 -0
  260. package/dist/lib/terminal/policy.d.ts +11 -0
  261. package/dist/lib/terminal/policy.js +11 -0
  262. package/dist/lib/terminal/quote.d.ts +11 -0
  263. package/dist/lib/terminal/quote.js +13 -0
  264. package/dist/lib/terminal/resolve.d.ts +64 -0
  265. package/dist/lib/terminal/resolve.js +90 -0
  266. package/dist/lib/terminal/shell.d.ts +6 -0
  267. package/dist/lib/terminal/shell.js +23 -0
  268. package/dist/lib/terminal/transport.d.ts +15 -0
  269. package/dist/lib/terminal/transport.js +40 -0
  270. package/dist/lib/terminal/types.d.ts +59 -0
  271. package/dist/lib/terminal/types.js +13 -0
  272. package/dist/lib/triggers/webhook.d.ts +85 -0
  273. package/dist/lib/triggers/webhook.js +141 -0
  274. package/dist/lib/types.d.ts +7 -0
  275. package/dist/lib/versions.d.ts +30 -3
  276. package/dist/lib/versions.js +129 -18
  277. package/dist/lib/watchdog/index.d.ts +3 -0
  278. package/dist/lib/watchdog/index.js +5 -0
  279. package/dist/lib/watchdog/read.d.ts +35 -0
  280. package/dist/lib/watchdog/read.js +149 -0
  281. package/dist/lib/watchdog/runner.d.ts +127 -0
  282. package/dist/lib/watchdog/runner.js +322 -0
  283. package/dist/lib/watchdog/watchdog.d.ts +40 -0
  284. package/dist/lib/watchdog/watchdog.js +166 -0
  285. package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
  286. package/dist/lib/watchdog/watchdogTail.js +154 -0
  287. package/dist/lib/whats-new.d.ts +9 -0
  288. package/dist/lib/whats-new.js +35 -0
  289. package/dist/lib/workflows.d.ts +166 -0
  290. package/dist/lib/workflows.js +193 -0
  291. package/package.json +5 -4
@@ -77,7 +77,7 @@ export interface ConflictInfo {
77
77
  * top-level entry add/remove — deep edits to plugin contents won't
78
78
  * trigger auto-resync, run `agents sync` for that.
79
79
  */
80
- export declare const SHIM_SCHEMA_VERSION = 19;
80
+ export declare const SHIM_SCHEMA_VERSION = 22;
81
81
  /**
82
82
  * Generate the full bash shim script for the given agent. The returned string
83
83
  * is written to ~/.agents/shims/{cliCommand} and made executable.
@@ -126,8 +126,27 @@ export declare function removeShim(agent: AgentId): boolean;
126
126
  * hardcoded node_modules/.bin, which never exists for these three and
127
127
  * made every versioned alias (the path `agents teams` pins to) fail
128
128
  * with "<agent>@<version> not installed". Also emit GROK_HOME.
129
- */
130
- export declare const VERSIONED_ALIAS_SCHEMA_VERSION = 8;
129
+ * v9 — kimi was wrong in v8: it npm-installs @moonshot-ai/kimi-code into
130
+ * node_modules/.bin/kimi (grok/droid ship native binaries elsewhere,
131
+ * kimi does not). The ~/.kimi-code/bin path never existed for an npm
132
+ * install and the `command -v kimi` fallback resolved to this alias's
133
+ * sibling dispatcher shim, re-exec-looping forever. Resolve kimi via the
134
+ * generic node_modules/.bin branch.
135
+ * v10 — guard grok's `command -v grok` fallback against resolving to our own
136
+ * shims dir (same infinite re-exec loop), mirroring droid.
137
+ * v11 — export DISABLE_AUTOUPDATER=1 for claude aliases so a pinned per-version
138
+ * install can't self-mutate via Claude Code's background auto-updater.
139
+ * Explicit user value wins.
140
+ * v12 — Windows: stop writing the extensionless bash alias next to the `.cmd`
141
+ * (and delete a lingering one). The version suffix contains dots, so
142
+ * cmd.exe and PowerShell treat `claude@2.1.201` as a complete filename
143
+ * with extension `.201`, exact-match the bash script AHEAD of PATHEXT
144
+ * probing, and ShellExecute it to the `.sh` editor association — the
145
+ * editor opens the script and the agent never launches. The `.cmd` is
146
+ * now the only Windows artifact and carries this alias marker so
147
+ * staleness checks read it directly.
148
+ */
149
+ export declare const VERSIONED_ALIAS_SCHEMA_VERSION = 12;
131
150
  /**
132
151
  * Generate a versioned alias script that directly execs a specific version.
133
152
  * e.g., claude@2.0.65 -> directly runs that version's binary
@@ -149,20 +168,39 @@ export declare function isVersionedAliasCurrent(agent: AgentId, version: string)
149
168
  */
150
169
  export declare function ensureVersionedAliasCurrent(agent: AgentId, version: string): 'created' | 'updated' | 'current';
151
170
  /**
152
- * Get the filesystem path for a versioned alias script.
171
+ * Get the filesystem path for a versioned alias script — the logical
172
+ * (extensionless) launch name. On Windows this is not a real file (see
173
+ * versionedAliasOnDiskFile); stat/read checks must use the on-disk path.
153
174
  */
154
175
  export declare function getVersionedAliasPath(agent: AgentId, version: string): string;
176
+ /**
177
+ * The file createVersionedAlias actually materializes for a platform:
178
+ * `<cmd>@<version>.cmd` on Windows, the bare bash script on POSIX. Pure —
179
+ * testable on any host. Mirrors onDiskShimFile for the main shim.
180
+ */
181
+ export declare function versionedAliasOnDiskFile(cliCommand: string, version: string, platform: NodeJS.Platform): string;
155
182
  /**
156
183
  * Create a versioned alias for a specific agent version.
157
184
  * e.g., claude@2.0.65
185
+ *
186
+ * Same platform split as createShim (shimTargetsFor): POSIX writes the
187
+ * extensionless bash script; Windows writes ONLY the `.cmd`. Unlike the main
188
+ * shim — where the bash file was merely dead weight — a bash alias next to the
189
+ * versioned `.cmd` is actively harmful: the dotted version suffix makes
190
+ * cmd.exe/PowerShell treat `claude@2.1.201` as a complete filename (extension
191
+ * `.201`), so the exact match wins over PATHEXT probing and the shell
192
+ * ShellExecutes the bash script to the `.sh` editor association — the editor
193
+ * opens, the agent never launches. Any legacy bash alias is deleted here.
158
194
  */
159
195
  export declare function createVersionedAlias(agent: AgentId, version: string): string;
160
196
  /**
161
- * Remove a versioned alias for a specific agent version.
197
+ * Remove a versioned alias for a specific agent version. Removes whichever
198
+ * companions exist — the extensionless script (POSIX, or a legacy Windows
199
+ * install that wrote it) AND the `.cmd` (Windows) — mirroring removeShim.
162
200
  */
163
201
  export declare function removeVersionedAlias(agent: AgentId, version: string): boolean;
164
202
  /**
165
- * Check if a versioned alias exists.
203
+ * Check if a versioned alias exists (the on-disk artifact for this platform).
166
204
  */
167
205
  export declare function versionedAliasExists(agent: AgentId, version: string): boolean;
168
206
  /**
package/dist/lib/shims.js CHANGED
@@ -202,7 +202,16 @@ async function promptConflictStrategy(conflictInfos) {
202
202
  * top-level entry add/remove — deep edits to plugin contents won't
203
203
  * trigger auto-resync, run `agents sync` for that.
204
204
  */
205
- export const SHIM_SCHEMA_VERSION = 19;
205
+ // v20 stop treating kimi like grok/droid: it npm-installs into
206
+ // node_modules/.bin/kimi, so resolve it via the generic branch. The old
207
+ // ~/.kimi-code/bin special-case never existed for npm installs and
208
+ // re-exec-looped through `command -v kimi` (the dispatcher itself).
209
+ // v21 — guard grok's `command -v grok` fallback against resolving to our own
210
+ // shims dir (same infinite re-exec loop), mirroring droid.
211
+ // v22 — export DISABLE_AUTOUPDATER=1 for claude shims so a pinned per-version
212
+ // install can't self-mutate: Claude Code's background auto-updater would
213
+ // otherwise rewrite the pinned binary in place. Explicit user value wins.
214
+ export const SHIM_SCHEMA_VERSION = 22;
206
215
  /** Internal marker string used to embed the schema version in shim scripts. */
207
216
  const SHIM_VERSION_MARKER = 'agents-shim-version:';
208
217
  function shellQuote(value) {
@@ -230,6 +239,10 @@ export function generateShimScript(agent) {
230
239
  # selected version's config directory so switching versions also switches the
231
240
  # live Claude account.
232
241
  export CLAUDE_CONFIG_DIR="$VERSION_DIR/home/${configDirName}"
242
+ # Managed installs are pinned in a per-version dir; Claude Code's background
243
+ # auto-updater would rewrite the pinned binary in place. Disable it so a pin
244
+ # stays a pin. An explicit user value always wins.
245
+ export DISABLE_AUTOUPDATER="\${DISABLE_AUTOUPDATER:-1}"
233
246
  # On Linux sandboxes (no keychain), fall back to a per-version token file.
234
247
  # The env var always wins if already set; no-op on macOS.
235
248
  if [ "\$(uname -s)" = "Linux" ] && [ -z "\${CLAUDE_CODE_OAUTH_TOKEN:-}" ] && [ -f "\$CLAUDE_CONFIG_DIR/.oauth_token" ]; then
@@ -401,19 +414,25 @@ if [ "$AGENT" = "grok" ]; then
401
414
  fi
402
415
  fi
403
416
  if [ -z "$BINARY" ] || [ ! -x "$BINARY" ]; then
404
- # Last resort: whatever is on PATH (user may have installed grok globally)
417
+ # Last resort: whatever is on PATH (user may have installed grok globally).
418
+ # Refuse anything under our own shims dir: the shims dir sits ahead of
419
+ # ~/.local/bin on PATH, so "command -v grok" resolves to THIS dispatcher.
420
+ # exec-ing it would re-enter and spin in an infinite re-exec loop (the same
421
+ # bug the droid branch below guards against). Fall through to the clean
422
+ # "not installed" error instead.
405
423
  BINARY=$(command -v grok 2>/dev/null || echo "")
424
+ case "$BINARY" in
425
+ "$AGENTS_USER_DIR/.cache/shims/"*) BINARY="" ;;
426
+ esac
406
427
  fi
407
- # Kimi special case: binary lives in ~/.kimi-code/bin/, not node_modules.
408
- # We still use the agents-cli version dir purely for KIMI_CODE_HOME isolation.
409
- elif [ "$AGENT" = "kimi" ]; then
410
- KIMI_BINARY="$HOME/.kimi-code/bin/kimi"
411
- if [ -x "$KIMI_BINARY" ]; then
412
- BINARY="$KIMI_BINARY"
413
- else
414
- # Last resort: whatever is on PATH
415
- BINARY=$(command -v kimi 2>/dev/null || echo "")
416
- fi
428
+ # Kimi is a normal npm agent: "agents add kimi" npm-installs
429
+ # @moonshot-ai/kimi-code into the version dir and the binary lands at
430
+ # node_modules/.bin/kimi (a curl-installed kimi is symlinked to the same spot
431
+ # by installVersion). So kimi resolves via the generic node_modules branch
432
+ # below -- never a bespoke ~/.kimi-code/bin path that does not exist for npm
433
+ # installs and fell back to "command -v kimi", which resolves to THIS
434
+ # dispatcher (shims dir is ahead on PATH) and re-execs forever. Only
435
+ # KIMI_CODE_HOME (config isolation) stays special-cased, separately below.
417
436
  # Droid (Factory AI) special case: the official installer drops a standalone
418
437
  # native binary at ~/.local/bin/droid — there is no npm package and nothing
419
438
  # lands in node_modules/.bin. Resolve the fixed install path directly. The
@@ -568,12 +587,18 @@ export function createShim(agent) {
568
587
  * `cliCommand@version` for a versioned alias. node + the dist entrypoint are
569
588
  * resolved at generation time so the launcher does not depend on `agents`
570
589
  * already being on PATH.
590
+ *
591
+ * `extraMarkerLines` lets callers stamp additional schema markers into the
592
+ * header — versioned aliases embed their own alias-schema marker so
593
+ * readVersionedAliasSchemaVersion can stat the `.cmd` (the only Windows
594
+ * artifact) the same way it reads the bash script on POSIX.
571
595
  */
572
- function writeWindowsCmdShim(cmdPath, spec) {
596
+ function writeWindowsCmdShim(cmdPath, spec, extraMarkerLines = []) {
573
597
  const indexJs = getAgentsBinForGeneratedShim();
574
598
  const content = `@echo off\r\n` +
575
599
  `rem Auto-generated by agents-cli - do not edit\r\n` +
576
600
  `rem ${SHIM_VERSION_MARKER} ${SHIM_SCHEMA_VERSION}\r\n` +
601
+ extraMarkerLines.map((line) => `rem ${line}\r\n`).join('') +
577
602
  `node "${indexJs}" __shim ${spec} %*\r\n`;
578
603
  fs.writeFileSync(cmdPath, content);
579
604
  }
@@ -620,8 +645,27 @@ export function removeShim(agent) {
620
645
  * hardcoded node_modules/.bin, which never exists for these three and
621
646
  * made every versioned alias (the path `agents teams` pins to) fail
622
647
  * with "<agent>@<version> not installed". Also emit GROK_HOME.
648
+ * v9 — kimi was wrong in v8: it npm-installs @moonshot-ai/kimi-code into
649
+ * node_modules/.bin/kimi (grok/droid ship native binaries elsewhere,
650
+ * kimi does not). The ~/.kimi-code/bin path never existed for an npm
651
+ * install and the `command -v kimi` fallback resolved to this alias's
652
+ * sibling dispatcher shim, re-exec-looping forever. Resolve kimi via the
653
+ * generic node_modules/.bin branch.
654
+ * v10 — guard grok's `command -v grok` fallback against resolving to our own
655
+ * shims dir (same infinite re-exec loop), mirroring droid.
656
+ * v11 — export DISABLE_AUTOUPDATER=1 for claude aliases so a pinned per-version
657
+ * install can't self-mutate via Claude Code's background auto-updater.
658
+ * Explicit user value wins.
659
+ * v12 — Windows: stop writing the extensionless bash alias next to the `.cmd`
660
+ * (and delete a lingering one). The version suffix contains dots, so
661
+ * cmd.exe and PowerShell treat `claude@2.1.201` as a complete filename
662
+ * with extension `.201`, exact-match the bash script AHEAD of PATHEXT
663
+ * probing, and ShellExecute it to the `.sh` editor association — the
664
+ * editor opens the script and the agent never launches. The `.cmd` is
665
+ * now the only Windows artifact and carries this alias marker so
666
+ * staleness checks read it directly.
623
667
  */
624
- export const VERSIONED_ALIAS_SCHEMA_VERSION = 8;
668
+ export const VERSIONED_ALIAS_SCHEMA_VERSION = 12;
625
669
  /** Internal marker string used to embed the schema version in versioned alias scripts. */
626
670
  const VERSIONED_ALIAS_VERSION_MARKER = 'agents-versioned-alias-version:';
627
671
  // The version string is interpolated into a generated bash script and into
@@ -649,6 +693,10 @@ export function generateVersionedAliasScript(agent, version) {
649
693
  # Claude stores OAuth credentials in the macOS keychain. Scope them to this
650
694
  # version's config directory so direct aliases also switch the live account.
651
695
  export CLAUDE_CONFIG_DIR="$HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}"
696
+ # Managed installs are pinned in a per-version dir; Claude Code's background
697
+ # auto-updater would rewrite the pinned binary in place. Disable it so a pin
698
+ # stays a pin. An explicit user value always wins.
699
+ export DISABLE_AUTOUPDATER="\${DISABLE_AUTOUPDATER:-1}"
652
700
  `
653
701
  : agent === 'codex'
654
702
  ? `
@@ -680,12 +728,16 @@ export KIMI_CODE_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/
680
728
  : '';
681
729
  const launchArgs = agent === 'codex' ? ' -c check_for_update_on_startup=false' : '';
682
730
  // Resolve the binary the same way the main shim does (see generateShimScript).
683
- // Grok, Kimi, and Droid do NOT ship into node_modules/.bin — Grok downloads a
684
- // native binary to ~/.grok/downloads, Kimi to ~/.kimi-code/bin, and Droid
685
- // (Factory AI) installs a standalone binary to ~/.local/bin. Hardcoding the
686
- // node_modules path made every versioned alias for these three fail with
687
- // "<agent>@<version> not installed", which is exactly the path `agents teams`
688
- // takes once it pins a teammate's version.
731
+ // Grok and Droid do NOT ship into node_modules/.bin — Grok downloads a native
732
+ // binary to ~/.grok/downloads and Droid (Factory AI) installs a standalone
733
+ // binary to ~/.local/bin. Hardcoding the node_modules path made every
734
+ // versioned alias for those two fail with "<agent>@<version> not installed",
735
+ // which is exactly the path `agents teams` takes once it pins a teammate's
736
+ // version. Kimi is NOT one of them: `agents add kimi` npm-installs
737
+ // @moonshot-ai/kimi-code so its binary is at node_modules/.bin/kimi (the
738
+ // generic branch below). The old ~/.kimi-code/bin path never exists for an
739
+ // npm install and fell back to `command -v kimi`, which resolves to this
740
+ // alias's sibling dispatcher shim and re-execs forever.
689
741
  // This template is unix-only — on Windows the .cmd companion delegates to
690
742
  // "agents __shim" which resolves via getBinaryPath() instead.
691
743
  const versionDir = `$HOME/.agents/.history/versions/${agent}/${version}`;
@@ -697,17 +749,17 @@ if [ -d "$GROK_DOWNLOADS" ]; then
697
749
  BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | grep -i "${version}" | head -1)
698
750
  [ -n "$BINARY" ] || BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | head -1)
699
751
  fi
700
- [ -n "$BINARY" ] && [ -x "$BINARY" ] || BINARY=$(command -v grok 2>/dev/null || echo "")`
701
- : agent === 'kimi'
702
- ? `# Kimi ships its binary in ~/.kimi-code/bin, not node_modules.
703
- KIMI_BINARY="$HOME/.kimi-code/bin/kimi"
704
- if [ -x "$KIMI_BINARY" ]; then
705
- BINARY="$KIMI_BINARY"
706
- else
707
- BINARY=$(command -v kimi 2>/dev/null || echo "")
752
+ # Refuse a PATH match under our own shims dir it resolves to this alias's
753
+ # sibling dispatcher shim (shims dir is ahead of ~/.local/bin on PATH) and
754
+ # re-execs forever. Fall through to the clean "not installed" error instead.
755
+ if [ -z "$BINARY" ] || [ ! -x "$BINARY" ]; then
756
+ BINARY=$(command -v grok 2>/dev/null || echo "")
757
+ case "$BINARY" in
758
+ "$HOME/.agents/.cache/shims/"*) BINARY="" ;;
759
+ esac
708
760
  fi`
709
- : agent === 'droid'
710
- ? `# Droid (Factory AI) installs a standalone native binary at ~/.local/bin/droid;
761
+ : agent === 'droid'
762
+ ? `# Droid (Factory AI) installs a standalone native binary at ~/.local/bin/droid;
711
763
  # there is no npm package and nothing lands in node_modules/.bin. The PATH
712
764
  # fallback refuses anything under our shims dir to avoid an infinite re-exec.
713
765
  DROID_BINARY="$HOME/.local/bin/droid"
@@ -719,7 +771,7 @@ else
719
771
  "$HOME/.agents/.cache/shims/"*) BINARY="" ;;
720
772
  esac
721
773
  fi`
722
- : `BINARY="${versionDir}/node_modules/.bin/${agentConfig.cliCommand}"`;
774
+ : `BINARY="${versionDir}/node_modules/.bin/${agentConfig.cliCommand}"`;
723
775
  return `#!/bin/bash
724
776
  # Auto-generated by agents-cli - do not edit
725
777
  # ${VERSIONED_ALIAS_VERSION_MARKER} ${VERSIONED_ALIAS_SCHEMA_VERSION}
@@ -741,7 +793,7 @@ exec "$BINARY"${launchArgs} "$@"
741
793
  * alias doesn't exist or is a pre-v2 alias (no marker — treated as stale).
742
794
  */
743
795
  export function readVersionedAliasSchemaVersion(agent, version) {
744
- const aliasPath = getVersionedAliasPath(agent, version);
796
+ const aliasPath = versionedAliasOnDiskPath(agent, version);
745
797
  if (!fs.existsSync(aliasPath))
746
798
  return null;
747
799
  try {
@@ -768,11 +820,19 @@ export function isVersionedAliasCurrent(agent, version) {
768
820
  * was upgraded.
769
821
  */
770
822
  export function ensureVersionedAliasCurrent(agent, version) {
771
- const aliasPath = getVersionedAliasPath(agent, version);
772
- if (!fs.existsSync(aliasPath)) {
823
+ if (!fs.existsSync(versionedAliasOnDiskPath(agent, version))) {
773
824
  createVersionedAlias(agent, version);
774
825
  return 'created';
775
826
  }
827
+ // A lingering extensionless bash alias on Windows shadows the `.cmd` in both
828
+ // cmd.exe and PowerShell (the dotted version reads as a file extension, and
829
+ // an exact filename match beats PATHEXT probing), ShellExecuting the bash
830
+ // script to the `.sh` editor association instead of launching the agent.
831
+ // Regenerate regardless of the `.cmd`'s stamp so the shadow gets deleted.
832
+ if (shimTargetsFor(process.platform).cmd && fs.existsSync(getVersionedAliasPath(agent, version))) {
833
+ createVersionedAlias(agent, version);
834
+ return 'updated';
835
+ }
776
836
  // Upgrade-only (newest-wins), same rationale as ensureShimCurrent: never
777
837
  // downgrade an alias stamped by a newer install sharing the shims dir.
778
838
  const onDisk = readVersionedAliasSchemaVersion(agent, version);
@@ -783,14 +843,38 @@ export function ensureVersionedAliasCurrent(agent, version) {
783
843
  return 'current';
784
844
  }
785
845
  /**
786
- * Get the filesystem path for a versioned alias script.
846
+ * Get the filesystem path for a versioned alias script — the logical
847
+ * (extensionless) launch name. On Windows this is not a real file (see
848
+ * versionedAliasOnDiskFile); stat/read checks must use the on-disk path.
787
849
  */
788
850
  export function getVersionedAliasPath(agent, version) {
789
851
  return path.join(getShimsDir(), `${AGENTS[agent].cliCommand}@${version}`);
790
852
  }
853
+ /**
854
+ * The file createVersionedAlias actually materializes for a platform:
855
+ * `<cmd>@<version>.cmd` on Windows, the bare bash script on POSIX. Pure —
856
+ * testable on any host. Mirrors onDiskShimFile for the main shim.
857
+ */
858
+ export function versionedAliasOnDiskFile(cliCommand, version, platform) {
859
+ const name = `${cliCommand}@${version}`;
860
+ return shimTargetsFor(platform).cmd ? `${name}.cmd` : name;
861
+ }
862
+ /** The on-disk versioned-alias path for the current platform. */
863
+ function versionedAliasOnDiskPath(agent, version) {
864
+ return path.join(getShimsDir(), versionedAliasOnDiskFile(AGENTS[agent].cliCommand, version, process.platform));
865
+ }
791
866
  /**
792
867
  * Create a versioned alias for a specific agent version.
793
868
  * e.g., claude@2.0.65
869
+ *
870
+ * Same platform split as createShim (shimTargetsFor): POSIX writes the
871
+ * extensionless bash script; Windows writes ONLY the `.cmd`. Unlike the main
872
+ * shim — where the bash file was merely dead weight — a bash alias next to the
873
+ * versioned `.cmd` is actively harmful: the dotted version suffix makes
874
+ * cmd.exe/PowerShell treat `claude@2.1.201` as a complete filename (extension
875
+ * `.201`), so the exact match wins over PATHEXT probing and the shell
876
+ * ShellExecutes the bash script to the `.sh` editor association — the editor
877
+ * opens, the agent never launches. Any legacy bash alias is deleted here.
794
878
  */
795
879
  export function createVersionedAlias(agent, version) {
796
880
  assertSafeVersion(version);
@@ -798,40 +882,42 @@ export function createVersionedAlias(agent, version) {
798
882
  const shimsDir = getShimsDir();
799
883
  const agentConfig = AGENTS[agent];
800
884
  const aliasPath = path.join(shimsDir, `${agentConfig.cliCommand}@${version}`);
801
- const script = generateVersionedAliasScript(agent, version);
802
- fs.writeFileSync(aliasPath, script, { mode: 0o755 });
803
- if (IS_WINDOWS) {
804
- writeWindowsCmdShim(aliasPath + '.cmd', `${agentConfig.cliCommand}@${version}`);
885
+ const targets = shimTargetsFor(process.platform);
886
+ if (targets.bash) {
887
+ fs.writeFileSync(aliasPath, generateVersionedAliasScript(agent, version), { mode: 0o755 });
888
+ }
889
+ else {
890
+ try {
891
+ fs.unlinkSync(aliasPath);
892
+ }
893
+ catch { }
894
+ }
895
+ if (targets.cmd) {
896
+ writeWindowsCmdShim(aliasPath + '.cmd', `${agentConfig.cliCommand}@${version}`, [`${VERSIONED_ALIAS_VERSION_MARKER} ${VERSIONED_ALIAS_SCHEMA_VERSION}`]);
805
897
  }
806
898
  return aliasPath;
807
899
  }
808
900
  /**
809
- * Remove a versioned alias for a specific agent version.
901
+ * Remove a versioned alias for a specific agent version. Removes whichever
902
+ * companions exist — the extensionless script (POSIX, or a legacy Windows
903
+ * install that wrote it) AND the `.cmd` (Windows) — mirroring removeShim.
810
904
  */
811
905
  export function removeVersionedAlias(agent, version) {
812
- const shimsDir = getShimsDir();
813
- const agentConfig = AGENTS[agent];
814
- const aliasPath = path.join(shimsDir, `${agentConfig.cliCommand}@${version}`);
815
- if (fs.existsSync(aliasPath)) {
816
- fs.unlinkSync(aliasPath);
817
- if (IS_WINDOWS) {
818
- try {
819
- fs.unlinkSync(aliasPath + '.cmd');
820
- }
821
- catch { }
906
+ const aliasPath = getVersionedAliasPath(agent, version);
907
+ let removed = false;
908
+ for (const p of [aliasPath, aliasPath + '.cmd']) {
909
+ if (fs.existsSync(p)) {
910
+ fs.unlinkSync(p);
911
+ removed = true;
822
912
  }
823
- return true;
824
913
  }
825
- return false;
914
+ return removed;
826
915
  }
827
916
  /**
828
- * Check if a versioned alias exists.
917
+ * Check if a versioned alias exists (the on-disk artifact for this platform).
829
918
  */
830
919
  export function versionedAliasExists(agent, version) {
831
- const shimsDir = getShimsDir();
832
- const agentConfig = AGENTS[agent];
833
- const aliasPath = path.join(shimsDir, `${agentConfig.cliCommand}@${version}`);
834
- return fs.existsSync(aliasPath);
920
+ return fs.existsSync(versionedAliasOnDiskPath(agent, version));
835
921
  }
836
922
  /**
837
923
  * Get the path to the agent's config directory in HOME.
@@ -17,7 +17,16 @@ export declare const SSH_TARGET_RE: RegExp;
17
17
  export declare function assertValidSshTarget(host: string): void;
18
18
  /** POSIX single-quote a string for safe interpolation into a remote shell command. */
19
19
  export declare function shellQuote(s: string): string;
20
- /** Hardened ssh options applied to every connection. */
20
+ /**
21
+ * Hardened ssh options applied to every connection — the single baseline every
22
+ * `ssh` in the codebase composes from (directly here, or as `[...SSH_OPTS, …]`
23
+ * in the few callers that need extra flags like `-L`/`-N`/`ProxyCommand`).
24
+ *
25
+ * `ServerAliveInterval`/`ServerAliveCountMax` add in-connection keepalive: a
26
+ * silently-dropped link (laptop sleeps, Wi-Fi flips) is detected and the ssh
27
+ * process exits within ~45s instead of hanging forever — so a followed run or a
28
+ * long-lived `-N` tunnel can't leave a zombie ssh + socket pinned on the laptop.
29
+ */
21
30
  export declare const SSH_OPTS: readonly string[];
22
31
  export declare function controlOpts(): string[];
23
32
  export interface SshExecOptions {
@@ -27,7 +36,7 @@ export interface SshExecOptions {
27
36
  timeoutMs?: number;
28
37
  /** Extra ssh flags inserted before the target (e.g. `-tt`). */
29
38
  extraSshArgs?: string[];
30
- /** Reuse a persistent control socket across calls (see `controlOpts`). */
39
+ /** Reuse a persistent control socket across calls (default true; see `controlOpts`). */
31
40
  multiplex?: boolean;
32
41
  }
33
42
  export interface SshExecResult {
@@ -44,6 +53,20 @@ export interface SshExecResult {
44
53
  * it); callers that build it from user input must `shellQuote` the pieces.
45
54
  */
46
55
  export declare function sshExec(target: string, remoteCmd: string, opts?: SshExecOptions): SshExecResult;
56
+ export interface SshExecRawResult {
57
+ code: number | null;
58
+ stdout: Buffer;
59
+ stderr: Buffer;
60
+ timedOut: boolean;
61
+ }
62
+ /**
63
+ * Like {@link sshExec} but returns raw stdout/stderr Buffers — no UTF-8 decode.
64
+ *
65
+ * Use when byte-exactness matters, e.g. offset-tracked log tailing: a multibyte
66
+ * character split across a read boundary must stay raw bytes, not collapse to a
67
+ * U+FFFD replacement char (which would desync a byte offset from the wire).
68
+ */
69
+ export declare function sshExecRaw(target: string, remoteCmd: string, opts?: SshExecOptions): SshExecRawResult;
47
70
  /** True if `target` is reachable over ssh (a passwordless `true` succeeds quickly). */
48
71
  export declare function sshReachable(target: string, timeoutMs?: number): boolean;
49
72
  export interface SshStreamOptions {
@@ -54,7 +77,7 @@ export interface SshStreamOptions {
54
77
  * leave it off and forward a non-interactive invocation instead.
55
78
  */
56
79
  tty?: boolean;
57
- /** Reuse a persistent control socket across calls (see `controlOpts`). */
80
+ /** Reuse a persistent control socket across calls (default true; see `controlOpts`). */
58
81
  multiplex?: boolean;
59
82
  }
60
83
  /**
@@ -29,11 +29,22 @@ export function shellQuote(s) {
29
29
  return s;
30
30
  return "'" + s.replace(/'/g, "'\\''") + "'";
31
31
  }
32
- /** Hardened ssh options applied to every connection. */
32
+ /**
33
+ * Hardened ssh options applied to every connection — the single baseline every
34
+ * `ssh` in the codebase composes from (directly here, or as `[...SSH_OPTS, …]`
35
+ * in the few callers that need extra flags like `-L`/`-N`/`ProxyCommand`).
36
+ *
37
+ * `ServerAliveInterval`/`ServerAliveCountMax` add in-connection keepalive: a
38
+ * silently-dropped link (laptop sleeps, Wi-Fi flips) is detected and the ssh
39
+ * process exits within ~45s instead of hanging forever — so a followed run or a
40
+ * long-lived `-N` tunnel can't leave a zombie ssh + socket pinned on the laptop.
41
+ */
33
42
  export const SSH_OPTS = [
34
43
  '-o', 'StrictHostKeyChecking=accept-new',
35
44
  '-o', 'BatchMode=yes',
36
45
  '-o', 'ConnectTimeout=10',
46
+ '-o', 'ServerAliveInterval=15',
47
+ '-o', 'ServerAliveCountMax=3',
37
48
  ];
38
49
  /**
39
50
  * OpenSSH connection-multiplexing options. The first connection to a host opens
@@ -44,6 +55,12 @@ export const SSH_OPTS = [
44
55
  * client exits. `%C` (a short fixed-length hash of local-host/remote/port/user)
45
56
  * keeps the socket path well under macOS's 104-char `sun_path` limit.
46
57
  *
58
+ * This is **on by default** for every `sshExec`/`sshStream` call: the poll loops
59
+ * (`followHostTask`), readiness probes, and per-host fan-outs are exactly the
60
+ * high-frequency callers that benefit most from socket reuse, and they should
61
+ * never have to remember to opt in. A caller passes `multiplex: false` only for
62
+ * a genuine one-shot where a lingering 60s master is pure overhead.
63
+ *
47
64
  * The socket directory is created lazily; if ssh can't open the control socket
48
65
  * it falls back to a normal connection (multiplexing is an optimisation, never a
49
66
  * requirement), so this can never make a reachable host unreachable.
@@ -79,13 +96,14 @@ export function controlOpts() {
79
96
  */
80
97
  export function sshExec(target, remoteCmd, opts = {}) {
81
98
  assertValidSshTarget(target);
82
- const mux = opts.multiplex ? controlOpts() : [];
99
+ const mux = opts.multiplex === false ? [] : controlOpts();
83
100
  const args = [...SSH_OPTS, ...mux, ...(opts.extraSshArgs ?? []), target, remoteCmd];
84
101
  const res = spawnSync('ssh', args, {
85
102
  input: opts.input,
86
103
  encoding: 'utf-8',
87
104
  timeout: opts.timeoutMs,
88
105
  stdio: ['pipe', 'pipe', 'pipe'],
106
+ windowsHide: true,
89
107
  });
90
108
  const timedOut = !!(res.error && res.error.code === 'ETIMEDOUT');
91
109
  return {
@@ -95,6 +113,32 @@ export function sshExec(target, remoteCmd, opts = {}) {
95
113
  timedOut,
96
114
  };
97
115
  }
116
+ /**
117
+ * Like {@link sshExec} but returns raw stdout/stderr Buffers — no UTF-8 decode.
118
+ *
119
+ * Use when byte-exactness matters, e.g. offset-tracked log tailing: a multibyte
120
+ * character split across a read boundary must stay raw bytes, not collapse to a
121
+ * U+FFFD replacement char (which would desync a byte offset from the wire).
122
+ */
123
+ export function sshExecRaw(target, remoteCmd, opts = {}) {
124
+ assertValidSshTarget(target);
125
+ const mux = opts.multiplex === false ? [] : controlOpts();
126
+ const args = [...SSH_OPTS, ...mux, ...(opts.extraSshArgs ?? []), target, remoteCmd];
127
+ const res = spawnSync('ssh', args, {
128
+ input: opts.input,
129
+ // No `encoding` → spawnSync returns Buffers.
130
+ timeout: opts.timeoutMs,
131
+ stdio: ['pipe', 'pipe', 'pipe'],
132
+ windowsHide: true,
133
+ });
134
+ const timedOut = !!(res.error && res.error.code === 'ETIMEDOUT');
135
+ return {
136
+ code: typeof res.status === 'number' ? res.status : null,
137
+ stdout: res.stdout ?? Buffer.alloc(0),
138
+ stderr: res.stderr ?? Buffer.alloc(0),
139
+ timedOut,
140
+ };
141
+ }
98
142
  /** True if `target` is reachable over ssh (a passwordless `true` succeeds quickly). */
99
143
  export function sshReachable(target, timeoutMs = 10000) {
100
144
  return sshExec(target, 'true', { timeoutMs, multiplex: true }).code === 0;
@@ -108,7 +152,7 @@ export function sshReachable(target, timeoutMs = 10000) {
108
152
  */
109
153
  export function sshStream(target, remoteCmd, opts = {}) {
110
154
  assertValidSshTarget(target);
111
- const mux = opts.multiplex ? controlOpts() : [];
155
+ const mux = opts.multiplex === false ? [] : controlOpts();
112
156
  const tty = opts.tty ? ['-tt'] : [];
113
157
  const args = [...SSH_OPTS, ...mux, ...tty, target, remoteCmd];
114
158
  const res = spawnSync('ssh', args, { stdio: 'inherit' });
@@ -28,7 +28,11 @@ export interface StartTunnelOptions {
28
28
  */
29
29
  detached?: boolean;
30
30
  }
31
- /** Build the ssh argv (after the `ssh` program name) for an `-L` tunnel. Pure. */
31
+ /** Build the ssh argv (after the `ssh` program name) for an `-L` tunnel. Pure.
32
+ *
33
+ * Composes the shared hardened baseline (`SSH_OPTS`) rather than re-listing it,
34
+ * so the tunnel inherits the same options — crucially the keepalive, which lets
35
+ * a dropped `-N` tunnel exit instead of lingering as a zombie on the laptop. */
32
36
  export declare function buildTunnelArgs(user: string, host: string, localPort: number, remotePort: number): string[];
33
37
  /**
34
38
  * Spawn `ssh -L localPort:127.0.0.1:remotePort -N user@host`.
@@ -74,12 +78,13 @@ export declare function resolveRemoteDevice(name: string): Promise<{
74
78
  host: string;
75
79
  }>;
76
80
  /**
77
- * PowerShell that streams base64 from stdin, decodes it incrementally to
78
- * %LOCALAPPDATA%\agents\computer-helper-win.exe, and stops any running instance
79
- * first so the file isn't locked. The CryptoStream/FromBase64Transform decode
80
- * is streaming — the ~156MB exe never lands in memory whole on the remote.
81
+ * PowerShell that resolves the destination under %LOCALAPPDATA%\agents and
82
+ * stops any running instance first so the file is not locked. The caller copies
83
+ * the exe with scp and then verifies the byte count separately.
81
84
  */
82
85
  export declare function buildPushScript(): string;
86
+ /** PowerShell that verifies scp wrote the expected number of bytes. */
87
+ export declare function buildVerifyPushScript(remotePath: string, expectedBytes: number): string;
83
88
  /**
84
89
  * PowerShell that registers the daemon as a LOGON scheduled task. Interactive
85
90
  * logon type + Highest run level so the daemon runs in the real desktop session
@@ -90,6 +95,20 @@ export declare function buildPushScript(): string;
90
95
  export declare function buildRegisterTaskScript(port: number, taskName: string): string;
91
96
  /** PowerShell that unregisters the task and stops any running daemon process. */
92
97
  export declare function buildUnregisterTaskScript(taskName: string): string;
98
+ /** Convert a Windows path returned by PowerShell into the scp/SFTP path form. */
99
+ export declare function scpRemotePath(remotePath: string): string;
100
+ /**
101
+ * Build the scp argv used for the helper exe transfer. Exported so tests can
102
+ * assert the real binary copy path keeps BatchMode and does not route bytes
103
+ * through a PowerShell decoder.
104
+ */
105
+ export declare function buildScpArgs(target: string, remotePath: string, filePath: string): string[];
106
+ /**
107
+ * `setup --host`: push the exe, then register + start the LOGON task. Remote
108
+ * PowerShell hops go through `sshExec` (BatchMode key auth — the same hardening
109
+ * the browser driver and `agents ssh` use), and the large exe rides a binary
110
+ * scp transfer. Throws with the remote stderr on any failure.
111
+ */
93
112
  export declare function setupRemoteHelper(name: string): Promise<{
94
113
  target: string;
95
114
  taskName: string;