openmausbot 0.1.91 → 0.1.93

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 (164) hide show
  1. package/dist/assets/DesktopViewer-CKNR3SEW.js +5 -0
  2. package/dist/assets/{abnfDiagram-O67JEVCF-yP-w8pRq.js → abnfDiagram-O67JEVCF-Ckrm1I1X.js} +1 -1
  3. package/dist/assets/{arc-DsivfZEh.js → arc-BBdBuCgB.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CILw7lzT.js → architectureDiagram-NJMV4G6O-CDdA3Hjs.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-DEQUV3HF.js → blockDiagram-BEXU5L5S-AUrsx1YR.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-lSXCZoKA.js → c4Diagram-YGBWAQC7-BEqQ0Unh.js} +1 -1
  7. package/dist/assets/channel-BU94VpxE.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-QCD5bgWP.js → chunk-2Q5K7J3B-CEEuvd4C.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-C5-iZJuL.js → chunk-3FUC2YCW-DyANCiHl.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-cxsawMpY.js → chunk-5VM5RSS4-Dx_Slpw9.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-DV8tV8H7.js → chunk-GTNCS2PH-Dl7h7iiD.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-CJFZyfbW.js → chunk-GWA4HPMP-BLx07SPb.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-BigbMXco.js → chunk-JWPE2WC7-BMxDj3n7.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-oVxTfpDy.js → chunk-NETBCI7D-CN66PT4H.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-CH4BiroW.js → chunk-WEXAMYUT-6HwhsDKw.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-KEDa6gax.js → chunk-XXDRQBXY-gWnLt7SN.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-BiPjfqVv.js → classDiagram-v2-NBCMYWYE-CMS5l2ea.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-Caxew3OY.js → cose-bilkent-JH36ORCC-CV7sQwIG.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-B_Pui369.js → cynefin-OW5HDTMX-CSyl-7vl.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-X64ydIhP.js → cynefinDiagram-VND7K2PF-BCLKL6az.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-TqYCI1E1.js → dagre-6A5THRUB-DiaZs6S1.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-TeOt3rAQ.js → diagram-22UHCM2B-BeTtmt3I.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-D70BLhDA.js → diagram-3UASUU5V-CsdBG-QQ.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-BbQDLwUC.js → diagram-ATOU4E4O-BczvdbR1.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-ChNJZ1u-.js → diagram-CDSNMT55-DHTTTMhi.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-DMBl2APa.js → diagram-MLGK6HIB-RTjHyd8z.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-CEqDchOp.js → diagram-MPIPVDR6-BDenHEH_.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-BVDKRp_-.js → ebnfDiagram-ZINNZB2B-BSiRSbtL.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-CRkofO9B.js → elk-276RUBZZ-BCAElss-.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-DruOMh5e.js → erDiagram-OPXOYQCR-C4_1ZPJe.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-8GMQeB7t.js → flowDiagram-KWPJA3E3-Bcb4Z75S.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-ORE9jcsM.js → ganttDiagram-FUAMR5RP-BiPOzxzb.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-BmsedsWh.js → gitGraphDiagram-X574FWY7-B1wtF3ZS.js} +1 -1
  34. package/dist/assets/index-Biji-bfL.css +1 -0
  35. package/dist/assets/{index-DKNEM8Q6.js → index-BkYwilkG.js} +1 -1
  36. package/dist/assets/index-DZlhoES0.js +597 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-E_p7AooJ.js → infoDiagram-VRGFBTTK-A19No71U.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-Y-B2mKda.js → ishikawaDiagram-OU5B5YK6-BQsqJ_cV.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-eA8RELcK.js → journeyDiagram-ZHPQQLJL-DAO0pVan.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-DqIL2e8e.js → kanban-definition-PNTS6WVX-NepmKEe2.js} +1 -1
  41. package/dist/assets/{linear-yC_29R6e.js → linear-bq1PTDTW.js} +1 -1
  42. package/dist/assets/{mermaid.core-VhEMkSND.js → mermaid.core-s4LTbQle.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-oJ7kyQZy.js → mindmap-definition-NLK3R4M7-LtdbgHFH.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-BmfYcY0O.js → pegDiagram-GJSIUBJH-tdbaCxiK.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-DKvgP7w9.js → pieDiagram-5QR66LMP-DvkbpW8l.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-BEKL3W0L.js → quadrantDiagram-O4NWA36T-Do4KaH-E.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-BEsiqR5s.js → railroadDiagram-XR7U4H2S-BQWULOAs.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-DWBcWvAP.js → requirementDiagram-PLB6GJNP-w9kei1FU.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-rpZ2_O4s.js → sankeyDiagram-IPEJSGJF-D2gRy0e8.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-B99HQdJR.js → sequenceDiagram-PO4LG4MO-a2sO5_Eo.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-BjN0qT5T.js → sizeCapture-INFHLROL-CiPJmFEB.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-BbsVgGsC.js → stateDiagram-v2-GCMORJYK-DAkUUlL7.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DG3gKkeR.js → swimlanes-2SLR337P-DANf2R8Y.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-CdHxG8aM.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-BTHOdtF_.js → timeline-definition-EJHVYXUP-BKpQmzrT.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-acLSLFwc.js → usecaseDiagram-POWQR4AR-BtI7qLN-.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-C5JzNX9G.js → vennDiagram-UO4OBE2U-gXJXrAG_.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-DL1-PrR_.js → wardleyDiagram-VNRHLVJA-GU8Po5-n.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-Bvr1477p.js → xychartDiagram-PMCCYNJV-BF4J30g6.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist/novnc-NOTICE.txt +118 -0
  62. package/dist-server/cloud-home-start.js +112 -14
  63. package/dist-server/companion/src/routes.js +24 -1
  64. package/dist-server/container-mcp.js +201 -16
  65. package/dist-server/drivers/agents-proxy.js +7 -4
  66. package/dist-server/enterprise/server/index.js +5 -1
  67. package/dist-server/index.js +10698 -4737
  68. package/dist-server/local-computer-proxy.js +133 -8
  69. package/dist-server/mcp-gate.js +16 -6
  70. package/dist-server/model-catalog/models-dev.snapshot.json +7299 -0
  71. package/dist-server/openmausbot.js +4656 -1258
  72. package/dist-server/pair-cli.js +4656 -1258
  73. package/dist-server/server/boat.js +5 -5
  74. package/dist-server/server/browser-engine.js +10 -1
  75. package/dist-server/server/browser-live.js +46 -2
  76. package/dist-server/server/browser-runtime.js +74 -8
  77. package/dist-server/server/browser-tool-shape.js +6 -4
  78. package/dist-server/server/chief-of-staff.js +1 -1
  79. package/dist-server/server/cloud-home-start.js +118 -26
  80. package/dist-server/server/cloud-home.js +39 -3
  81. package/dist-server/server/cloud-lending.js +74 -10
  82. package/dist-server/server/cloud-owner.js +196 -0
  83. package/dist-server/server/cloud-secrets-boot.js +6 -0
  84. package/dist-server/server/cloud-secrets.js +65 -0
  85. package/dist-server/server/computer-control.js +6 -0
  86. package/dist-server/server/config.js +187 -14
  87. package/dist-server/server/container-computer.js +46 -34
  88. package/dist-server/server/desktop-viewer-targets.js +26 -0
  89. package/dist-server/server/digest.js +12 -2
  90. package/dist-server/server/drivers/acp/core.js +339 -33
  91. package/dist-server/server/drivers/acp/cursor.js +13 -1
  92. package/dist-server/server/drivers/acp/opencode-go.js +614 -85
  93. package/dist-server/server/drivers/acp/quiet-status.js +257 -0
  94. package/dist-server/server/drivers/acp/qwen.js +18 -0
  95. package/dist-server/server/drivers/agents-catalog.js +6 -6
  96. package/dist-server/server/drivers/antigravity-acp.js +69 -11
  97. package/dist-server/server/drivers/antigravity-temp.js +360 -0
  98. package/dist-server/server/drivers/antigravity.js +21 -0
  99. package/dist-server/server/drivers/builtIn.js +2 -0
  100. package/dist-server/server/drivers/cerebras.js +108 -0
  101. package/dist-server/server/drivers/chat-mcp-tools.js +20 -3
  102. package/dist-server/server/drivers/chatgpt-plan-auth.js +481 -0
  103. package/dist-server/server/drivers/claude.js +329 -37
  104. package/dist-server/server/drivers/codex.js +228 -18
  105. package/dist-server/server/drivers/grok.js +2 -2
  106. package/dist-server/server/drivers/mistral.js +4 -3
  107. package/dist-server/server/drivers/openai-chat-protocol.js +4 -3
  108. package/dist-server/server/drivers/openai-chat.js +66 -4
  109. package/dist-server/server/drivers/openai-compat.js +73 -23
  110. package/dist-server/server/drivers/prompt-split.js +2 -0
  111. package/dist-server/server/engine-install.js +46 -12
  112. package/dist-server/server/env-path.js +21 -5
  113. package/dist-server/server/harness/registry.js +2 -2
  114. package/dist-server/server/index.js +1545 -238
  115. package/dist-server/server/lending-memory.js +479 -0
  116. package/dist-server/server/local-vm-idle.js +24 -6
  117. package/dist-server/server/local-vm-stop-reason.js +26 -0
  118. package/dist-server/server/managed-desktop.js +3 -0
  119. package/dist-server/server/mcp-bridge.js +5 -3
  120. package/dist-server/server/mcp-http.js +6 -3
  121. package/dist-server/server/mcp-oauth-discovery.js +133 -0
  122. package/dist-server/server/mcp-oauth-store.js +70 -0
  123. package/dist-server/server/mcp-oauth.js +550 -0
  124. package/dist-server/server/mcp-probe.js +14 -1
  125. package/dist-server/server/mcp-registry.js +43 -0
  126. package/dist-server/server/memory-upkeep.js +94 -84
  127. package/dist-server/server/model-catalog/catalog.js +374 -0
  128. package/dist-server/server/model-catalog/presets.js +176 -0
  129. package/dist-server/server/model-catalog/trim.js +219 -0
  130. package/dist-server/server/provider-key-check.js +5 -2
  131. package/dist-server/server/recent-work.js +1 -1
  132. package/dist-server/server/request-auth.js +23 -0
  133. package/dist-server/server/routes/antigravity-leftovers.js +45 -0
  134. package/dist-server/server/routes/bot-memory.js +31 -6
  135. package/dist-server/server/routes/desktop-viewer.js +238 -0
  136. package/dist-server/server/routine-requests.js +1 -1
  137. package/dist-server/server/routines.js +6 -3
  138. package/dist-server/server/sessions.js +56 -9
  139. package/dist-server/server/skills.js +25 -10
  140. package/dist-server/server/store.js +116 -122
  141. package/dist-server/server/thread-events.js +2 -0
  142. package/dist-server/server/thread-starters.js +33 -0
  143. package/dist-server/server/thread-title.js +46 -0
  144. package/dist-server/server/tts/fish.js +4 -5
  145. package/dist-server/server/tts/index.js +5 -3
  146. package/dist-server/server/vps-computer.js +80 -13
  147. package/dist-server/server/webhooks.js +18 -1
  148. package/dist-server/server/workspace-backup-policy.js +3 -0
  149. package/dist-server/server/workspace-backup.js +25 -0
  150. package/dist-server/server/workspace.js +66 -16
  151. package/dist-server/shared/approval-mode.js +1 -1
  152. package/dist-server/shared/redact.js +1 -0
  153. package/dist-server/shared/tool-name.js +22 -0
  154. package/dist-server/shared/workspace-backup-client.js +1 -0
  155. package/dist-server/src/lib/citations.js +177 -0
  156. package/dist-server/src/lib/composer-attachments.js +7 -1
  157. package/dist-server/vps-container-mcp.js +202 -16
  158. package/dist-server/workspace-backup.worker.js +87 -9
  159. package/enterprise/server/index.js +5 -1
  160. package/package.json +1 -1
  161. package/dist/assets/channel-DrfGMu7X.js +0 -1
  162. package/dist/assets/index-BlnCEcU6.js +0 -588
  163. package/dist/assets/index-DVxjOvID.css +0 -1
  164. package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.js +0 -8
@@ -30,6 +30,17 @@ const usageFrom = (usage) => usage
30
30
  const asError = (value) => value instanceof Error ? value : new Error(String(value));
31
31
  class UnsupportedChatToolsError extends Error {
32
32
  }
33
+ const NUDGE_ANNOUNCED_ACTION = "You said what you would do next but called no tool. Do it now with your tools, or reply with your final answer if nothing is left to do.";
34
+ /** A short reply that only announces a next step, with nothing to answer. */
35
+ export function announcesAction(text) {
36
+ const reply = text.trim();
37
+ if (!reply || reply.length > 400 || reply.includes("?"))
38
+ return false;
39
+ return /(?:[:\u2014\u2013]|\.\.\.|\u2026)$/.test(reply) ||
40
+ /^(?:checking|let me|i'll|i will|i'm going to|i am going to|opening|pulling|looking|searching|navigating|reading|fetching|loading|now (?:i'll|let me|opening|checking|pulling|reading))\b/i.test(reply);
41
+ }
42
+ const TEXT_ONLY_ATTACHMENT_NOTE = "[The person attached image(s), but this model cannot see images. Say so if the request depends on them.]";
43
+ const TEXT_ONLY_SCREENSHOT_NOTE = "[Screenshot not shown: this model cannot see images. Read the page with a snapshot or a text tool instead.]";
33
44
  function rejectsToolsParameter(status, body) {
34
45
  if (status !== 400 && status !== 422)
35
46
  return false;
@@ -48,11 +59,32 @@ function rejectsToolsParameter(status, body) {
48
59
  /\b(?:tools|tool calling|function calling) (?:is|are) not supported\b/i.test(message) ||
49
60
  /^(?:unsupported|unknown|unrecognized) (?:parameter|field):?\s*['"]?tools['"]?[.!]?$/i.test(message.trim()));
50
61
  }
62
+ class UnsupportedReasoningReplayError extends Error {
63
+ }
64
+ /** DeepSeek-style providers need `reasoning_content` echoed on a tool-call
65
+ * assistant message; strict endpoints such as Groq reject the property. */
66
+ function rejectsReasoningReplay(status, body) {
67
+ if (status !== 400 && status !== 422)
68
+ return false;
69
+ let envelope;
70
+ try {
71
+ envelope = object(JSON.parse(body));
72
+ }
73
+ catch {
74
+ return false;
75
+ }
76
+ const message = object(envelope?.error)?.message ?? envelope?.error;
77
+ return typeof message === "string" && /\breasoning_content\b/.test(message) &&
78
+ /\b(?:unsupported|not supported|unknown|unrecognized|unexpected|not permitted|not allowed)\b/i.test(message);
79
+ }
51
80
  /** Shared runtime for the three providers that speak OpenAI chat completions. */
52
81
  export function createOpenAIChatRuntime(options) {
53
82
  const { input } = options;
54
83
  const listeners = new Set();
55
84
  const active = new Map();
85
+ /** Models whose endpoint rejected an echoed `reasoning_content`. In memory
86
+ * only: later rounds and turns omit the field instead of failing again. */
87
+ const reasoningReplayRejected = new Set();
56
88
  const emit = (event) => {
57
89
  for (const listener of Array.from(listeners))
58
90
  listener(event);
@@ -95,6 +127,9 @@ export function createOpenAIChatRuntime(options) {
95
127
  const message = `${options.httpErrorLabel} HTTP ${response.status}${body ? `: ${body.slice(0, 200)}` : ""}`;
96
128
  if (rejectsToolsParameter(response.status, body))
97
129
  throw new UnsupportedChatToolsError(message);
130
+ if (messages.some((entry) => entry.reasoning_content !== undefined) && rejectsReasoningReplay(response.status, body)) {
131
+ throw new UnsupportedReasoningReplayError(message);
132
+ }
98
133
  throw new Error(message);
99
134
  }
100
135
  if (!stream || response.headers.get("content-type")?.includes("application/json")) {
@@ -258,6 +293,7 @@ export function createOpenAIChatRuntime(options) {
258
293
  clearTimeout(idleTimer);
259
294
  }
260
295
  };
296
+ const seesImages = (turn) => options.imageInput?.(turn.model || options.models().default) ?? true;
261
297
  const messagesFor = (turn) => {
262
298
  // The system message is the head of the resent prefix, so only the
263
299
  // stable half belongs there: a volatile edit must not re-price the
@@ -278,7 +314,9 @@ export function createOpenAIChatRuntime(options) {
278
314
  role: message.role,
279
315
  content: message.text,
280
316
  })),
281
- { role: "user", content: options.computerUse ? chatUserContent(userTurn) : userTurn.text },
317
+ { role: "user", content: !options.computerUse ? userTurn.text
318
+ : seesImages(turn) ? chatUserContent(userTurn)
319
+ : userTurn.images?.length ? `${userTurn.text}\n\n${TEXT_ONLY_ATTACHMENT_NOTE}` : userTurn.text },
282
320
  ];
283
321
  };
284
322
  const sendTurn = async (turn) => {
@@ -357,6 +395,7 @@ export function createOpenAIChatRuntime(options) {
357
395
  let toolFailed = false;
358
396
  const denials = [];
359
397
  const seenCalls = new Set();
398
+ let nudged = false;
360
399
  try {
361
400
  tools = await mountChatTools(options.tools === false ? undefined : turn.integrations, abort.signal, options.computerUse);
362
401
  let optionalQuestionOnly = options.tools !== false && tools.definitions.length === 0;
@@ -377,7 +416,7 @@ export function createOpenAIChatRuntime(options) {
377
416
  const parked = turnEntry.asides.splice(0);
378
417
  if (parked.length)
379
418
  messages.push({ role: "user", content: parked.join("\n\n") });
380
- native("out", options.nativeLog.outgoing(turn, messages, model));
419
+ native("out", options.nativeLog.outgoing(turn, messages, model, tools.definitions));
381
420
  let attempt = 0;
382
421
  let completion;
383
422
  for (;;) {
@@ -419,6 +458,16 @@ export function createOpenAIChatRuntime(options) {
419
458
  catch (value) {
420
459
  const error = asError(value);
421
460
  const verdict = classifyError(error);
461
+ // A schema rejection of the echoed reasoning executed nothing
462
+ // upstream, so the same transcript without that one property
463
+ // repeats no operation. The error needs the field in the
464
+ // request, so the stripped resend cannot loop.
465
+ if (error instanceof UnsupportedReasoningReplayError && !streamed && !abort.signal.aborted) {
466
+ reasoningReplayRejected.add(model);
467
+ for (const message of messages)
468
+ delete message.reasoning_content;
469
+ continue;
470
+ }
422
471
  // Only our optional question changed a previously plain request.
423
472
  // A structured parameter rejection has executed nothing; never
424
473
  // downgrade mounted tools, streamed output or a handled call.
@@ -456,6 +505,11 @@ export function createOpenAIChatRuntime(options) {
456
505
  if (completion.finishReason && completion.finishReason !== "stop") {
457
506
  throw new ChatProtocolError(`provider did not finish the response (${completion.finishReason})`);
458
507
  }
508
+ if (options.nudgeAnnouncedAction && !nudged && tools.definitions.length && announcesAction(completion.text)) {
509
+ nudged = true;
510
+ messages.push({ role: "assistant", content: completion.text }, { role: "user", content: NUDGE_ANNOUNCED_ACTION });
511
+ continue;
512
+ }
459
513
  if (toolFailed) {
460
514
  stopReason = "tool_error";
461
515
  throw new ChatProtocolError("One or more tool operations failed or were denied. See the tool results; the final response is not an execution receipt.");
@@ -474,7 +528,7 @@ export function createOpenAIChatRuntime(options) {
474
528
  if (seenCalls.size > MAX_CHAT_TOOL_CALLS)
475
529
  throw new ChatProtocolError("tool-call limit reached");
476
530
  messages.push({ role: "assistant", content: completion.text || null, tool_calls: completion.toolCalls,
477
- ...(completion.protocolReasoning ? { reasoning_content: completion.protocolReasoning } : {}),
531
+ ...(completion.protocolReasoning && !reasoningReplayRejected.has(model) ? { [options.reasoningReplayField ?? "reasoning_content"]: completion.protocolReasoning } : {}),
478
532
  ...(completion.protocolReasoningDetails.length ? { reasoning_details: completion.protocolReasoningDetails } : {}),
479
533
  });
480
534
  const screenshotParts = [];
@@ -539,6 +593,10 @@ export function createOpenAIChatRuntime(options) {
539
593
  started = true;
540
594
  if (allowed) {
541
595
  result = await tools.execute(call.function.name, args, abort.signal);
596
+ if (result.images?.length && !seesImages(turn)) {
597
+ const { images: _dropped, ...rest } = result;
598
+ result = { ...rest, text: `${rest.text}\n${TEXT_ONLY_SCREENSHOT_NOTE}` };
599
+ }
542
600
  if (result.images?.length) {
543
601
  try {
544
602
  assertImageTransport(options.apiUrl);
@@ -637,7 +695,11 @@ export function createOpenAIChatRuntime(options) {
637
695
  sessionModelSwitch: "in-session", customMcp: options.tools !== false, agentsMcp: options.tools !== false, composioMcp: options.tools !== false,
638
696
  // The runtime owns the whole tool loop, so it can always take a
639
697
  // user message mid-turn: park it, deliver before the next completion.
640
- queueing: true },
698
+ queueing: true,
699
+ // No shell and no file tool on the host at all: only MCP tools, each
700
+ // call a card in Ask (the Boat's `exec` runs on the Boat). A guest's
701
+ // turn is as confined as it gets.
702
+ guestTurns: "confined" },
641
703
  sendTurn,
642
704
  interruptTurn: async (threadId, turnId) => {
643
705
  const turn = active.get(threadId);
@@ -15,6 +15,27 @@ const DEFAULT_MODELS = {
15
15
  { id: "llama-3.3-70b-versatile", label: "Llama 3.3 70B (Groq)", custom: true },
16
16
  ],
17
17
  };
18
+ const DEFAULT_KEY_ENV = "OPENAI_COMPAT_API_KEY";
19
+ /** A provider's own key (the built-in OpenAI and OpenRouter instances). An
20
+ * instance reading one of these never falls back to the workspace
21
+ * OpenAI-compatible key, URL, model or routing. */
22
+ const OWN_KEY_ENVS = new Set(["OMB_OPENAI_API_KEY", "OMB_OPENROUTER_API_KEY"]);
23
+ /** OpenAI's own API, seeded until its live catalog loads. */
24
+ const OPENAI_MODELS = {
25
+ default: "gpt-5",
26
+ options: [
27
+ { id: "gpt-5", label: "gpt-5" },
28
+ { id: "gpt-5-mini", label: "gpt-5-mini" },
29
+ ],
30
+ };
31
+ /** OpenAI's /models lists every model the key can reach: embeddings, speech,
32
+ * images, moderation and Responses-only models too. Only chat models belong
33
+ * in a chat picker. */
34
+ export function isOpenAIChatModel(id) {
35
+ if (!/^(gpt-|chatgpt-|o\d)/i.test(id))
36
+ return false;
37
+ return !/(audio|realtime|tts|transcribe|search|image|embedding|moderation|instruct|codex|computer-use|deep-research|-pro\b)/i.test(id);
38
+ }
18
39
  function isOpenRouterUrl(url) {
19
40
  try {
20
41
  const host = new URL(url).hostname.toLowerCase();
@@ -30,7 +51,11 @@ function decodeConfig(raw) {
30
51
  throw new Error("tools must be a boolean");
31
52
  if (config.managedModels !== undefined && (!Array.isArray(config.managedModels) || !config.managedModels.length || config.managedModels.some(model => typeof model !== "string" || !model.trim())))
32
53
  throw new Error("Invalid managed models.");
33
- const envUrl = process.env.OPENAI_COMPAT_URL;
54
+ const ownKey = typeof config.apiKeyEnv === "string" && OWN_KEY_ENVS.has(config.apiKeyEnv);
55
+ if (config.catalog !== undefined && config.catalog !== "openai")
56
+ throw new Error("catalog must be \"openai\"");
57
+ // Workspace defaults belong to the shared OpenAI-compatible connection only.
58
+ const envUrl = ownKey ? undefined : process.env.OPENAI_COMPAT_URL;
34
59
  return {
35
60
  ...(config.tools !== undefined ? { tools: config.tools } : {}),
36
61
  ...(config.managedModels ? { managedModels: config.managedModels } : {}),
@@ -38,55 +63,65 @@ function decodeConfig(raw) {
38
63
  .replace(/\/+$/, ""),
39
64
  apiKeyEnv: typeof config.apiKeyEnv === "string" && config.apiKeyEnv
40
65
  ? config.apiKeyEnv
41
- : "OPENAI_COMPAT_API_KEY",
66
+ : DEFAULT_KEY_ENV,
67
+ ...(config.catalog === "openai" ? { catalog: "openai" } : {}),
42
68
  key: typeof config.key === "string" && config.key ? config.key : undefined,
43
69
  model: typeof config.model === "string" && config.model
44
70
  ? config.model
45
- : process.env.OPENAI_COMPAT_MODEL || undefined,
71
+ : ownKey ? undefined : process.env.OPENAI_COMPAT_MODEL || undefined,
46
72
  // An explicit empty override disables inherited routing for an isolated
47
73
  // connection (CLI setup uses this). Absent still inherits the global pin.
48
74
  provider: typeof config.provider === "string"
49
75
  ? config.provider || undefined
50
- : process.env.OPENAI_COMPAT_PROVIDER || undefined,
76
+ : ownKey ? undefined : process.env.OPENAI_COMPAT_PROVIDER || undefined,
51
77
  };
52
78
  }
53
79
  export const OpenAICompatDriver = {
54
80
  driverKind: DRIVER_KIND,
55
81
  metadata: {
56
- displayName: "OpenAI-compatible (OpenRouter / Groq)",
82
+ displayName: "Other (OpenAI-compatible)",
57
83
  supportsMultipleInstances: true,
58
- access: "custom",
84
+ access: "api",
59
85
  },
60
86
  models: DEFAULT_MODELS,
87
+ // Nothing to install: the key and base URL are saved in the app. The old
88
+ // descriptor offered a config.json sentence as an "Open install in
89
+ // Terminal" command.
61
90
  install: {
62
91
  docsUrl: "https://openrouter.ai/keys",
63
- signInCommand: "add {\"openaiCompat\":{\"key\":\"sk-or-v1-…\"}} to ~/.openmausbot/config.json (or set OPENAI_COMPAT_API_KEY)",
64
- command: {
65
- darwin: "Get a free key at https://openrouter.ai/keys (or https://console.groq.com) then add it to ~/.openmausbot/config.json under openaiCompat.key",
66
- linux: "Get a free key at https://openrouter.ai/keys (or https://console.groq.com) then add it to ~/.openmausbot/config.json under openaiCompat.key",
67
- win32: "Get a free key at https://openrouter.ai/keys (or https://console.groq.com) then add it to %USERPROFILE%\\.openmausbot\\config.json under openaiCompat.key",
68
- },
92
+ settings: "connections",
93
+ signInCommand: "Save an OpenAI-compatible API key in Settings → API keys, or set OPENAI_COMPAT_API_KEY on the server.",
69
94
  },
70
95
  decodeConfig,
71
96
  defaultConfig: () => decodeConfig({}),
72
97
  async create(input) {
73
98
  const { config } = input;
99
+ // An instance that names its own key variable reads only that one: the
100
+ // workspace key (OPENAI_COMPAT_API_KEY) belongs to the workspace's
101
+ // endpoint and must not reach this instance's host.
102
+ const ownKeyVariable = config.apiKeyEnv !== DEFAULT_KEY_ENV;
74
103
  const apiKey = config.key ??
75
104
  input.environment[config.apiKeyEnv] ??
76
- input.environment.OPENAI_COMPAT_API_KEY ??
105
+ (ownKeyVariable ? undefined : input.environment[DEFAULT_KEY_ENV]) ??
77
106
  process.env[config.apiKeyEnv] ??
78
- process.env.OPENAI_COMPAT_API_KEY ??
107
+ (ownKeyVariable ? undefined : process.env[DEFAULT_KEY_ENV]) ??
79
108
  "";
109
+ // The default key and the built-in providers' keys are saved in
110
+ // Settings → API keys; any other variable is configured where it was written.
111
+ const missingKey = !ownKeyVariable || OWN_KEY_ENVS.has(config.apiKeyEnv)
112
+ ? "No API key — open Settings → API keys."
113
+ : `no API key — set ${config.apiKeyEnv} or add it to the instance config`;
114
+ const seeded = config.catalog === "openai" ? OPENAI_MODELS : DEFAULT_MODELS;
80
115
  let catalog = config.managedModels
81
116
  ? { default: config.managedModels[0], options: config.managedModels.map(id => ({ id, label: id })) }
82
117
  : config.model
83
118
  ? {
84
119
  default: config.model,
85
- options: DEFAULT_MODELS.options.some((model) => model.id === config.model)
86
- ? DEFAULT_MODELS.options
87
- : [{ id: config.model, label: config.model, custom: true }, ...DEFAULT_MODELS.options],
120
+ options: seeded.options.some((model) => model.id === config.model)
121
+ ? seeded.options
122
+ : [{ id: config.model, label: config.model, custom: true }, ...seeded.options],
88
123
  }
89
- : DEFAULT_MODELS;
124
+ : seeded;
90
125
  const fetchModels = async () => {
91
126
  if (config.managedModels)
92
127
  return;
@@ -100,7 +135,14 @@ export const OpenAICompatDriver = {
100
135
  if (!response.ok)
101
136
  return;
102
137
  const json = await response.json();
103
- const rows = Array.isArray(json) ? json : Array.isArray(json.data) ? json.data : [];
138
+ let rows = Array.isArray(json) ? json : Array.isArray(json.data) ? json.data : [];
139
+ // OpenAI lists its catalog unordered and mixed with non-chat models:
140
+ // keep chat models, newest first.
141
+ if (config.catalog === "openai") {
142
+ rows = rows
143
+ .filter((row) => typeof row.id === "string" && isOpenAIChatModel(row.id))
144
+ .sort((a, b) => (typeof b.created === "number" ? b.created : 0) - (typeof a.created === "number" ? a.created : 0));
145
+ }
104
146
  const seen = new Set();
105
147
  const options = [];
106
148
  for (const row of rows) {
@@ -111,7 +153,8 @@ export const OpenAICompatDriver = {
111
153
  options.push({
112
154
  id,
113
155
  label: typeof row.name === "string" && row.name.trim() ? row.name : id,
114
- custom: true,
156
+ // A provider's own catalog is its official list, not a custom model.
157
+ ...(config.catalog === "openai" ? {} : { custom: true }),
115
158
  });
116
159
  }
117
160
  if (!options.length)
@@ -146,15 +189,22 @@ export const OpenAICompatDriver = {
146
189
  : {}),
147
190
  }),
148
191
  httpErrorLabel: "upstream",
149
- missingKeyError: `no API key — set ${config.apiKeyEnv} or add it to the instance config`,
150
- unavailableReason: `no API key — set ${config.apiKeyEnv} or add it to the instance config`,
192
+ missingKeyError: missingKey,
193
+ unavailableReason: missingKey,
151
194
  timeoutMs: idleTimeoutMs(),
152
195
  reasoning: true,
153
196
  billing: "metered",
154
197
  includeUsageInCompleted: true,
155
198
  nativeLog: {
156
199
  source: "openai-compat.chat.completions",
157
- outgoing: (_turn, messages, model) => ({ model, messageCount: messages.length }),
200
+ // Tool names are the answer to "did the harness send them?" — the
201
+ // LiteLLM/proxy hop after this point is what drops tools silently,
202
+ // and until now the tee held no record either side could compare.
203
+ outgoing: (_turn, messages, model, tools) => ({
204
+ model,
205
+ messageCount: messages.length,
206
+ tools: tools.map(tool => tool.function.name),
207
+ }),
158
208
  incoming: ({ text, reasoning, usage }) => ({
159
209
  textLength: text.length,
160
210
  reasoningLength: reasoning.length,
@@ -57,6 +57,8 @@ export function readPromptSplitReceipt(scope, key) {
57
57
  stable: record.stable,
58
58
  volatile: record.volatile,
59
59
  ...(typeof record.turnsSinceFull === "number" ? { turnsSinceFull: record.turnsSinceFull } : {}),
60
+ ...(typeof record.lastUsed === "number" && record.lastUsed > 0 ? { lastUsed: record.lastUsed } : {}),
61
+ ...(typeof record.peakUsed === "number" && record.peakUsed > 0 ? { peakUsed: record.peakUsed } : {}),
60
62
  };
61
63
  }
62
64
  }
@@ -9,9 +9,10 @@ import { join } from "node:path";
9
9
  import { stripVTControlCharacters } from "node:util";
10
10
  import { DATA_DIR, stripWorkspaceCredentialEnv } from "./config.js";
11
11
  import { augmentedPath, findCliCandidates, registerPathDir, resetPathCache } from "./env-path.js";
12
- import { killCliTree, spawnCli } from "./procs.js";
12
+ import { execCli, killCliTree, spawnCli } from "./procs.js";
13
13
  const MAX_OUTPUT = 64 * 1024;
14
14
  const DEFAULT_TIMEOUT_MS = 10 * 60_000;
15
+ const PROBE_TIMEOUT_MS = 30_000;
15
16
  /** npm's global prefix for engines the app installs itself. */
16
17
  export function enginesPrefix(baseDir = DATA_DIR) {
17
18
  return join(baseDir, "tools", "npm");
@@ -68,21 +69,54 @@ async function installOnce(pkg, options) {
68
69
  stripWorkspaceCredentialEnv(env);
69
70
  // npm 11 skips a dependency's install script unless the package is named
70
71
  // here; the engines that need one (Claude Code) are exactly these.
71
- const args = ["install", "-g", "--prefix", prefix, "--loglevel=error", `--allow-scripts=${pkg}`, `${pkg}@latest`];
72
- const result = await runNpm(args, env, prefix, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
73
- if (result.code !== 0) {
74
- throw new Error(`npm could not install ${pkg} on this server.${tail(result.output)}`);
75
- }
72
+ // Engines like Codex ship their native binary as a platform optional
73
+ // dependency, so a user-level `omit=optional` must not leave it out.
74
+ const args = ["install", "-g", "--prefix", prefix, "--loglevel=error", "--include=optional", `--allow-scripts=${pkg}`, `${pkg}@latest`];
75
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
76
+ await runInstall(pkg, args, env, prefix, timeoutMs);
76
77
  // Whatever the boot registration did, the directory we just filled must
77
78
  // be on the engines' PATH from here on.
78
- registerPathDir(enginesBinDir(options.baseDir));
79
+ const binDir = enginesBinDir(options.baseDir);
80
+ registerPathDir(binDir);
79
81
  resetPathCache();
80
- if (options.cli) {
81
- const binDir = enginesBinDir(options.baseDir);
82
- if (!findCliCandidates(options.cli).some((path) => path.startsWith(binDir))) {
83
- throw new Error(`${pkg} installed, but it did not provide a \`${options.cli}\` command. Check the package name in this engine's install descriptor.`);
84
- }
82
+ if (!options.cli)
83
+ return;
84
+ const installed = findCliCandidates(options.cli).find((path) => path.startsWith(binDir));
85
+ if (!installed) {
86
+ throw new Error(`${pkg} installed, but it did not provide a \`${options.cli}\` command. Check the package name in this engine's install descriptor.`);
85
87
  }
88
+ // npm exits 0 even when it drops an optional dependency it failed to
89
+ // download, which leaves a launcher without its platform binary (#2064).
90
+ // A copy here shadows every other one on PATH, so prove it starts; a
91
+ // second run fetches what the first one dropped.
92
+ let failure = await probeCli(installed, env);
93
+ if (failure === null)
94
+ return;
95
+ await runInstall(pkg, args, env, prefix, timeoutMs);
96
+ failure = await probeCli(installed, env);
97
+ if (failure === null)
98
+ return;
99
+ throw new Error(`npm installed ${pkg}, but \`${options.cli}\` does not start: ${failure}\nnpm can leave out a platform package when its download fails, even after a retry. Check this server's network connection, then install again from Settings.`);
100
+ }
101
+ async function runInstall(pkg, args, env, prefix, timeoutMs) {
102
+ const result = await runNpm(args, env, prefix, timeoutMs);
103
+ if (result.code !== 0) {
104
+ throw new Error(`npm could not install ${pkg} on this server.${tail(result.output)}`);
105
+ }
106
+ }
107
+ /** Null when `<cli> --version` succeeds, else why it did not. A Node
108
+ * launcher that throws prints its source line first, so its `Error:` line
109
+ * is the one worth showing. */
110
+ function probeCli(cli, env) {
111
+ return new Promise((resolveProbe) => {
112
+ execCli(cli, ["--version"], { env: { ...env, PATH: augmentedPath() }, timeout: PROBE_TIMEOUT_MS, killSignal: "SIGKILL", maxBuffer: MAX_OUTPUT }, (error, stdout, stderr) => {
113
+ if (!error)
114
+ return resolveProbe(null);
115
+ const lines = stripVTControlCharacters(`${stderr ?? ""}\n${stdout}`).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
116
+ const said = lines.find((line) => /^\w*Error: /.test(line)) ?? lines[0] ?? error.message;
117
+ resolveProbe(said.slice(0, 400));
118
+ });
119
+ });
86
120
  }
87
121
  function tail(output) {
88
122
  const clean = stripVTControlCharacters(output).trim();
@@ -27,6 +27,15 @@ function nvmBinDirs() {
27
27
  return [];
28
28
  }
29
29
  }
30
+ /** A sealed test fixture finds CLIs only where it was granted them: its own
31
+ * PATH, OMB_EXTRA_PATH, app-managed dirs, and install dirs under its own
32
+ * temporary home. The machine-wide install dirs and the login shell's PATH
33
+ * would hand it this computer's real CLIs — a Homebrew `codex` made the
34
+ * fixture's ChatGPT plan engine "available" on a developer Mac (#2035).
35
+ * Only the verification launcher (scripts/control-omb.ts) sets it. */
36
+ function sealedFixture() {
37
+ return process.env.OMB_TEST_SEALED_PATH === "1";
38
+ }
30
39
  function knownDirs() {
31
40
  const home = homedir();
32
41
  return [
@@ -36,8 +45,11 @@ function knownDirs() {
36
45
  join(home, ".grok", "bin"), // x.ai installer
37
46
  join(home, ".opencode", "bin"), // opencode installer
38
47
  join(home, ".claude", "local"), // claude "local install"
39
- "/opt/homebrew/bin", // brew, Apple silicon
40
- "/usr/local/bin", // brew Intel / classic installs
48
+ // Machine-wide rather than under the (possibly temporary) home.
49
+ ...(sealedFixture() ? [] : [
50
+ "/opt/homebrew/bin", // brew, Apple silicon
51
+ "/usr/local/bin", // brew Intel / classic installs
52
+ ]),
41
53
  join(home, ".volta", "bin"),
42
54
  join(home, ".bun", "bin"),
43
55
  join(home, ".asdf", "shims"),
@@ -52,8 +64,9 @@ function knownDirs() {
52
64
  * invisible until it restarts, because Windows never pushes PATH changes
53
65
  * into a live process. Scanning the standard install locations recovers
54
66
  * those without a restart — `~/.grok/bin` (the x.ai installer) and
55
- * `%APPDATA%\npm` (global npm shims), plus `%LOCALAPPDATA%\agy\bin`, cover
56
- * every engine we ship an install command for. */
67
+ * `%APPDATA%\npm` (global npm shims), plus `%LOCALAPPDATA%\agy\bin` and
68
+ * `%LOCALAPPDATA%\cursor-agent`, cover every engine we ship an install
69
+ * command for. */
57
70
  function windowsKnownDirs() {
58
71
  const home = homedir();
59
72
  const appData = process.env.APPDATA ?? join(home, "AppData", "Roaming");
@@ -62,12 +75,15 @@ function windowsKnownDirs() {
62
75
  join(appData, "npm"), // npm -g shims: claude, codex
63
76
  join(home, ".grok", "bin"), // x.ai installer
64
77
  join(localAppData, "agy", "bin"), // Antigravity installer
78
+ // Cursor installer: cursor-agent.* and its `agent` copies (MOCA-272)
79
+ join(localAppData, "cursor-agent"),
65
80
  join(home, ".local", "bin"), // claude native installer
66
81
  join(home, ".claude", "local"),
67
82
  join(home, "bin"), // Factory droid installer (%USERPROFILE%\bin)
68
83
  join(home, ".bun", "bin"),
69
84
  join(home, ".deno", "bin"),
70
85
  join(home, "go", "bin"),
86
+ join(process.env.ProgramFiles ?? "C:\\Program Files", "Docker", "Docker", "resources", "bin"), // Docker Desktop installer
71
87
  ];
72
88
  }
73
89
  let cached = null;
@@ -115,7 +131,7 @@ export function augmentedPath() {
115
131
  // belt-and-braces: fold in the login shell's PATH once, in the
116
132
  // background — catches anything the known-dirs list doesn't (custom
117
133
  // rc exports). Never blocks a spawn; the next one benefits.
118
- if (!probed && !process.env.VITEST && process.platform !== "win32") {
134
+ if (!probed && !process.env.VITEST && !sealedFixture() && process.platform !== "win32") {
119
135
  probed = true;
120
136
  probeLoginShellPath();
121
137
  }
@@ -235,11 +235,11 @@ export class ProviderRegistry {
235
235
  install: withServerInstall(driver?.install, npmPresent),
236
236
  authentication: inst.startAuthentication
237
237
  ? {
238
- method: inst.getAuthentication && inst.completeAuthentication
238
+ method: inst.authenticationMethod ?? (inst.getAuthentication && inst.completeAuthentication
239
239
  ? "paste-code" // a link to open, then a code pasted back (Claude)
240
240
  : inst.getAuthentication
241
241
  ? "device-code" // a code to enter at the provider's page (Codex)
242
- : "browser", // a link and a callback URL (managed engines)
242
+ : "browser"), // a link and a callback URL (managed engines)
243
243
  // the browser may remove the stored sign-in to switch accounts
244
244
  signOut: inst.signOut !== undefined,
245
245
  }