@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
@@ -112,6 +112,12 @@ interface WindowsAgentsCommand {
112
112
  env?: Record<string, string>;
113
113
  /** Directory to enter before running (`--remote-cwd`). */
114
114
  cwd?: string;
115
+ /**
116
+ * `cwd` was DERIVED from the local cwd rather than named by the user (see
117
+ * `deriveMirroredCwd`): a directory the peer does not have falls back to
118
+ * `$HOME` instead of failing the run, exactly as `remoteCdPrefix({ mirror })`.
119
+ */
120
+ mirrorCwd?: boolean;
115
121
  /**
116
122
  * Append `exit $LASTEXITCODE` so a native `agents` exit code propagates out
117
123
  * through `powershell.exe` (which otherwise exits 0 regardless). Default true;
@@ -158,12 +164,77 @@ export declare const POWERSHELL_PROGRESS_SILENCE = "$ProgressPreference = 'Silen
158
164
  * every remote-JSON boundary regardless of the peer's OS.
159
165
  */
160
166
  export declare function stripClixml(stdout: string): string;
167
+ /**
168
+ * Statements that run the Agents CLI on a Windows peer with EXACT argv, leaving the
169
+ * child's exit code in `$zq`.
170
+ *
171
+ * `& agents …` cannot be used, and the reason is not our quoting. On Windows
172
+ * `agents` is an npm-generated `agents.ps1`, whose body ends in
173
+ *
174
+ * & "node$exe" --no-warnings=… "$basedir/node_modules/@phnx-labs/agents-cli/dist/index.js" $args
175
+ *
176
+ * — `$args` splatted into a NATIVE program, which is the PowerShell 5.1 lossy
177
+ * serializer. So the loss happens INSIDE the user's shim, after our tokens were
178
+ * already correct: measured on a real peer, `no-such-"menu"-proof` reached the
179
+ * Agents parser as `no-such-menu-proof`. Quoting harder upstream cannot fix that,
180
+ * and rewriting a user's npm shim is not ours to do.
181
+ *
182
+ * Instead the shim is bypassed the same way `getCliLaunch` (`lib/cli-entry.ts`)
183
+ * resolves a launch locally: a node-script entry becomes `<node> <entry> …args`.
184
+ * The entry and runtime are resolved ON THE PEER from the launcher's own location —
185
+ * the identical two-step the shim itself performs — and executed through .NET with
186
+ * a `CommandLineToArgvW`-escaped argument string.
187
+ *
188
+ * A peer whose `agents` is already a native executable is used directly. A peer
189
+ * where neither resolves FAILS LOUD: falling back to `& agents` would silently
190
+ * restore the argument loss, which is worse than an error naming what is missing.
191
+ *
192
+ * The entry comes from the package's DECLARED `bin` rather than a literal
193
+ * `dist/index.js`, so it follows the package instead of needing to be kept in sync
194
+ * with it.
195
+ */
196
+ export declare function windowsAgentsInvocation(args: string[], binName?: 'agents' | 'ag'): string;
197
+ /**
198
+ * PowerShell expression for a remote path. A home-anchored path (`~/x`,
199
+ * `$HOME/x`) re-roots onto the peer's `$HOME` through `Join-Path` — the Windows
200
+ * counterpart of `remoteCdPrefix`'s `"$HOME"/x` — so a directory mirrored from
201
+ * another box resolves on the peer. Any other path is quoted verbatim.
202
+ */
203
+ export declare function windowsRemotePath(p: string): string;
204
+ /**
205
+ * The `Set-Location` step of a Windows remote command. `-ErrorAction Stop` is
206
+ * spelled out because a missing directory is a NON-terminating error for
207
+ * `Set-Location`, which a `try`/`catch` would otherwise sail past: the mirrored
208
+ * form must genuinely land in `$HOME` when the peer lacks the checkout, and the
209
+ * explicit form must abort rather than run the command in the wrong directory.
210
+ */
211
+ export declare function windowsSetLocation(cwd: string, mirror?: boolean): string;
161
212
  export declare function windowsAgentsScript(cmd: WindowsAgentsCommand): string;
162
213
  /**
163
214
  * Build the `ssh <target> <cmd>` string for one `agents …` invocation on a
164
- * Windows remote: a `powershell -NoProfile -EncodedCommand <base64>` call. The
215
+ * Windows remote: a rendered `powershell -NoProfile` call. The
165
216
  * Windows counterpart of `bash -lc '<...>'`, shared by every `--device` site.
166
217
  */
218
+ /**
219
+ * Render a PowerShell script as the single command string ssh sends to a Windows
220
+ * peer, compressing it when that is genuinely shorter.
221
+ *
222
+ * The plain route is `-EncodedCommand <base64 of UTF-16LE>`, which inflates the
223
+ * script by ~2.67x. That matters because OpenSSH-for-Windows caps the whole remote
224
+ * command far below cmd.exe's 8191 — bisected against a live peer, 2934 characters
225
+ * succeed and 3102 fail — so a longer script eats the headroom a caller's
226
+ * arguments, `--remote-cwd` and forwarded env need.
227
+ *
228
+ * The alternative is to deflate the UTF-8 script and emit a small fixed bootstrap
229
+ * that inflates it back. That trades ~2.67x for ~1.33x on the compressible part,
230
+ * which on scripts of this shape is a large net win. The variable-free bootstrap
231
+ * uses `-Command` with a base64 payload and leaves stdin available to the child.
232
+ *
233
+ * Applied ONLY when the result is actually shorter: for a small script the fixed
234
+ * bootstrap costs more than it saves, so picking the shorter of the two keeps the
235
+ * plain form in play for every small caller and is obviously correct either way.
236
+ */
237
+ export declare function renderPowershellCommand(script: string): string;
167
238
  export declare function buildWindowsAgentsCommand(cmd: WindowsAgentsCommand): string;
168
239
  /**
169
240
  * Build the `ssh <target> <cmd>` string for `agents secrets import` on a Windows
@@ -8,6 +8,10 @@
8
8
  * (`sshExec`/`sshStream`); orchestration lives in `passthrough.ts`.
9
9
  */
10
10
  import { shellQuote } from '../ssh-exec.js';
11
+ import { pwshLiteral, pwshNativeExecStatements } from '../pwsh.js';
12
+ import { quoteWin32ExecArg } from '../platform/exec.js';
13
+ import { homeRemainder } from '../project-root.js';
14
+ import * as zlib from 'node:zlib';
11
15
  /**
12
16
  * Remove routing flags (and their values) from a command's args, leaving the
13
17
  * rest untouched and in order so they forward verbatim to the remote binary.
@@ -136,8 +140,6 @@ export const RUN_OPTION_FORWARDING = {
136
140
  // On a --device dispatch the remote box runs its own run-exit sync, so this
137
141
  // flag is never forwarded — it is a local-exit-behavior toggle, not remote.
138
142
  traceSync: 'local-only',
139
- // Deprecated alias for --device auto; resolved on the launching box before SSH.
140
- smart: 'local-only',
141
143
  // Broadcast mode (agents run --broadcast) is its own fan-out dispatch — mutually
142
144
  // exclusive with --host. All broadcast options are local-only; exec.ts handles them
143
145
  // before any SSH dispatch path is reached.
@@ -293,30 +295,169 @@ export function stripClixml(stdout) {
293
295
  .replace(/#< CLIXML[^\n]*\r?\n?(?:\s*<Objs\b[\s\S]*?<\/Objs>)*/g, '')
294
296
  .trim();
295
297
  }
298
+ /**
299
+ * Statements that run the Agents CLI on a Windows peer with EXACT argv, leaving the
300
+ * child's exit code in `$zq`.
301
+ *
302
+ * `& agents …` cannot be used, and the reason is not our quoting. On Windows
303
+ * `agents` is an npm-generated `agents.ps1`, whose body ends in
304
+ *
305
+ * & "node$exe" --no-warnings=… "$basedir/node_modules/@phnx-labs/agents-cli/dist/index.js" $args
306
+ *
307
+ * — `$args` splatted into a NATIVE program, which is the PowerShell 5.1 lossy
308
+ * serializer. So the loss happens INSIDE the user's shim, after our tokens were
309
+ * already correct: measured on a real peer, `no-such-"menu"-proof` reached the
310
+ * Agents parser as `no-such-menu-proof`. Quoting harder upstream cannot fix that,
311
+ * and rewriting a user's npm shim is not ours to do.
312
+ *
313
+ * Instead the shim is bypassed the same way `getCliLaunch` (`lib/cli-entry.ts`)
314
+ * resolves a launch locally: a node-script entry becomes `<node> <entry> …args`.
315
+ * The entry and runtime are resolved ON THE PEER from the launcher's own location —
316
+ * the identical two-step the shim itself performs — and executed through .NET with
317
+ * a `CommandLineToArgvW`-escaped argument string.
318
+ *
319
+ * A peer whose `agents` is already a native executable is used directly. A peer
320
+ * where neither resolves FAILS LOUD: falling back to `& agents` would silently
321
+ * restore the argument loss, which is worse than an error naming what is missing.
322
+ *
323
+ * The entry comes from the package's DECLARED `bin` rather than a literal
324
+ * `dist/index.js`, so it follows the package instead of needing to be kept in sync
325
+ * with it.
326
+ */
327
+ export function windowsAgentsInvocation(args, binName = 'agents') {
328
+ const escaped = pwshLiteral(args.map(quoteWin32ExecArg).join(' '));
329
+ return [
330
+ // Set before any resolution: a non-terminating failure below would otherwise
331
+ // leave the exit code null, and `exit $null` reports 0.
332
+ `$ErrorActionPreference = 'Stop'`,
333
+ `$zc = Get-Command ${powershellQuote(binName)} -ErrorAction Stop`,
334
+ `$ze = $zc.Source`,
335
+ `$zr = ''`,
336
+ // A real native executable needs no interpreter prefix. `CommandType` alone is
337
+ // NOT that test: `Get-Command` reports `Application` for `agents.cmd` too, and
338
+ // a .cmd launcher re-parses through cmd.exe with the same argument loss as the
339
+ // .ps1 one — so a cmd-only install must not bypass this either.
340
+ `if ($zc.CommandType -ne 'Application' -or $ze -match '\\.(cmd|bat)$') {`,
341
+ ` $zb = Split-Path $ze`,
342
+ ` $zk = Join-Path $zb 'node_modules\\@phnx-labs\\agents-cli'`,
343
+ // The package's DECLARED bin, so the entry follows the package rather than a
344
+ // hand-synced `dist/index.js` literal that would rot on upgrade. One check
345
+ // covers a missing bin AND the single-string `bin` form (which yields $null).
346
+ ` $zl = (Get-Content -Raw (Join-Path $zk 'package.json') | ConvertFrom-Json).bin.${binName}`,
347
+ ` if (-not $zl) { throw "agents package at $zk declares no bin.${binName}" }`,
348
+ ` $zt = Join-Path $zk $zl`,
349
+ ` if (-not [IO.File]::Exists($zt)) { throw "agents CLI entry not found at $zt" }`,
350
+ // The npm shim prefers a node.exe beside itself before PATH; match that.
351
+ ` $zd = Join-Path $zb 'node.exe'`,
352
+ ` $ze = if ([IO.File]::Exists($zd)) { $zd } else { 'node' }`,
353
+ // A Windows path cannot contain `"`, so wrapping is sufficient escaping here.
354
+ ` $zr = '--no-warnings=ExperimentalWarning "' + $zt + '" '`,
355
+ `}`,
356
+ ...pwshNativeExecStatements('$ze', `$zr + ${escaped}`),
357
+ // Reported through a plain variable rather than by assigning the automatic
358
+ // $LASTEXITCODE, which nothing set here since no PowerShell command ran.
359
+ `$zq = $zp.ExitCode`,
360
+ // Never let an unknown outcome read as success.
361
+ `if ($null -eq $zq) { $zq = 1 }`,
362
+ // NEWLINE-joined, and that is required rather than stylistic: the caller joins
363
+ // its parts with `'; '`, which between a closing `}` and `else`/an indented block
364
+ // would produce invalid PowerShell.
365
+ ].join('\n');
366
+ }
367
+ /**
368
+ * PowerShell expression for a remote path. A home-anchored path (`~/x`,
369
+ * `$HOME/x`) re-roots onto the peer's `$HOME` through `Join-Path` — the Windows
370
+ * counterpart of `remoteCdPrefix`'s `"$HOME"/x` — so a directory mirrored from
371
+ * another box resolves on the peer. Any other path is quoted verbatim.
372
+ */
373
+ export function windowsRemotePath(p) {
374
+ const rest = homeRemainder(p);
375
+ if (rest === null)
376
+ return powershellQuote(p);
377
+ return rest === '' ? '$HOME' : `(Join-Path $HOME ${powershellQuote(rest)})`;
378
+ }
379
+ /**
380
+ * The `Set-Location` step of a Windows remote command. `-ErrorAction Stop` is
381
+ * spelled out because a missing directory is a NON-terminating error for
382
+ * `Set-Location`, which a `try`/`catch` would otherwise sail past: the mirrored
383
+ * form must genuinely land in `$HOME` when the peer lacks the checkout, and the
384
+ * explicit form must abort rather than run the command in the wrong directory.
385
+ */
386
+ export function windowsSetLocation(cwd, mirror = false) {
387
+ const enter = `Set-Location -LiteralPath ${windowsRemotePath(cwd)} -ErrorAction Stop`;
388
+ return mirror ? `try { ${enter} } catch { Set-Location -LiteralPath $HOME }` : enter;
389
+ }
296
390
  export function windowsAgentsScript(cmd) {
297
- const { args, env, cwd, propagateExit = true, remapExit255 = false } = cmd;
298
- const parts = [POWERSHELL_PROGRESS_SILENCE];
391
+ const { args, env, cwd, mirrorCwd = false, propagateExit = true, remapExit255 = false } = cmd;
392
+ // `Stop` FIRST, before the env assignments and the Set-Location: a failing
393
+ // `Set-Location` (a cwd that does not exist on the peer) must abort rather than
394
+ // continue into the launcher and run the command in the wrong directory.
395
+ const parts = [POWERSHELL_PROGRESS_SILENCE, `$ErrorActionPreference = 'Stop'`];
299
396
  if (env)
300
397
  for (const [k, v] of Object.entries(env))
301
398
  parts.push(`$env:${k} = ${powershellQuote(v)}`);
302
399
  if (cwd)
303
- parts.push(`Set-Location -LiteralPath ${powershellQuote(cwd)}`);
304
- parts.push(`& ${['agents', ...args].map(powershellQuote).join(' ')}`);
400
+ parts.push(windowsSetLocation(cwd, mirrorCwd));
401
+ parts.push(windowsAgentsInvocation(args));
305
402
  if (propagateExit) {
306
403
  if (remapExit255)
307
- parts.push('$agentsExit = $LASTEXITCODE', 'if ($agentsExit -eq 255) { exit 254 }', 'exit $agentsExit');
404
+ parts.push('if ($zq -eq 255) { exit 254 }', 'exit $zq');
308
405
  else
309
- parts.push('exit $LASTEXITCODE');
406
+ parts.push('exit $zq');
310
407
  }
311
408
  return parts.join('; ');
312
409
  }
313
410
  /**
314
411
  * Build the `ssh <target> <cmd>` string for one `agents …` invocation on a
315
- * Windows remote: a `powershell -NoProfile -EncodedCommand <base64>` call. The
412
+ * Windows remote: a rendered `powershell -NoProfile` call. The
316
413
  * Windows counterpart of `bash -lc '<...>'`, shared by every `--device` site.
317
414
  */
415
+ /**
416
+ * Render a PowerShell script as the single command string ssh sends to a Windows
417
+ * peer, compressing it when that is genuinely shorter.
418
+ *
419
+ * The plain route is `-EncodedCommand <base64 of UTF-16LE>`, which inflates the
420
+ * script by ~2.67x. That matters because OpenSSH-for-Windows caps the whole remote
421
+ * command far below cmd.exe's 8191 — bisected against a live peer, 2934 characters
422
+ * succeed and 3102 fail — so a longer script eats the headroom a caller's
423
+ * arguments, `--remote-cwd` and forwarded env need.
424
+ *
425
+ * The alternative is to deflate the UTF-8 script and emit a small fixed bootstrap
426
+ * that inflates it back. That trades ~2.67x for ~1.33x on the compressible part,
427
+ * which on scripts of this shape is a large net win. The variable-free bootstrap
428
+ * uses `-Command` with a base64 payload and leaves stdin available to the child.
429
+ *
430
+ * Applied ONLY when the result is actually shorter: for a small script the fixed
431
+ * bootstrap costs more than it saves, so picking the shorter of the two keeps the
432
+ * plain form in play for every small caller and is obviously correct either way.
433
+ */
434
+ export function renderPowershellCommand(script) {
435
+ const plain = `powershell -NoProfile -EncodedCommand ${encodePowershell(script)}`;
436
+ // Raw DEFLATE (RFC 1951) — what .NET's `DeflateStream` reads. `deflateSync`
437
+ // would prepend a zlib header that `DeflateStream` rejects.
438
+ const packed = zlib.deflateRawSync(Buffer.from(script, 'utf-8'), { level: 9 }).toString('base64');
439
+ // The blob is embedded DIRECTLY in a `-Command` string rather than wrapped in a
440
+ // second `-EncodedCommand`: routing an already-base64 payload through the encoded
441
+ // form would inflate it by another 2.67x and cancel most of the compression.
442
+ //
443
+ // The bootstrap is deliberately VARIABLE-FREE — one nested expression, no `$`
444
+ // anywhere. A `$b = …` form is unsafe here: OpenSSH-for-Windows may have
445
+ // PowerShell as its `DefaultShell`, in which case the outer double-quoted string
446
+ // is parsed by PowerShell first and `$b` would be expanded before our script ever
447
+ // runs. With no `$`, no `%`, no backtick and no cmd metacharacter — base64's
448
+ // alphabet is `A-Za-z0-9+/=` and the rest is ASCII punctuation neither shell
449
+ // touches inside quotes — the same text survives either default shell. stdin is
450
+ // untouched by both routes.
451
+ const bootstrap = 'iex ([IO.StreamReader]::new([IO.Compression.DeflateStream]::new('
452
+ + `[IO.MemoryStream]::new([Convert]::FromBase64String('${packed}')),`
453
+ + '[IO.Compression.CompressionMode]::Decompress),[Text.Encoding]::UTF8).ReadToEnd())';
454
+ const compressed = `powershell -NoProfile -Command "${bootstrap}"`;
455
+ // Only when genuinely shorter: for a small script the fixed bootstrap costs more
456
+ // than it saves, so this keeps the plain form in play for every small caller.
457
+ return compressed.length < plain.length ? compressed : plain;
458
+ }
318
459
  export function buildWindowsAgentsCommand(cmd) {
319
- return `powershell -NoProfile -EncodedCommand ${encodePowershell(windowsAgentsScript(cmd))}`;
460
+ return renderPowershellCommand(windowsAgentsScript(cmd));
320
461
  }
321
462
  /**
322
463
  * Build the `ssh <target> <cmd>` string for `agents secrets import` on a Windows
@@ -0,0 +1,2 @@
1
+ /** The script behind either render route. Throws if the command is neither. */
2
+ export declare function decodeRenderedPowershell(command: string): string;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Test-only decoder for a rendered remote PowerShell command.
3
+ *
4
+ * `renderPowershellCommand` emits whichever of two representations is shorter: the
5
+ * plain `-EncodedCommand <base64 UTF-16LE>`, or a `-Command` bootstrap carrying a
6
+ * deflated UTF-8 payload. Tests assert on the SCRIPT, not the representation, so
7
+ * every suite needs both — and four copies of that parsing is exactly how one of
8
+ * them silently stops decoding the form it was meant to check.
9
+ *
10
+ * Named `*.test-fixture.ts` after the existing `daemon.test-fixture.ts`.
11
+ */
12
+ import * as zlib from 'node:zlib';
13
+ /** The script behind either render route. Throws if the command is neither. */
14
+ export function decodeRenderedPowershell(command) {
15
+ const encoded = /-EncodedCommand (\S+)\s*$/.exec(command);
16
+ if (encoded)
17
+ return Buffer.from(encoded[1], 'base64').toString('utf16le');
18
+ const packed = /FromBase64String\('([A-Za-z0-9+/=]+)'\)/.exec(command);
19
+ if (!packed)
20
+ throw new Error(`not a rendered PowerShell command: ${command}`);
21
+ return zlib.inflateRawSync(Buffer.from(packed[1], 'base64')).toString('utf-8');
22
+ }
@@ -22,6 +22,16 @@ export interface MailboxMessage {
22
22
  * receipts back to the feed store.
23
23
  */
24
24
  blockId?: string;
25
+ /**
26
+ * Which ASK of that block this message answers, and which claim sent it. A
27
+ * blockId is per SESSION and is reused by every generation of that session's
28
+ * questions, so without these a `consumed` receipt raised on drain cannot say
29
+ * WHICH question it acknowledges -- and a late one for question N would
30
+ * resolve question N+1 (PHNX-3999). Carried on the message so the binding
31
+ * survives the process that enqueued it.
32
+ */
33
+ generation?: string;
34
+ attempt?: string;
25
35
  /**
26
36
  * Drop reason when a message is archived without delivery (expired, dead box, etc.).
27
37
  * Set by the TTL/liveness layer, not by writers.
@@ -49,6 +59,8 @@ export declare function enqueue(boxDir: string, msg: {
49
59
  text: string;
50
60
  from?: string;
51
61
  blockId?: string;
62
+ generation?: string;
63
+ attempt?: string;
52
64
  ttlSeconds?: number;
53
65
  }): string;
54
66
  /** Parse a message file. Returns null on missing/corrupt/invalid-shape. */
@@ -97,6 +97,8 @@ export function enqueue(boxDir, msg) {
97
97
  ts: now.toISOString(),
98
98
  text: msg.text,
99
99
  blockId: msg.blockId,
100
+ ...(msg.generation !== undefined ? { generation: msg.generation } : {}),
101
+ ...(msg.attempt !== undefined ? { attempt: msg.attempt } : {}),
100
102
  };
101
103
  const ttlSeconds = msg.ttlSeconds ?? resolveDefaultTtlSeconds();
102
104
  if (ttlSeconds > 0) {
@@ -128,7 +130,14 @@ export function readMessage(file) {
128
130
  if (typeof m?.msgId !== 'string' || typeof m?.to !== 'string' || typeof m?.text !== 'string') {
129
131
  return null;
130
132
  }
131
- return { msgId: m.msgId, to: m.to, from: m.from, ts: m.ts ?? '', text: m.text, expiresAt: m.expiresAt, blockId: m.blockId, dropped: m.dropped };
133
+ // Field-by-field on purpose (an unknown key from a foreign writer is dropped),
134
+ // so EVERY field the receipt binding depends on must be listed here or it is
135
+ // silently lost on read and the binding it enables never fires.
136
+ return {
137
+ msgId: m.msgId, to: m.to, from: m.from, ts: m.ts ?? '', text: m.text,
138
+ expiresAt: m.expiresAt, blockId: m.blockId, generation: m.generation, attempt: m.attempt,
139
+ dropped: m.dropped,
140
+ };
132
141
  }
133
142
  /** True when a message has a parsed expiry in the past. */
134
143
  export function isExpired(msg, now = new Date()) {
@@ -230,7 +239,12 @@ function consumeClaimed(boxDir, name, expectedTo) {
230
239
  if (msg.blockId) {
231
240
  try {
232
241
  const feedRoot = process.env.AGENTS_FEED_DIR;
233
- recordMessageReceipt(msg.blockId, { msgId: msg.msgId, status: 'consumed', at: new Date().toISOString(), from: msg.from }, feedRoot);
242
+ // The origin rides the message, so the receipt names the ask it answers.
243
+ recordMessageReceipt(msg.blockId, {
244
+ msgId: msg.msgId, status: 'consumed', at: new Date().toISOString(), from: msg.from,
245
+ ...(msg.generation !== undefined ? { generation: msg.generation } : {}),
246
+ ...(msg.attempt !== undefined ? { attempt: msg.attempt } : {}),
247
+ }, feedRoot);
234
248
  }
235
249
  catch {
236
250
  // Receipt surfacing is best-effort; never stall delivery.
package/dist/lib/mcp.js CHANGED
@@ -380,25 +380,31 @@ function installMcpViaClaude(binaryPath, server, versionHome) {
380
380
  * Uses: codex mcp add <name> -- <cmd> [args...]
381
381
  */
382
382
  function installMcpViaCodex(binaryPath, server, versionHome) {
383
- if (server.config.transport === 'stdio') {
383
+ let args;
384
+ if (server.config.transport === 'http') {
385
+ if (!server.config.url)
386
+ throw new Error(`HTTP MCP '${server.name}' has no url`);
387
+ // codex mcp add <name> --url <url>
388
+ args = ['mcp', 'add', server.name, '--url', server.config.url];
389
+ }
390
+ else {
384
391
  // codex mcp add -- <name> <cmd> [args...]
385
- const args = [
392
+ args = [
386
393
  'mcp', 'add',
387
394
  '--',
388
395
  server.name,
389
396
  server.config.command,
390
397
  ...(server.config.args || [])
391
398
  ];
392
- // RUSH-1752: user-controlled MCP command/args must not reach cmd.exe unquoted.
393
- const spec = execFileShellSpec(binaryPath, args);
394
- execFileSync(spec.command, spec.args, {
395
- stdio: 'pipe',
396
- timeout: 30000,
397
- env: { ...process.env, HOME: versionHome },
398
- shell: spec.shell,
399
- });
400
399
  }
401
- // Note: Codex may not support HTTP MCPs
400
+ // RUSH-1752: user-controlled MCP command/args must not reach cmd.exe unquoted.
401
+ const spec = execFileShellSpec(binaryPath, args);
402
+ execFileSync(spec.command, spec.args, {
403
+ stdio: 'pipe',
404
+ timeout: 30000,
405
+ env: { ...process.env, HOME: versionHome },
406
+ shell: spec.shell,
407
+ });
402
408
  }
403
409
  export async function registerMcpCommandToTargets(targets, name, commandSpec, scope = 'user', transport = 'stdio') {
404
410
  const results = [];
@@ -1,3 +1,4 @@
1
+ import type { DeviceStats } from '../devices/health.js';
1
2
  import type { WatchdogTickResult } from '../watchdog/runner.js';
2
3
  /**
3
4
  * One registered fleet device, for the menu-bar's collapsible DEVICES section.
@@ -20,6 +21,47 @@ interface MenubarDevice {
20
21
  interactive: boolean;
21
22
  isLocal: boolean;
22
23
  preferred: boolean;
24
+ /**
25
+ * The operator's role mark — `worker` | `personal` | `desktop`, or `unknown`
26
+ * when the device was never marked (PHNX-3999 F25). Read from the same
27
+ * device-scope config `--device auto` consults, so the menu shows the fact that
28
+ * governs placement rather than a second notion of it.
29
+ */
30
+ role: string;
31
+ /**
32
+ * True when automatic placement (`agents run --device auto`) may pick this
33
+ * device: the verdict of the ONE canonical pool filter, so a `personal` box the
34
+ * user sits at reads as ineligible here exactly as it behaves there.
35
+ */
36
+ autoEligible: boolean;
37
+ /** Hardware facts and the current reading, or null when never observed. */
38
+ stats: MenubarDeviceStats | null;
39
+ }
40
+ /**
41
+ * One device's hardware facts and last reading, projected from the fleet-stats
42
+ * cache the CLI already keeps (`~/.agents/.cache/.fleet-stats.json`).
43
+ *
44
+ * Cache-only by construction: the snapshot never probes the fleet, so opening
45
+ * Settings costs nothing and a device nobody has measured says so. Every value
46
+ * is `null` when that number was not observed — never 0, which would render as a
47
+ * real "idle, empty disk" reading. `stale` carries the CLI's own freshness bound
48
+ * so the menu can label an old reading instead of showing it as live, and
49
+ * `observedAt` / `specsObservedAt` give the observation age F25 asks for
50
+ * (hardware totals survive an unreachable probe; current-state readings do not).
51
+ */
52
+ interface MenubarDeviceStats {
53
+ reachable: boolean;
54
+ observedAt: string;
55
+ stale: boolean;
56
+ cpus: number | null;
57
+ memTotalBytes: number | null;
58
+ memFreeBytes: number | null;
59
+ memPercent: number | null;
60
+ diskTotalBytes: number | null;
61
+ diskFreeBytes: number | null;
62
+ diskUsedPercent: number | null;
63
+ loadPercent: number | null;
64
+ specsObservedAt: string | null;
23
65
  }
24
66
  interface MenubarSnapshot {
25
67
  version: 1;
@@ -55,6 +97,15 @@ interface MenubarSnapshot {
55
97
  * (only `defaultProject`) is omitted so the native app keeps its own default.
56
98
  */
57
99
  export declare function buildMenuPreferences(): Record<string, unknown>;
100
+ /**
101
+ * Project one cached {@link DeviceStats} row into the snapshot shape, or `null`
102
+ * when the device has no cached reading at all.
103
+ *
104
+ * `?? null` rather than a default: an absent number means "not observed", and the
105
+ * menu renders that as unavailable. A 0 would be indistinguishable from a real
106
+ * measurement of an idle box or a full disk.
107
+ */
108
+ export declare function projectDeviceStats(row: DeviceStats | undefined): MenubarDeviceStats | null;
58
109
  export declare function readLastWatchdogTick(stateDir?: string): WatchdogTickResult | null;
59
110
  /** One-process read model for AGI Menu's repeating three-minute refresh. */
60
111
  export declare function computeMenubarSnapshot(): Promise<MenubarSnapshot>;
@@ -2,7 +2,9 @@ import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { buildRoutineListJson } from '../scheduling/routines.js';
4
4
  import { backfillActiveRowsFromIndex, isRunningLiveSession, serializeActiveSessionsForJson, serializeSessionsJson } from '../session/active.js';
5
- import { getConfigValue, loadAutoLaunchPreferences } from '../device-config.js';
5
+ import { getConfigValue, listConfiguredDeviceRoles, loadAutoLaunchPreferences } from '../device-config.js';
6
+ import { filterAutoPool } from '../devices/pool.js';
7
+ import { isFreshDeviceStats, readStatsCache } from '../devices/stats-cache.js';
6
8
  import { MENUBAR_MENU_PROPERTIES } from '../config-keys.js';
7
9
  import { migrateMenubarPreferencesFromUserDefaults } from './migrate-prefs.js';
8
10
  import { loadDevices } from '../devices/registry.js';
@@ -35,12 +37,20 @@ export function buildMenuPreferences() {
35
37
  */
36
38
  async function buildMenubarDevices() {
37
39
  const reg = await loadDevices();
40
+ const roster = Object.keys(reg);
38
41
  // Pass the roster so a fleet-wide default (fleet.defaults.config) reaches
39
42
  // devices that have no doc of their own.
40
- const prefs = loadAutoLaunchPreferences(Object.keys(reg));
43
+ const prefs = loadAutoLaunchPreferences(roster);
44
+ const roles = listConfiguredDeviceRoles(roster);
45
+ // The placement verdict from the one canonical filter — not a re-implementation
46
+ // of the role rule (`devices/pool.ts` owns it).
47
+ const autoEligible = new Set(filterAutoPool(roster, { roles, autoLaunch: prefs }));
48
+ // Cache read only: no ssh, no probe, so this still rides the existing snapshot
49
+ // poll (docs/menubar.md: the menu bar must not probe the fleet per render).
50
+ const stats = readStatsCache();
41
51
  const interactiveHost = getConfigValue('interactive.host').value;
42
52
  const self = machineId();
43
- return Object.keys(reg)
53
+ return roster
44
54
  .sort()
45
55
  .map((name) => ({
46
56
  name,
@@ -50,8 +60,37 @@ async function buildMenubarDevices() {
50
60
  interactive: name === interactiveHost,
51
61
  isLocal: name === self,
52
62
  preferred: prefs[name]?.preferred === true,
63
+ role: roles[name] ?? 'unknown',
64
+ autoEligible: autoEligible.has(name),
65
+ stats: projectDeviceStats(stats[name]),
53
66
  }));
54
67
  }
68
+ /**
69
+ * Project one cached {@link DeviceStats} row into the snapshot shape, or `null`
70
+ * when the device has no cached reading at all.
71
+ *
72
+ * `?? null` rather than a default: an absent number means "not observed", and the
73
+ * menu renders that as unavailable. A 0 would be indistinguishable from a real
74
+ * measurement of an idle box or a full disk.
75
+ */
76
+ export function projectDeviceStats(row) {
77
+ if (!row)
78
+ return null;
79
+ return {
80
+ reachable: row.reachable,
81
+ observedAt: new Date(row.fetchedAt).toISOString(),
82
+ stale: !isFreshDeviceStats(row),
83
+ cpus: row.ncpu ?? null,
84
+ memTotalBytes: row.memTotalBytes ?? null,
85
+ memFreeBytes: row.memFreeBytes ?? null,
86
+ memPercent: row.memPercent ?? null,
87
+ diskTotalBytes: row.diskTotalBytes ?? null,
88
+ diskFreeBytes: row.diskFreeBytes ?? null,
89
+ diskUsedPercent: row.diskUsedPercent ?? null,
90
+ loadPercent: row.loadPercent ?? null,
91
+ specsObservedAt: row.specsFetchedAt ? new Date(row.specsFetchedAt).toISOString() : null,
92
+ };
93
+ }
55
94
  export function readLastWatchdogTick(stateDir = path.join(getRuntimeStateDir(), 'watchdog')) {
56
95
  try {
57
96
  return JSON.parse(fs.readFileSync(path.join(stateDir, 'last-tick.json'), 'utf-8'));
@@ -9,10 +9,10 @@
9
9
  * The OS handler has none of that.
10
10
  *
11
11
  * Before this seam existed, `agents browser navigate` honoured the configured
12
- * profile and nothing else did: `fleet login`, `devices lease`, `feedback`, and
12
+ * profile and nothing else did: `devices lease`, `feedback`, and
13
13
  * the browser-session artifact opener each shelled straight to `open`/`xdg-open`,
14
14
  * so every one of them landed in whatever the OS handler happened to be. This
15
- * module replaces all of those call sites; do not add a sixth raw `open`.
15
+ * module replaces all of those call sites; do not add a new raw `open`.
16
16
  *
17
17
  * Never throws. A viewer that cannot be reached degrades to the OS handler with
18
18
  * one stderr line naming the reason, and a total failure returns `via: 'none'`
@@ -3,6 +3,9 @@
3
3
  * timeout — matching the `execFileAsync` contract the version probe had — and
4
4
  * reaps the probe's whole process group on every settle path.
5
5
  */
6
- export declare function probeCapture(cmd: string, args: string[], timeoutMs: number): Promise<{
6
+ export declare function probeCapture(cmd: string, args: string[], timeoutMs: number, options?: {
7
+ acceptedExitCodes?: number[];
8
+ maxOutputBytes?: number;
9
+ }): Promise<{
7
10
  stdout: string;
8
11
  }>;
package/dist/lib/probe.js CHANGED
@@ -59,7 +59,7 @@ function reapGroup(pid) {
59
59
  * timeout — matching the `execFileAsync` contract the version probe had — and
60
60
  * reaps the probe's whole process group on every settle path.
61
61
  */
62
- export function probeCapture(cmd, args, timeoutMs) {
62
+ export function probeCapture(cmd, args, timeoutMs, options = {}) {
63
63
  return new Promise((resolve, reject) => {
64
64
  const child = spawn(cmd, args, {
65
65
  detached: GROUP_REAP,
@@ -92,6 +92,9 @@ export function probeCapture(cmd, args, timeoutMs) {
92
92
  child.stdout?.setEncoding('utf8');
93
93
  child.stdout?.on('data', (d) => {
94
94
  out += d;
95
+ if (Buffer.byteLength(out) > (options.maxOutputBytes ?? 1024 * 1024)) {
96
+ settle(new Error(`probe output exceeded limit: ${cmd}`));
97
+ }
95
98
  });
96
99
  child.on('error', (e) => settle(e));
97
100
  // 'exit', not 'close': a forked grandchild inherits the stdout pipe, and
@@ -99,7 +102,7 @@ export function probeCapture(cmd, args, timeoutMs) {
99
102
  // process this helper exists to reap. Settle when the probed binary
100
103
  // itself exits; one tick's grace lets its final stdout chunks land.
101
104
  child.on('exit', (code) => {
102
- setImmediate(() => settle(code === 0 ? null : new Error(`probe exited ${code}: ${cmd} ${args.join(' ')}`)));
105
+ setImmediate(() => settle(code !== null && (options.acceptedExitCodes ?? [0]).includes(code) ? null : new Error(`probe exited ${code}: ${cmd} ${args.join(' ')}`)));
103
106
  });
104
107
  });
105
108
  }
@@ -207,6 +207,29 @@ export declare function projectDirsAbs(def: ProjectDef, opts: {
207
207
  * home-relative variant lands (see docs/concepts.md).
208
208
  */
209
209
  export declare function projectNameForCwd(cwd: string | undefined, defs: ProjectDef[]): string | undefined;
210
+ export declare function listProjectDefsCached(): ProjectDef[];
211
+ /** Drop the {@link listProjectDefsCached} memo — for tests that rewrite the dir within one mtime tick. */
212
+ export declare function resetProjectDefsCache(): void;
213
+ /**
214
+ * The **confirmed** project for a working directory, or `undefined` when the
215
+ * association is not confirmed (PHNX-3999 F08/F09).
216
+ *
217
+ * Confirmed means exactly one thing: a registered project definition
218
+ * ({@link projectNameForCwd}) whose root contains this path. Being inside *some*
219
+ * git repository is NOT a project association — a repo nobody registered, a
220
+ * checkout of someone else's code, or a loose directory that merely has a
221
+ * `.git` in it would each invent a project name out of a folder, which is the
222
+ * wrong-grouping the owner's recording shows at 01:30–01:56. So is a bare
223
+ * directory basename ({@link resolveProjectKey}, which always answers).
224
+ *
225
+ * A consumer renders `undefined` as Uncategorized and keeps the session
226
+ * reachable in its full list — nothing is hidden by not being grouped.
227
+ *
228
+ * Use {@link resolveProjectNameForCwd} instead when you want a best-effort
229
+ * bucket KEY for joining rows; use this when the answer is shown to a person as
230
+ * "this work belongs to that project".
231
+ */
232
+ export declare function confirmedProjectForCwd(cwd: string | undefined | null, defs?: ProjectDef[]): string | undefined;
210
233
  /**
211
234
  * The canonical project label for a cwd, for every surface that buckets work by
212
235
  * project (the activity timeline, feed posts, the sessions overview): the