openmausbot 0.1.84 → 0.1.86

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 (230) hide show
  1. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  2. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  3. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  4. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  5. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  6. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  7. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  8. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  9. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  10. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  11. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  12. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  13. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  14. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  15. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  16. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  17. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  18. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  19. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  20. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  21. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  22. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  23. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  24. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  25. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  26. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  27. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  28. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  29. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  30. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  31. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  32. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  33. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  34. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  35. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  36. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  37. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  38. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  39. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  40. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  41. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  42. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  43. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  44. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  45. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  46. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  47. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  48. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  49. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  50. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  51. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  52. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  53. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  54. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  55. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  56. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  57. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  58. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  59. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  60. package/dist/assets/abnfDiagram-O67JEVCF-DpUZNnFD.js +1 -0
  61. package/dist/assets/arc-DW7mrI4p.js +1 -0
  62. package/dist/assets/architectureDiagram-NJMV4G6O-CyeZ5WLZ.js +36 -0
  63. package/dist/assets/blockDiagram-BEXU5L5S-CBVaNjO7.js +140 -0
  64. package/dist/assets/c4Diagram-YGBWAQC7-sf8d41uT.js +38 -0
  65. package/dist/assets/channel-BXFkgus9.js +1 -0
  66. package/dist/assets/chunk-2Q5K7J3B-BOGvy7Y_.js +1 -0
  67. package/dist/assets/chunk-3FUC2YCW-BiF9zyiz.js +2 -0
  68. package/dist/assets/chunk-5VM5RSS4-EFuZ1SBZ.js +15 -0
  69. package/dist/assets/chunk-GTNCS2PH-pJLNSAT7.js +88 -0
  70. package/dist/assets/chunk-GWA4HPMP-Cb-3_3IT.js +1 -0
  71. package/dist/assets/chunk-JWPE2WC7-DAf5aogn.js +1 -0
  72. package/dist/assets/chunk-NETBCI7D-DR3kiwZz.js +1 -0
  73. package/dist/assets/chunk-WEXAMYUT-BmU8PeyM.js +1 -0
  74. package/dist/assets/chunk-XXDRQBXY-Buh8g8OT.js +1 -0
  75. package/dist/assets/classDiagram-v2-NBCMYWYE-BurfmMWM.js +217 -0
  76. package/dist/assets/cose-bilkent-JH36ORCC-KkGZUEkU.js +1 -0
  77. package/dist/assets/cynefin-OW5HDTMX-C3PpV96d.js +166 -0
  78. package/dist/assets/cynefinDiagram-VND7K2PF-Dt8JwJaI.js +62 -0
  79. package/dist/assets/cytoscape.esm-BB4DxJjf.js +321 -0
  80. package/dist/assets/dagre-6A5THRUB-DkjN8266.js +4 -0
  81. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  82. package/dist/assets/diagram-22UHCM2B-DUAJS0JV.js +200 -0
  83. package/dist/assets/diagram-3UASUU5V-Dw4KhF1a.js +24 -0
  84. package/dist/assets/diagram-ATOU4E4O-Dd1gmQqf.js +3 -0
  85. package/dist/assets/diagram-CDSNMT55-CjwGs0Yw.js +30 -0
  86. package/dist/assets/diagram-MLGK6HIB-BL-69Zrq.js +24 -0
  87. package/dist/assets/diagram-MPIPVDR6-5Zi2N7wo.js +41 -0
  88. package/dist/assets/ebnfDiagram-ZINNZB2B-D9NekWUX.js +1 -0
  89. package/dist/assets/elk-276RUBZZ-CkaSQjxw.js +27 -0
  90. package/dist/assets/erDiagram-OPXOYQCR-CdAVBV8-.js +99 -0
  91. package/dist/assets/flowDiagram-KWPJA3E3-Bm_lWWH7.js +213 -0
  92. package/dist/assets/ganttDiagram-FUAMR5RP-FZ8ejYWJ.js +292 -0
  93. package/dist/assets/gitGraphDiagram-X574FWY7-Dlv9rizZ.js +106 -0
  94. package/dist/assets/graph-D8UmR1vK.js +1 -0
  95. package/dist/assets/index-BGgqlWPJ.css +1 -0
  96. package/dist/assets/index-BtHo8ZKl.js +577 -0
  97. package/dist/assets/{index-DNa2umw-.js → index-h9FFNwTu.js} +1 -1
  98. package/dist/assets/infoDiagram-VRGFBTTK-BdQ_gW5B.js +2 -0
  99. package/dist/assets/init-Gi6I4Gst.js +1 -0
  100. package/dist/assets/ishikawaDiagram-OU5B5YK6-EwssEp2X.js +70 -0
  101. package/dist/assets/journeyDiagram-ZHPQQLJL-BX1G1vOd.js +139 -0
  102. package/dist/assets/kanban-definition-PNTS6WVX-BB4kAsyC.js +89 -0
  103. package/dist/assets/linear-3su2TG93.js +1 -0
  104. package/dist/assets/mermaid.core-CDD1E0s9.js +311 -0
  105. package/dist/assets/mindmap-definition-NLK3R4M7-Ba50MsNK.js +96 -0
  106. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  107. package/dist/assets/pegDiagram-GJSIUBJH-DZLkkZlM.js +1 -0
  108. package/dist/assets/pieDiagram-5QR66LMP-DaTz77Uq.js +39 -0
  109. package/dist/assets/quadrantDiagram-O4NWA36T-CpaiidpR.js +7 -0
  110. package/dist/assets/railroadDiagram-XR7U4H2S-mN4JrMeI.js +1 -0
  111. package/dist/assets/requirementDiagram-PLB6GJNP-BSNDRpg7.js +84 -0
  112. package/dist/assets/sankeyDiagram-IPEJSGJF-FngzGNug.js +40 -0
  113. package/dist/assets/sequenceDiagram-PO4LG4MO-CM4pCC3L.js +169 -0
  114. package/dist/assets/sizeCapture-INFHLROL-CRICHXkw.js +1 -0
  115. package/dist/assets/stateDiagram-v2-GCMORJYK-oyTWpxH2.js +291 -0
  116. package/dist/assets/swimlanes-2SLR337P-D9h9mPBq.js +1 -0
  117. package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +8 -0
  118. package/dist/assets/timeline-definition-EJHVYXUP-BJUlN9bW.js +120 -0
  119. package/dist/assets/usecaseDiagram-POWQR4AR-3uBNkbAb.js +370 -0
  120. package/dist/assets/vennDiagram-UO4OBE2U-D2odalzT.js +34 -0
  121. package/dist/assets/wardleyDiagram-VNRHLVJA-DHgFaB9S.js +78 -0
  122. package/dist/assets/xychartDiagram-PMCCYNJV-w5GYM2h0.js +7 -0
  123. package/dist/index.html +2 -2
  124. package/dist-server/companion/src/routes.js +1 -0
  125. package/dist-server/container-mcp.js +231 -17
  126. package/dist-server/drivers/agents-proxy.js +16097 -1411
  127. package/dist-server/enterprise/LICENSE +32 -0
  128. package/dist-server/enterprise/server/index.js +6 -5
  129. package/dist-server/hooks/omb-hook.js +74 -0
  130. package/dist-server/index.js +12495 -7584
  131. package/dist-server/local-computer-proxy.js +83 -1
  132. package/dist-server/local-computer.js +14 -2
  133. package/dist-server/mcp-server.js +3 -1
  134. package/dist-server/openmausbot.js +5458 -4587
  135. package/dist-server/pair-cli.js +5458 -4587
  136. package/dist-server/proxy-paths.js +1 -0
  137. package/dist-server/server/admin-activity.js +410 -0
  138. package/dist-server/server/agent-tool-policy.js +1 -0
  139. package/dist-server/server/bot-package.js +2 -0
  140. package/dist-server/server/bot-visibility.js +462 -0
  141. package/dist-server/server/box.js +25 -5
  142. package/dist-server/server/browser-engine.js +87 -10
  143. package/dist-server/server/browser-live.js +7 -5
  144. package/dist-server/server/browser-runtime.js +66 -11
  145. package/dist-server/server/channel-queue.js +2 -0
  146. package/dist-server/server/checkpoints.js +62 -18
  147. package/dist-server/server/cli-prompts.js +26 -2
  148. package/dist-server/server/cli.js +79 -10
  149. package/dist-server/server/commands.js +27 -0
  150. package/dist-server/server/compaction-summary.js +78 -0
  151. package/dist-server/server/composio.js +49 -11
  152. package/dist-server/server/config.js +137 -14
  153. package/dist-server/server/context-budget.js +29 -0
  154. package/dist-server/server/context-rebuild.js +72 -0
  155. package/dist-server/server/decision-log.js +209 -48
  156. package/dist-server/server/default-model-selection.js +20 -3
  157. package/dist-server/server/delegations.js +11 -9
  158. package/dist-server/server/digest.js +130 -0
  159. package/dist-server/server/drivers/acp/core.js +637 -238
  160. package/dist-server/server/drivers/acp/grok.js +4 -3
  161. package/dist-server/server/drivers/agents-call.js +1123 -0
  162. package/dist-server/server/drivers/agents-catalog.js +659 -0
  163. package/dist-server/server/drivers/agents-client.js +28 -0
  164. package/dist-server/server/drivers/agents-proxy.js +18 -1683
  165. package/dist-server/server/drivers/agents-result.js +23 -0
  166. package/dist-server/server/drivers/chat-mcp-tools.js +17 -2
  167. package/dist-server/server/drivers/claude.js +83 -16
  168. package/dist-server/server/drivers/codex-mcp-names.js +44 -0
  169. package/dist-server/server/drivers/codex.js +32 -4
  170. package/dist-server/server/drivers/grok.js +2 -1
  171. package/dist-server/server/drivers/openai-chat.js +66 -45
  172. package/dist-server/server/drivers/openai-compat.js +16 -8
  173. package/dist-server/server/enterprise.js +97 -16
  174. package/dist-server/server/external-runtime.js +83 -0
  175. package/dist-server/server/guarded-requests.js +60 -0
  176. package/dist-server/server/harness/http.js +61 -0
  177. package/dist-server/server/hooks/omb-hook.js +103 -0
  178. package/dist-server/server/hosted-models.js +151 -0
  179. package/dist-server/server/hosted-slack.js +11 -0
  180. package/dist-server/server/index.js +2810 -678
  181. package/dist-server/server/local-computer.js +21 -2
  182. package/dist-server/server/managed-desktop.js +79 -14
  183. package/dist-server/server/managed-policy.js +153 -0
  184. package/dist-server/server/mcp-bridge.js +10 -2
  185. package/dist-server/server/mcp-tool-images.js +37 -0
  186. package/dist-server/server/mcp-tool-schema.js +108 -0
  187. package/dist-server/server/message-db.js +108 -21
  188. package/dist-server/server/message-file.js +3 -2
  189. package/dist-server/server/model-context-window.js +7 -0
  190. package/dist-server/server/model-prices.js +121 -0
  191. package/dist-server/server/notify.js +10 -1
  192. package/dist-server/server/package-export.js +1 -0
  193. package/dist-server/server/peer-roster.js +17 -4
  194. package/dist-server/server/provider-icon.js +20 -0
  195. package/dist-server/server/proxy-paths.js +1 -0
  196. package/dist-server/server/recent-work.js +15 -0
  197. package/dist-server/server/request-auth.js +97 -0
  198. package/dist-server/server/room-handoffs.js +41 -13
  199. package/dist-server/server/routes/hosted-slack.js +20 -0
  200. package/dist-server/server/routes/table.js +14 -0
  201. package/dist-server/server/routine-requests.js +39 -1
  202. package/dist-server/server/routines.js +65 -9
  203. package/dist-server/server/shared-computer-control.js +31 -4
  204. package/dist-server/server/spend.js +111 -16
  205. package/dist-server/server/steer-queue.js +12 -1
  206. package/dist-server/server/store.js +384 -33
  207. package/dist-server/server/system-prompt.js +4 -4
  208. package/dist-server/server/team-backup.js +19 -2
  209. package/dist-server/server/thread-starters.js +53 -0
  210. package/dist-server/server/tool-results.js +72 -0
  211. package/dist-server/server/tts/grok.js +74 -0
  212. package/dist-server/server/tts/index.js +19 -1
  213. package/dist-server/server/usage-ledger.js +31 -8
  214. package/dist-server/server/webhooks.js +28 -0
  215. package/dist-server/server/workspace-backup-policy.js +9 -2
  216. package/dist-server/server/workspace-backup.js +7 -4
  217. package/dist-server/shared/approval-mode.js +7 -1
  218. package/dist-server/shared/digest.js +1 -0
  219. package/dist-server/shared/markdown-windows-paths.js +36 -0
  220. package/dist-server/shared/mention-boundary.js +15 -0
  221. package/dist-server/shared/provider-icon.js +110 -0
  222. package/dist-server/shared/routines.js +7 -1
  223. package/dist-server/shared/team-backup.js +5 -0
  224. package/dist-server/src/lib/composer-attachments.js +16 -0
  225. package/dist-server/vps-container-mcp.js +236 -22
  226. package/enterprise/README.md +33 -12
  227. package/enterprise/server/index.js +6 -5
  228. package/package.json +1 -1
  229. package/dist/assets/index-BbU5REzd.js +0 -310
  230. package/dist/assets/index-CLGfYlx_.css +0 -1
@@ -25,7 +25,7 @@ const DOWNLOAD_TIMEOUT_MS = 10 * 60_000;
25
25
  /** Native restore looks up a filename prefix. Fixed-length keys prevent a
26
26
  * legacy profile named work from accidentally restoring work-client. */
27
27
  export function browserRestoreKey(session) {
28
- if (!/^[A-Za-z0-9_-]{1,96}$/.test(session))
28
+ if (!/^[A-Za-z0-9_.-]{1,96}$/.test(session))
29
29
  throw new Error("Invalid browser session.");
30
30
  return `omb-${createHash("sha256").update(session).digest("hex")}`;
31
31
  }
@@ -56,7 +56,7 @@ function ensureManagedBrowserConfig(env) {
56
56
  }
57
57
  /** Close exactly one daemon without the CLI's implicit launch envelope. */
58
58
  export async function closeBrowserSession(binaryPath, env, timeoutMs = 15_000) {
59
- if (!env.AGENT_BROWSER_SESSION || !/^[A-Za-z0-9_-]{1,96}$/.test(env.AGENT_BROWSER_SESSION))
59
+ if (!env.AGENT_BROWSER_SESSION || !/^[A-Za-z0-9_.-]{1,96}$/.test(env.AGENT_BROWSER_SESSION))
60
60
  return false;
61
61
  // Native CLI prepends a launch even to `close` when launch flags are set.
62
62
  // Remove them, and bypass external project/user config, so closing a
@@ -85,7 +85,16 @@ export async function closeBrowserSession(binaryPath, env, timeoutMs = 15_000) {
85
85
  catch {
86
86
  return finish(false);
87
87
  }
88
- const timer = setTimeout(() => { child.kill(); finish(false); }, Math.max(1, deadline - Date.now()));
88
+ let exited = false;
89
+ let killTimer;
90
+ const timer = setTimeout(() => {
91
+ // TERM lets the daemon flush; KILL after a grace period reaps a stuck one.
92
+ child.kill("SIGTERM");
93
+ killTimer = setTimeout(() => { if (!exited)
94
+ child.kill("SIGKILL"); }, 5_000);
95
+ killTimer.unref?.();
96
+ finish(false);
97
+ }, Math.max(1, deadline - Date.now()));
89
98
  timer.unref?.();
90
99
  child.stdout?.on("data", (chunk) => {
91
100
  output += String(chunk);
@@ -95,9 +104,9 @@ export async function closeBrowserSession(binaryPath, env, timeoutMs = 15_000) {
95
104
  finish(false);
96
105
  }
97
106
  });
98
- child.on("error", () => { clearTimeout(timer); finish(false); });
107
+ child.on("error", () => { exited = true; clearTimeout(timer); clearTimeout(killTimer); finish(false); });
99
108
  // exit can precede the last piped stdout chunk; close follows stdio.
100
- child.on("close", (code) => { clearTimeout(timer); finish(code === 0); });
109
+ child.on("close", (code) => { exited = true; clearTimeout(timer); clearTimeout(killTimer); finish(code === 0); });
101
110
  });
102
111
  if (!(await run(["close"])).ok)
103
112
  return false;
@@ -241,6 +250,8 @@ export function resolveAgentBrowserBinary(options = {}) {
241
250
  const pinned = pinnedBinaryPath(options.dataDir, platform, options.arch);
242
251
  if (exists(pinned))
243
252
  return pinned;
253
+ if (options.managedOnly)
254
+ return null;
244
255
  return onPath(env, platform, exists);
245
256
  }
246
257
  /** Download the pinned release asset for this machine into the data dir,
@@ -263,7 +274,35 @@ export async function installAgentBrowserBinary(options = {}) {
263
274
  const response = await (options.fetchImpl ?? fetch)(url, { redirect: "follow", signal: controller.signal });
264
275
  if (!response.ok)
265
276
  throw new Error(`the agent-browser download failed (HTTP ${response.status})`);
266
- body = Buffer.from(await response.arrayBuffer());
277
+ const declared = Number(response.headers.get("content-length") ?? NaN);
278
+ if (Number.isFinite(declared) && declared !== asset.bytes)
279
+ throw new Error("the agent-browser download did not match its pinned size; nothing was installed");
280
+ // Stream, never buffer whole: a body past the pin is cut off mid-flight.
281
+ const chunks = [];
282
+ let received = 0;
283
+ if (response.body) {
284
+ try {
285
+ for await (const chunk of response.body) {
286
+ chunks.push(chunk);
287
+ received += chunk.byteLength;
288
+ if (received > asset.bytes) {
289
+ controller.abort();
290
+ break;
291
+ }
292
+ }
293
+ }
294
+ catch (error) {
295
+ // Only the abort we asked for is swallowed; the size check reports it.
296
+ if (received <= asset.bytes)
297
+ throw error;
298
+ }
299
+ body = Buffer.concat(chunks);
300
+ }
301
+ else {
302
+ body = Buffer.from(await response.arrayBuffer());
303
+ if (body.length > asset.bytes)
304
+ controller.abort();
305
+ }
267
306
  }
268
307
  finally {
269
308
  clearTimeout(timer);
@@ -273,6 +312,18 @@ export async function installAgentBrowserBinary(options = {}) {
273
312
  const digest = createHash("sha256").update(body).digest("hex");
274
313
  if (digest !== asset.sha256)
275
314
  throw new Error("the agent-browser download failed its SHA-256 check; nothing was installed");
315
+ // Sweep staging files abandoned by earlier runs before writing a new one.
316
+ try {
317
+ for (const name of readdirSync(directory)) {
318
+ if (!name.endsWith(".part"))
319
+ continue;
320
+ try {
321
+ unlinkSync(join(directory, name));
322
+ }
323
+ catch { /* already gone */ }
324
+ }
325
+ }
326
+ catch { /* best effort */ }
276
327
  const staging = `${destination}.${randomBytes(6).toString("hex")}.part`;
277
328
  writeFileSync(staging, body, { mode: 0o755 });
278
329
  if (platform !== "win32")
@@ -293,10 +344,34 @@ export function ensureChrome(binaryPath, options = {}) {
293
344
  return new Promise((done, fail) => {
294
345
  const child = spawn(binaryPath, args, { env: options.env ?? process.env, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
295
346
  let output = "";
296
- child.stdout?.on("data", (chunk) => { output += String(chunk); });
297
- child.stderr?.on("data", (chunk) => { output += String(chunk); });
298
- child.on("error", fail);
347
+ // Keep only the tail: the failure path quotes the last lines, so a chatty
348
+ // installer cannot grow the buffer without bound while it hangs.
349
+ const record = (chunk) => {
350
+ output += String(chunk);
351
+ if (output.length > 262_144)
352
+ output = output.slice(-262_144);
353
+ };
354
+ child.stdout?.on("data", record);
355
+ child.stderr?.on("data", record);
356
+ let timedOut = false;
357
+ let exited = false;
358
+ // A hung installer gets the same wall clock as the download: TERM, then
359
+ // KILL after a grace period, with the promise failing at the deadline.
360
+ const timer = setTimeout(() => {
361
+ timedOut = true;
362
+ child.kill("SIGTERM");
363
+ const killTimer = setTimeout(() => { if (!exited)
364
+ child.kill("SIGKILL"); }, 5_000);
365
+ killTimer.unref?.();
366
+ fail(new Error("agent-browser install timed out"));
367
+ }, DOWNLOAD_TIMEOUT_MS);
368
+ timer.unref?.();
369
+ child.on("error", (error) => { exited = true; clearTimeout(timer); fail(error); });
299
370
  child.on("exit", (code) => {
371
+ exited = true;
372
+ clearTimeout(timer);
373
+ if (timedOut)
374
+ return; // already failed with the timeout error
300
375
  if (code === 0) {
301
376
  options.log?.("agent-browser: Chrome is ready");
302
377
  done();
@@ -369,6 +444,8 @@ export function agentBrowserIntegration(input) {
369
444
  };
370
445
  if (input.headless !== false)
371
446
  env.AGENT_BROWSER_HEADLESS = "1";
447
+ if (input.attachCdpUrl)
448
+ env.AGENT_BROWSER_CDP = input.attachCdpUrl;
372
449
  // MCP clients may filter the parent environment. Carry the configured
373
450
  // Chrome path explicitly without forwarding unrelated secrets or flags.
374
451
  for (const name of ["PATH", "AGENT_BROWSER_EXECUTABLE_PATH"]) {
@@ -464,7 +541,7 @@ export const BUILT_IN_BROWSER_SYSTEM_PROMPT = " You have your own web browser th
464
541
  export async function clearBrowserSessionState(binaryPath, session, options = {}) {
465
542
  // The native state-clear CLI ignores the daemon session, and even its named
466
543
  // form currently drops that name before dispatch. Never invoke it here.
467
- if (!/^[A-Za-z0-9_-]{1,96}$/.test(session))
544
+ if (!/^[A-Za-z0-9_.-]{1,96}$/.test(session))
468
545
  return false;
469
546
  const env = browserRuntimeEnv({ ...options.env, AGENT_BROWSER_SESSION: session, AGENT_BROWSER_HEADLESS: "1" });
470
547
  let directory;
@@ -45,7 +45,7 @@ export function normalizeBrowserLiveMessage(value) {
45
45
  if (!message)
46
46
  return null;
47
47
  if (message.type === "error")
48
- return { type: "error", message: "The browser stream was interrupted. Reopen the browser panel to reconnect." };
48
+ return { type: "error", retryable: true, message: "The browser stream was interrupted." };
49
49
  if (message.type === "url")
50
50
  return { type: "url", url: displayUrl(message.url) };
51
51
  if (message.type === "tabs" && Array.isArray(message.tabs) && message.tabs.length <= 100) {
@@ -292,7 +292,7 @@ export class BrowserLive {
292
292
  const env = browserRuntimeEnv({ ...viewer.spec.env, AGENT_BROWSER_SESSION: viewer.session });
293
293
  try {
294
294
  const { stdout } = await execute(viewer.spec.command, [...args, "--json", "--no-webmcp"], {
295
- env, timeout: 30_000, maxBuffer: 1024 * 1024, encoding: "utf8", windowsHide: true,
295
+ env, timeout: 30_000, killSignal: "SIGKILL", maxBuffer: 1024 * 1024, encoding: "utf8", windowsHide: true,
296
296
  });
297
297
  if (!this.current(viewer))
298
298
  throw new Error("stale viewer");
@@ -302,7 +302,8 @@ export class BrowserLive {
302
302
  throw new Error("browser command failed");
303
303
  return data;
304
304
  }
305
- catch {
305
+ catch (error) {
306
+ console.warn("browser-live:", error);
306
307
  throw new BrowserLiveError("The browser could not complete this action. Check that the browser engine is installed, then reconnect.", 503);
307
308
  }
308
309
  }
@@ -432,7 +433,7 @@ export class BrowserLive {
432
433
  this.send(viewer, message);
433
434
  });
434
435
  socket.addEventListener("error", () => { if (!viewer.restarting) {
435
- this.send(viewer, { type: "error", message: "The browser stream disconnected. Reopen the browser panel." });
436
+ this.send(viewer, { type: "error", retryable: true, message: "The browser stream disconnected." });
436
437
  this.close(viewer);
437
438
  } });
438
439
  socket.addEventListener("close", () => { if (!viewer.restarting)
@@ -461,8 +462,9 @@ export class BrowserLive {
461
462
  viewer.heartbeat.unref();
462
463
  }
463
464
  catch (error) {
465
+ console.warn("browser-live:", error);
464
466
  if (options.res.headersSent) {
465
- this.send(viewer, { type: "error", message: "The browser stream could not start. Reopen the browser panel." });
467
+ this.send(viewer, { type: "error", retryable: true, message: "The browser stream could not start." });
466
468
  this.close(viewer);
467
469
  return;
468
470
  }
@@ -3,6 +3,9 @@ import { DEFAULT_BROWSER_RESULT_BUDGET, shapeBrowserToolResult, slimBrowserToolL
3
3
  export const BROWSER_CONTROL_REFUSAL = "Browser tools are paused while a person controls this browser. Wait for them to hand control back; do not try another browser or execution tool.";
4
4
  const MAX_REQUEST_BYTES = 1_048_576;
5
5
  const MAX_RESPONSE_BYTES = 16_777_216;
6
+ /** Startup, not per-request work: a cold engine spawn can exceed a tight
7
+ * per-request budget before anything has been accepted to guard. */
8
+ const HANDSHAKE_TIMEOUT_MS = 1_000;
6
9
  const HOST_ENV = ["HOME", "USERPROFILE", "APPDATA", "LOCALAPPDATA", "PATH", "Path", "TMPDIR", "TMP", "TEMP", "SystemRoot", "WINDIR", "SYSTEMDRIVE", "COMSPEC", "PATHEXT", "LANG", "LC_ALL", "XDG_CONFIG_HOME", "XDG_CACHE_HOME", "XDG_RUNTIME_DIR"];
7
10
  /** MCP, viewer commands, and cleanup must resolve the same HOME/socket paths.
8
11
  * Inherit OS plumbing, never the harness's model-provider credentials. */
@@ -10,7 +13,7 @@ export function browserRuntimeEnv(overrides) {
10
13
  const env = Object.fromEntries(HOST_ENV.flatMap((key) => process.env[key] === undefined ? [] : [[key, process.env[key]]]));
11
14
  return { ...env, ...overrides };
12
15
  }
13
- class TransportError extends Error {
16
+ export class TransportError extends Error {
14
17
  }
15
18
  /** A server-owned JSONL client. Neither child stderr nor its environment is
16
19
  * returned to the agent. Reuse the existing cross-platform spawn/kill rules. */
@@ -27,11 +30,13 @@ class BrowserClient {
27
30
  idleMs;
28
31
  maxPending;
29
32
  onClose;
30
- constructor(spec, requestTimeoutMs, idleMs, maxPending, onClose) {
33
+ onRequestTimeout;
34
+ constructor(spec, requestTimeoutMs, idleMs, maxPending, onClose, onRequestTimeout) {
31
35
  this.requestTimeoutMs = requestTimeoutMs;
32
36
  this.idleMs = idleMs;
33
37
  this.maxPending = maxPending;
34
38
  this.onClose = onClose;
39
+ this.onRequestTimeout = onRequestTimeout;
35
40
  this.child = spawnCli(spec.command, spec.args, {
36
41
  env: browserRuntimeEnv(spec.env), stdio: ["pipe", "pipe", "pipe"], shell: false,
37
42
  });
@@ -43,7 +48,7 @@ class BrowserClient {
43
48
  this.ready = this.rpc("initialize", {
44
49
  protocolVersion: "2024-11-05", capabilities: {},
45
50
  clientInfo: { name: "openmausbot-browser", version: "1" },
46
- }).then((result) => {
51
+ }, Math.max(this.requestTimeoutMs, HANDSHAKE_TIMEOUT_MS)).then((result) => {
47
52
  if (!result || typeof result !== "object" || !("protocolVersion" in result)) {
48
53
  throw new TransportError("Browser engine returned an invalid handshake.");
49
54
  }
@@ -102,7 +107,7 @@ class BrowserClient {
102
107
  void this.stop(new TransportError("Browser connection closed."));
103
108
  });
104
109
  }
105
- rpc(method, params) {
110
+ rpc(method, params, timeoutMs = this.requestTimeoutMs) {
106
111
  if (this.stopped)
107
112
  return Promise.reject(new TransportError("Browser connection closed."));
108
113
  if (this.pending.size >= this.maxPending)
@@ -114,7 +119,13 @@ class BrowserClient {
114
119
  if (this.idleTimer)
115
120
  clearTimeout(this.idleTimer);
116
121
  return new Promise((resolve, reject) => {
117
- const timer = setTimeout(() => { void this.stop(new TransportError("Browser request timed out; restart the browser before taking control.")); }, this.requestTimeoutMs);
122
+ const timer = setTimeout(() => {
123
+ // Seal the gate synchronously with the timer, before stop() teardown:
124
+ // the rejection can surface through the ready handshake, which sits
125
+ // outside agentRpc's uncertainty classifier.
126
+ this.onRequestTimeout();
127
+ void this.stop(new TransportError("Browser request timed out; restart the browser before taking control."));
128
+ }, timeoutMs);
118
129
  timer.unref();
119
130
  this.pending.set(id, { resolve, reject, timer });
120
131
  try {
@@ -131,11 +142,35 @@ class BrowserClient {
131
142
  if (!this.stopped && this.pending.size === 0) {
132
143
  // Only the stateless MCP transport expires; saved browser state and the
133
144
  // browser daemon itself belong to the profile, not this client.
134
- this.idleTimer = setTimeout(() => { void this.stop(); }, this.idleMs);
145
+ this.idleTimer = setTimeout(() => { void this.stop(undefined, "transport"); }, this.idleMs);
135
146
  this.idleTimer.unref();
136
147
  }
137
148
  }
138
- stop(error = new TransportError("Browser connection closed.")) {
149
+ /** Upstream's MCP loop exits on stdin EOF without closing the daemon.
150
+ * In particular, Windows taskkill /T would also kill that profile's Chrome,
151
+ * even when the daemon created a new process group. Idle is not shutdown. */
152
+ retireTransport() {
153
+ return new Promise((resolve) => {
154
+ const finish = () => {
155
+ clearTimeout(timer);
156
+ this.child.off("exit", finish);
157
+ resolve();
158
+ };
159
+ const timer = setTimeout(() => {
160
+ try {
161
+ this.child.kill("SIGKILL");
162
+ }
163
+ catch { /* transport already exited */ }
164
+ finish();
165
+ }, 1_000);
166
+ this.child.once("exit", finish);
167
+ if (this.child.exitCode !== null || this.child.signalCode !== null)
168
+ finish();
169
+ else
170
+ this.child.stdin.end();
171
+ });
172
+ }
173
+ stop(error = new TransportError("Browser connection closed."), scope = "tree") {
139
174
  if (this.stoppedPromise)
140
175
  return this.stoppedPromise;
141
176
  this.stopped = true;
@@ -148,7 +183,7 @@ class BrowserClient {
148
183
  }
149
184
  this.pending.clear();
150
185
  this.onClose();
151
- this.stoppedPromise = killCliTree(this.child, 1_000).then((stopped) => {
186
+ this.stoppedPromise = scope === "transport" ? this.retireTransport() : killCliTree(this.child, 1_000).then((stopped) => {
152
187
  if (stopped)
153
188
  return;
154
189
  try {
@@ -211,6 +246,14 @@ export class BrowserRuntime {
211
246
  async agentRpc(session, spec, method, params, beforeDispatch) {
212
247
  if (method !== "tools/list" && method !== "tools/call")
213
248
  throw new Error("Unsupported browser method.");
249
+ // tools/list bypasses withAgentAction (a human may hold control), so it
250
+ // must refuse the closing window itself or its client outlives restart().
251
+ if (method !== "tools/call" && this.gate(session).closing)
252
+ throw new Error("The browser is closing. Try again shortly.");
253
+ // Uncertainty survives client replacement and never self-resolves: refuse
254
+ // every new browser request until an explicit restart clears it.
255
+ if (this.gate(session).uncertain)
256
+ throw new Error("A browser action was interrupted. Restart this browser before continuing.");
214
257
  const invoke = async () => {
215
258
  const key = JSON.stringify([spec.command, spec.args, Object.entries(spec.env).sort(([a], [b]) => a.localeCompare(b))]);
216
259
  let entry = this.clients.get(session);
@@ -220,6 +263,10 @@ export class BrowserRuntime {
220
263
  const client = new BrowserClient(spec, this.options.requestTimeoutMs, this.options.idleMs, this.options.maxPending, () => {
221
264
  if (this.clients.get(session)?.client === client)
222
265
  this.clients.delete(session);
266
+ }, () => {
267
+ const gate = this.gate(session);
268
+ if (!gate.closing)
269
+ gate.uncertain = true;
223
270
  });
224
271
  entry = { key, client };
225
272
  this.clients.set(session, entry);
@@ -242,8 +289,11 @@ export class BrowserRuntime {
242
289
  catch (error) {
243
290
  // An MCP timeout cannot prove the independent daemon stopped an
244
291
  // accepted action. Recovery must close the browser, not just its pipe.
245
- if (method === "tools/call" && error instanceof TransportError)
246
- this.gate(session).uncertain = true;
292
+ // A stop from close()/restart() is intentional, not uncertainty.
293
+ if (method === "tools/call" && error instanceof TransportError && !this.gate(session).closing) {
294
+ const gate = this.gate(session);
295
+ gate.uncertain = true;
296
+ }
247
297
  throw error;
248
298
  }
249
299
  };
@@ -344,6 +394,10 @@ export class BrowserRuntime {
344
394
  try {
345
395
  await closeBrowser();
346
396
  await this.clients.get(session)?.client.stop();
397
+ // A tools/list admitted before closing set in can register a client
398
+ // while that stop awaits; registration is synchronous, so one re-check
399
+ // is deterministic and no stray transport survives to idle expiry.
400
+ await this.clients.get(session)?.client.stop();
347
401
  gate.uncertain = false;
348
402
  gate.owner = null;
349
403
  gate.releasing = false;
@@ -364,10 +418,11 @@ export class BrowserRuntime {
364
418
  const gate = this.gate(session);
365
419
  gate.closing = true;
366
420
  gate.ready = false;
421
+ // Clear before the awaited stop: any uncertain latch after this point must win.
422
+ gate.uncertain = false;
367
423
  this.changed(gate);
368
424
  await this.clients.get(session)?.client.stop();
369
425
  gate.closing = false;
370
- gate.uncertain = false;
371
426
  this.changed(gate);
372
427
  if (!gate.owner && !gate.agents && !gate.humans)
373
428
  this.gates.delete(session);
@@ -31,6 +31,8 @@ export function queueChannelMessage(groupId, threadId, text, options = {}) {
31
31
  sendId: options.sendId,
32
32
  mode: options.mode ?? "chat",
33
33
  via: options.via,
34
+ sender: options.sender,
35
+ trigger: options.trigger,
34
36
  };
35
37
  saveChatFollowup({ id: item.id, kind: "channel", ownerId: groupId, threadId, payload: item });
36
38
  entry.items.push(item);
@@ -133,7 +133,7 @@ function disable(botId, message) {
133
133
  let gitProbe = null;
134
134
  function gitAvailable() {
135
135
  gitProbe ??= new Promise((resolveProbe) => {
136
- execFile("git", ["--version"], { windowsHide: true }, (err) => resolveProbe(!err));
136
+ execFile("git", ["--version"], { windowsHide: true, timeout: 5_000 }, (err) => resolveProbe(!err));
137
137
  });
138
138
  return gitProbe;
139
139
  }
@@ -205,9 +205,10 @@ function gitEnv(shadow, cwd) {
205
205
  * "." pathspec in add/restore resolves relative to it. A hung git (index
206
206
  * lock, dead network filesystem) would otherwise jam the per-repo queue for
207
207
  * the whole session, so every call carries a hard timeout. */
208
- function runGit(args, cwd, env) {
208
+ function runGit(args, cwd, env, signal) {
209
209
  return new Promise((resolvePromise, rejectPromise) => {
210
- execFile("git", args, { cwd, env, windowsHide: true, encoding: "utf8", timeout: 120_000, maxBuffer: 16 * 1024 * 1024 }, (err, stdout, stderr) => {
210
+ signal?.throwIfAborted();
211
+ execFile("git", args, { cwd, env, signal, windowsHide: true, encoding: "utf8", timeout: 120_000, maxBuffer: 16 * 1024 * 1024 }, (err, stdout, stderr) => {
211
212
  if (err)
212
213
  rejectPromise(new Error(`git ${args[0]}: ${(stderr || err.message).trim().slice(0, 400)}`));
213
214
  else
@@ -219,9 +220,10 @@ function runGit(args, cwd, env) {
219
220
  * instead of `status --porcelain` emptiness on purpose — a nested repo with
220
221
  * a dirty work tree shows up in status forever while staging nothing, which
221
222
  * would either commit empty churn every turn or fail the commit outright. */
222
- function hasStagedChanges(cwd, env) {
223
+ function hasStagedChanges(cwd, env, signal) {
223
224
  return new Promise((resolvePromise, rejectPromise) => {
224
- execFile("git", ["diff", "--cached", "--quiet", "--ignore-submodules=dirty"], { cwd, env, windowsHide: true, encoding: "utf8", timeout: 120_000, maxBuffer: 16 * 1024 * 1024 }, (err, _stdout, stderr) => {
225
+ signal?.throwIfAborted();
226
+ execFile("git", ["diff", "--cached", "--quiet", "--ignore-submodules=dirty"], { cwd, env, signal, windowsHide: true, encoding: "utf8", timeout: 120_000, maxBuffer: 16 * 1024 * 1024 }, (err, _stdout, stderr) => {
225
227
  if (err === null)
226
228
  resolvePromise(false);
227
229
  else if (err.code === 1)
@@ -248,46 +250,49 @@ function serialize(key, fn) {
248
250
  * always resolves — it is filtered from listings and refused as a restore
249
251
  * target, because "restore to empty" on a user's own project folder would
250
252
  * delete their files. */
251
- async function ensureShadow(cwd, env, shadow) {
253
+ async function ensureShadow(cwd, env, shadow, signal) {
254
+ signal?.throwIfAborted();
252
255
  mkdirSync(shadow, { recursive: true, mode: 0o700 });
253
256
  writeFileSync(join(shadow, "gitconfig"), GITCONFIG, { mode: 0o600 });
254
257
  writeFileSync(join(shadow, "gitconfig_empty"), "", { mode: 0o600 });
255
258
  if (!existsSync(join(shadow, ".git", "HEAD"))) {
256
259
  // --template= keeps the user's init.templateDir hooks/config out
257
- await runGit(["init", "--initial-branch=main", "--template="], cwd, env);
260
+ await runGit(["init", "--initial-branch=main", "--template="], cwd, env, signal);
258
261
  }
259
262
  mkdirSync(join(shadow, ".git", "info"), { recursive: true });
260
263
  writeFileSync(join(shadow, ".git", "info", "exclude"), EXCLUDES);
261
264
  try {
262
- await runGit(["rev-parse", "--verify", "HEAD"], cwd, env);
265
+ await runGit(["rev-parse", "--verify", "HEAD"], cwd, env, signal);
263
266
  }
264
267
  catch {
265
268
  // brand-new repo (or a crash between init and first commit)
266
- await runGit(["commit", "--no-verify", "--allow-empty", "-m", "checkpoint base"], cwd, env);
269
+ await runGit(["commit", "--no-verify", "--allow-empty", "-m", "checkpoint base"], cwd, env, signal);
267
270
  }
268
271
  }
269
272
  /** Stage everything and commit if anything actually changed. `complete`
270
273
  * records whether every path was indexable: ordinary snapshots may keep the
271
274
  * useful subset, but restore must not delete files its safety point missed. */
272
- async function commitAll(cwd, env, label) {
275
+ async function commitAll(cwd, env, label, signal) {
273
276
  // --ignore-errors skips files git cannot index (unreadable, FIFOs) instead
274
277
  // of aborting — but still exits 1 when it skipped an unreadable file, so
275
278
  // the exit code is retained even though a partial snapshot remains useful.
276
279
  // Restore treats an incomplete safety point as a hard stop before touching
277
280
  // the work tree.
278
281
  let complete = true;
279
- await runGit(["add", "-A", "--ignore-errors", "."], cwd, env).catch(() => {
282
+ await runGit(["add", "-A", "--ignore-errors", "."], cwd, env, signal).catch(() => {
280
283
  complete = false;
281
284
  });
282
- if (await hasStagedChanges(cwd, env)) {
283
- await runGit(["commit", "--no-verify", "-m", label], cwd, env);
285
+ if (await hasStagedChanges(cwd, env, signal)) {
286
+ await runGit(["commit", "--no-verify", "-m", label], cwd, env, signal);
284
287
  }
285
- return { hash: (await runGit(["rev-parse", "HEAD"], cwd, env)).trim(), complete };
288
+ return { hash: (await runGit(["rev-parse", "HEAD"], cwd, env, signal)).trim(), complete };
286
289
  }
287
290
  /** Snapshot the folder. Returns the checkpoint hash, or null when the
288
291
  * feature is off for this bot, git is missing, or the folder is refused.
289
292
  * Never throws — this is called fire-and-forget on the turn path. */
290
- export async function snapshot(botId, cwd, label) {
293
+ export async function snapshot(botId, cwd, label, signal) {
294
+ if (signal?.aborted)
295
+ return null;
291
296
  if (disabledBots.has(botId))
292
297
  return null;
293
298
  if (!(await gitAvailable()))
@@ -299,12 +304,51 @@ export async function snapshot(botId, cwd, label) {
299
304
  const shadow = shadowDir(botId, worktree);
300
305
  return await serialize(shadow, async () => {
301
306
  const env = gitEnv(shadow, worktree);
302
- await ensureShadow(worktree, env, shadow);
303
- return (await commitAll(worktree, env, label)).hash;
307
+ await ensureShadow(worktree, env, shadow, signal);
308
+ return (await commitAll(worktree, env, label, signal)).hash;
304
309
  });
305
310
  }
306
311
  catch (e) {
307
- disable(botId, e instanceof Error ? e.message : String(e));
312
+ if (!signal?.aborted)
313
+ disable(botId, e instanceof Error ? e.message : String(e));
314
+ return null;
315
+ }
316
+ }
317
+ /** Paths that differ between two checkpoints of the same bot+folder, from
318
+ * `git diff --name-status` against the shadow repo. Null when the hashes
319
+ * are equal, the folder is refused or checkpoints are off, or git fails —
320
+ * the digest then simply omits its file list. Never throws (turn path). */
321
+ export async function diffStat(botId, cwd, fromHash, toHash, signal) {
322
+ if (signal?.aborted)
323
+ return null;
324
+ if (fromHash === toHash)
325
+ return null;
326
+ if (disabledBots.has(botId))
327
+ return null;
328
+ if (!(await gitAvailable()))
329
+ return null;
330
+ if (refusalReason(cwd) !== null)
331
+ return null;
332
+ try {
333
+ const worktree = realpathSync(resolve(cwd));
334
+ const shadow = shadowDir(botId, worktree);
335
+ const env = gitEnv(shadow, worktree);
336
+ const out = await serialize(shadow, () => runGit(["diff", "--name-status", "--no-renames", "-z", fromHash, toHash], worktree, env, signal));
337
+ const diff = { changed: [], added: [], deleted: [] };
338
+ const fields = out.split("\0").filter((field) => field.length > 0);
339
+ for (let i = 0; i + 1 < fields.length; i += 2) {
340
+ const status = fields[i];
341
+ const path = fields[i + 1];
342
+ if (status.startsWith("A"))
343
+ diff.added.push(path);
344
+ else if (status.startsWith("D"))
345
+ diff.deleted.push(path);
346
+ else
347
+ diff.changed.push(path);
348
+ }
349
+ return diff;
350
+ }
351
+ catch {
308
352
  return null;
309
353
  }
310
354
  }
@@ -9,10 +9,34 @@ export class SetupCancelled extends Error {
9
9
  }
10
10
  }
11
11
  function displayText(value, multiline = false) {
12
- const plain = stripVTControlCharacters(value);
12
+ // OSC 8 hyperlinks slip past stripVTControlCharacters on supported Node
13
+ // versions whenever the URI carries a character RFC 3986 allows unencoded
14
+ // ((, ), +, and friends): the failed match strands most of the URI and its
15
+ // BEL as printable text (nodejs/node#64313). Strip whole OSC sequences
16
+ // here, from the raw value while their openers are still intact; the
17
+ // label between a link's opening and closing sequences survives, and an
18
+ // unterminated sequence is removed outright. Both introducer forms
19
+ // match: ESC ] and the lone C1 byte, which carries no closing bracket.
20
+ // eslint-disable-next-line no-control-regex
21
+ const withoutOsc = value.replace(/(?:\u001b\]|\u009d)[^\u0007\u001b\u009c]*(?:\u0007|\u001b\\|\u009c)?/g, "");
22
+ const plain = stripVTControlCharacters(withoutOsc);
13
23
  // Provider-supplied labels must not issue terminal control commands.
24
+ // stripVTControlCharacters glues a trailing BEL onto the escape sequence
25
+ // before it on some Node versions and leaves it bare on others, so the
26
+ // fallback below must be version-independent: controls that render no
27
+ // glyph (BEL, DEL, C1, …) are removed outright — padding them would
28
+ // counterfeit whitespace — while tab and carriage return collapse to one
29
+ // space and a newline survives only in multiline mode.
14
30
  // eslint-disable-next-line no-control-regex
15
- return plain.replace(/[\u0000-\u001f\u007f-\u009f]/g, (character) => character === "\n" && multiline ? "\n" : " ");
31
+ const withoutControls = plain.replace(/[\u0000-\u001f\u007f-\u009f]/g, (character) => {
32
+ if (character === "\n")
33
+ return multiline ? "\n" : " ";
34
+ if (character === "\t" || character === "\r")
35
+ return " ";
36
+ return "";
37
+ });
38
+ // Log lines must not end in trailing spaces; question prompts keep spacing like "Key: ".
39
+ return multiline ? withoutControls.replace(/[ \t]+(?=\n|$)/g, "") : withoutControls;
16
40
  }
17
41
  /** A line-based fallback with no cursor/color output. Readline has no output
18
42
  * stream for secrets, so even pasted input cannot be echoed. */