openmausbot 0.1.85 → 0.1.87

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 (262) hide show
  1. package/README.md +2 -2
  2. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  3. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  4. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  5. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  6. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  7. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  8. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  9. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  10. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  11. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  12. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  13. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  14. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  15. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  16. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  17. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  18. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  19. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  20. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  21. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  22. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  23. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  24. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  25. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  26. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  27. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  28. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  29. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  30. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  31. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  32. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  33. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  34. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  35. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  36. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  37. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  38. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  39. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  40. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  41. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  42. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  44. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  45. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  46. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  47. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  48. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  49. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  50. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  51. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  52. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  53. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  54. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  55. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  56. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  57. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  58. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  59. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  60. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  61. package/dist/assets/abnfDiagram-O67JEVCF-BTiuL79F.js +1 -0
  62. package/dist/assets/arc-C88Blc2v.js +1 -0
  63. package/dist/assets/architectureDiagram-NJMV4G6O-BZsJIZrb.js +36 -0
  64. package/dist/assets/blockDiagram-BEXU5L5S-CVeMe9Ah.js +140 -0
  65. package/dist/assets/c4Diagram-YGBWAQC7-Cedk3Bhx.js +38 -0
  66. package/dist/assets/channel-taCNRlUU.js +1 -0
  67. package/dist/assets/chunk-2Q5K7J3B-D-VRqzTB.js +1 -0
  68. package/dist/assets/chunk-3FUC2YCW-BChov01Y.js +2 -0
  69. package/dist/assets/chunk-5VM5RSS4-C-TfwvHy.js +15 -0
  70. package/dist/assets/chunk-GTNCS2PH-Db7CGj5Y.js +88 -0
  71. package/dist/assets/chunk-GWA4HPMP-CNvIfqKT.js +1 -0
  72. package/dist/assets/chunk-JWPE2WC7-C_bEp5tq.js +1 -0
  73. package/dist/assets/chunk-NETBCI7D-Coi4BOls.js +1 -0
  74. package/dist/assets/chunk-WEXAMYUT-CzylEuL3.js +1 -0
  75. package/dist/assets/chunk-XXDRQBXY-m7p7ztaD.js +1 -0
  76. package/dist/assets/classDiagram-v2-NBCMYWYE-BMy6o9x2.js +217 -0
  77. package/dist/assets/cose-bilkent-JH36ORCC-BDQ__0_J.js +1 -0
  78. package/dist/assets/cynefin-OW5HDTMX-Djv4ghUb.js +166 -0
  79. package/dist/assets/cynefinDiagram-VND7K2PF-BULrlGYH.js +62 -0
  80. package/dist/assets/cytoscape.esm-BB4DxJjf.js +321 -0
  81. package/dist/assets/dagre-6A5THRUB-D-jrT_hU.js +4 -0
  82. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  83. package/dist/assets/diagram-22UHCM2B-p2UhQmfL.js +200 -0
  84. package/dist/assets/diagram-3UASUU5V-DHvGWZ7q.js +24 -0
  85. package/dist/assets/diagram-ATOU4E4O-ChhKDot-.js +3 -0
  86. package/dist/assets/diagram-CDSNMT55-B-T7A43R.js +30 -0
  87. package/dist/assets/diagram-MLGK6HIB-CJm8ENNY.js +24 -0
  88. package/dist/assets/diagram-MPIPVDR6-510tlza8.js +41 -0
  89. package/dist/assets/ebnfDiagram-ZINNZB2B-PkvRPHel.js +1 -0
  90. package/dist/assets/elk-276RUBZZ-BVBVPoX3.js +27 -0
  91. package/dist/assets/erDiagram-OPXOYQCR-BgYMVm7L.js +99 -0
  92. package/dist/assets/flowDiagram-KWPJA3E3-BGqsnc_3.js +213 -0
  93. package/dist/assets/ganttDiagram-FUAMR5RP-D_-UeReH.js +292 -0
  94. package/dist/assets/gitGraphDiagram-X574FWY7-D0uooONx.js +106 -0
  95. package/dist/assets/graph-D8UmR1vK.js +1 -0
  96. package/dist/assets/index-BwdMAQ6p.css +1 -0
  97. package/dist/assets/index-ByJennr7.js +581 -0
  98. package/dist/assets/{index-CTNNoqSJ.js → index-zRsJERGJ.js} +1 -1
  99. package/dist/assets/infoDiagram-VRGFBTTK-B9e1z77f.js +2 -0
  100. package/dist/assets/init-Gi6I4Gst.js +1 -0
  101. package/dist/assets/ishikawaDiagram-OU5B5YK6-BB4vNwvD.js +70 -0
  102. package/dist/assets/journeyDiagram-ZHPQQLJL-DpkosOTB.js +139 -0
  103. package/dist/assets/kanban-definition-PNTS6WVX-DbDP4kUu.js +89 -0
  104. package/dist/assets/linear-DBJGPmjT.js +1 -0
  105. package/dist/assets/mermaid.core-DWPRi-ue.js +311 -0
  106. package/dist/assets/mindmap-definition-NLK3R4M7-nszBbJmx.js +96 -0
  107. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  108. package/dist/assets/pegDiagram-GJSIUBJH-CWykJWu_.js +1 -0
  109. package/dist/assets/pieDiagram-5QR66LMP-g3Da-Vnv.js +39 -0
  110. package/dist/assets/quadrantDiagram-O4NWA36T-Wzrlpzgh.js +7 -0
  111. package/dist/assets/railroadDiagram-XR7U4H2S-moHTFMC0.js +1 -0
  112. package/dist/assets/requirementDiagram-PLB6GJNP-CwfXdFH8.js +84 -0
  113. package/dist/assets/sankeyDiagram-IPEJSGJF-3ZROYvnq.js +40 -0
  114. package/dist/assets/sequenceDiagram-PO4LG4MO-E1TBebP8.js +169 -0
  115. package/dist/assets/sizeCapture-INFHLROL-DHKEU5pu.js +1 -0
  116. package/dist/assets/stateDiagram-v2-GCMORJYK-BguEj09D.js +291 -0
  117. package/dist/assets/swimlanes-2SLR337P-C7IDYnhd.js +1 -0
  118. package/dist/assets/swimlanesDiagram-TC7HE7FX-BpEdoB2e.js +8 -0
  119. package/dist/assets/timeline-definition-EJHVYXUP-DCW-F5PW.js +120 -0
  120. package/dist/assets/usecaseDiagram-POWQR4AR-xBMFtWYV.js +370 -0
  121. package/dist/assets/vennDiagram-UO4OBE2U-B5nIkuH8.js +34 -0
  122. package/dist/assets/wardleyDiagram-VNRHLVJA-DYFAhdbr.js +78 -0
  123. package/dist/assets/xychartDiagram-PMCCYNJV-DeXwX6Cz.js +7 -0
  124. package/dist/index.html +2 -2
  125. package/dist-server/browser-proxy.js +2 -1
  126. package/dist-server/container-mcp.js +1047 -173
  127. package/dist-server/drivers/agents-proxy.js +16121 -1504
  128. package/dist-server/drivers/phone-proxy.js +33 -2
  129. package/dist-server/enterprise/LICENSE +32 -0
  130. package/dist-server/enterprise/server/index.js +5 -5
  131. package/dist-server/index.js +16134 -7446
  132. package/dist-server/local-computer-proxy.js +3 -3
  133. package/dist-server/local-computer.js +53 -17
  134. package/dist-server/mcp-server.js +3 -1
  135. package/dist-server/openmausbot.js +9353 -6124
  136. package/dist-server/pair-cli.js +9353 -6124
  137. package/dist-server/permission-proxy.js +29 -9
  138. package/dist-server/server/admin-activity.js +410 -0
  139. package/dist-server/server/attachments.js +55 -0
  140. package/dist-server/server/auto-approve.js +4 -3
  141. package/dist-server/server/auto-vm-claims.js +1 -0
  142. package/dist-server/server/bot-package.js +18 -292
  143. package/dist-server/server/bot-profile.js +5 -17
  144. package/dist-server/server/bot-visibility.js +462 -0
  145. package/dist-server/server/box-create-idempotency.js +3 -3
  146. package/dist-server/server/box.js +59 -56
  147. package/dist-server/server/browser-proxy.js +3 -2
  148. package/dist-server/server/browser-runtime.js +38 -2
  149. package/dist-server/server/channel-queue.js +2 -0
  150. package/dist-server/server/cli-prompts.js +24 -2
  151. package/dist-server/server/cli.js +81 -12
  152. package/dist-server/server/command-allowlist.js +154 -0
  153. package/dist-server/server/composio.js +49 -11
  154. package/dist-server/server/computer-wait.js +22 -9
  155. package/dist-server/server/config.js +133 -17
  156. package/dist-server/server/connector-verdict.js +141 -0
  157. package/dist-server/server/container-computer.js +1 -1
  158. package/dist-server/server/decision-log.js +209 -48
  159. package/dist-server/server/default-model-selection.js +29 -3
  160. package/dist-server/server/delegations.js +97 -44
  161. package/dist-server/server/drivers/acp/core.js +163 -33
  162. package/dist-server/server/drivers/acp/grok.js +4 -3
  163. package/dist-server/server/drivers/agents-call.js +1162 -0
  164. package/dist-server/server/drivers/agents-catalog.js +689 -0
  165. package/dist-server/server/drivers/agents-client.js +28 -0
  166. package/dist-server/server/drivers/agents-proxy.js +18 -1722
  167. package/dist-server/server/drivers/boxagent.js +174 -36
  168. package/dist-server/server/drivers/builtIn.js +2 -0
  169. package/dist-server/server/drivers/chat-box-tools.js +90 -0
  170. package/dist-server/server/drivers/chat-images.js +54 -0
  171. package/dist-server/server/drivers/chat-mcp-tools.js +55 -10
  172. package/dist-server/server/drivers/chat-tool-approval.js +70 -18
  173. package/dist-server/server/drivers/claude.js +28 -8
  174. package/dist-server/server/drivers/codex-instructions.js +48 -6
  175. package/dist-server/server/drivers/codex-mcp-names.js +44 -0
  176. package/dist-server/server/drivers/codex-tool-surface.js +20 -0
  177. package/dist-server/server/drivers/codex.js +121 -34
  178. package/dist-server/server/drivers/grok.js +2 -1
  179. package/dist-server/server/drivers/mistral.js +115 -0
  180. package/dist-server/server/drivers/openai-chat.js +155 -32
  181. package/dist-server/server/drivers/openai-compat.js +16 -8
  182. package/dist-server/server/drivers/permission-command.js +35 -0
  183. package/dist-server/server/drivers/phone-proxy.js +47 -2
  184. package/dist-server/server/drivers/pi.js +75 -4
  185. package/dist-server/server/drivers/prompt-split.js +87 -0
  186. package/dist-server/server/enterprise.js +97 -16
  187. package/dist-server/server/external-runtime.js +83 -0
  188. package/dist-server/server/fleet.js +1 -1
  189. package/dist-server/server/group-thread-usage.js +103 -0
  190. package/dist-server/server/guarded-requests.js +60 -0
  191. package/dist-server/server/harness/http.js +61 -0
  192. package/dist-server/server/harness/registry.js +1 -0
  193. package/dist-server/server/hosted-models.js +151 -0
  194. package/dist-server/server/hosted-slack.js +11 -0
  195. package/dist-server/server/index.js +3091 -658
  196. package/dist-server/server/local-computer.js +72 -23
  197. package/dist-server/server/managed-desktop.js +79 -14
  198. package/dist-server/server/managed-policy.js +153 -0
  199. package/dist-server/server/mcp-bridge.js +6 -6
  200. package/dist-server/server/message-db.js +28 -0
  201. package/dist-server/server/model-context-window.js +7 -0
  202. package/dist-server/server/model-prices.js +121 -0
  203. package/dist-server/server/new-bot-defaults.js +115 -0
  204. package/dist-server/server/notify.js +10 -1
  205. package/dist-server/server/org-library.js +1051 -0
  206. package/dist-server/server/package-export.js +627 -39
  207. package/dist-server/server/package-import.js +458 -0
  208. package/dist-server/server/package-parts.js +87 -0
  209. package/dist-server/server/peer-approval.js +16 -7
  210. package/dist-server/server/peer-roster.js +23 -2
  211. package/dist-server/server/presets.js +492 -0
  212. package/dist-server/server/provider-key-check.js +3 -2
  213. package/dist-server/server/published-teams.js +80 -0
  214. package/dist-server/server/recent-work.js +15 -0
  215. package/dist-server/server/request-auth.js +96 -0
  216. package/dist-server/server/room-handoffs.js +44 -14
  217. package/dist-server/server/routes/bot-presets.js +25 -0
  218. package/dist-server/server/routes/hosted-slack.js +20 -0
  219. package/dist-server/server/routes/table.js +14 -0
  220. package/dist-server/server/routine-requests.js +6 -3
  221. package/dist-server/server/routines.js +63 -1
  222. package/dist-server/server/section-context.js +10 -0
  223. package/dist-server/server/skills.js +34 -61
  224. package/dist-server/server/spend.js +111 -16
  225. package/dist-server/server/steer-queue.js +9 -3
  226. package/dist-server/server/store.js +550 -35
  227. package/dist-server/server/surface.js +10 -4
  228. package/dist-server/server/system-prompt.js +12 -4
  229. package/dist-server/server/team-backup.js +16 -3
  230. package/dist-server/server/team-computers.js +10 -0
  231. package/dist-server/server/team-library.js +6 -2
  232. package/dist-server/server/team-manifest.js +4 -3
  233. package/dist-server/server/team-setup-requests.js +16 -5
  234. package/dist-server/server/thread-events.js +17 -1
  235. package/dist-server/server/thread-starters.js +53 -0
  236. package/dist-server/server/turn-context.js +2 -0
  237. package/dist-server/server/turn-watchdog.js +12 -3
  238. package/dist-server/server/usage-ledger.js +43 -9
  239. package/dist-server/server/vps-computer.js +139 -51
  240. package/dist-server/server/vps-container-mcp.js +7 -0
  241. package/dist-server/server/vps-ssh.js +36 -7
  242. package/dist-server/server/workspace-backup-policy.js +21 -2
  243. package/dist-server/server/workspace-backup.js +7 -4
  244. package/dist-server/server/workspace.js +1 -1
  245. package/dist-server/shared/approval-mode.js +1 -1
  246. package/dist-server/shared/ask-question.js +188 -12
  247. package/dist-server/shared/bot-profile.js +15 -0
  248. package/dist-server/shared/command-allowlist.js +1 -0
  249. package/dist-server/shared/credential-request.js +1 -1
  250. package/dist-server/shared/mention-boundary.js +15 -0
  251. package/dist-server/shared/package-format.js +967 -0
  252. package/dist-server/shared/routines.js +7 -1
  253. package/dist-server/shared/skill-md.js +64 -0
  254. package/dist-server/shared/team-backup.js +9 -0
  255. package/dist-server/shared/wire.js +4 -0
  256. package/dist-server/src/lib/composer-attachments.js +16 -0
  257. package/dist-server/vps-container-mcp.js +1147 -179
  258. package/enterprise/README.md +33 -12
  259. package/enterprise/server/index.js +5 -5
  260. package/package.json +1 -1
  261. package/dist/assets/index-Ba9G44HI.js +0 -310
  262. package/dist/assets/index-CkRNp7wX.css +0 -1
@@ -70,19 +70,21 @@ function sameDriverFileIdentity(expected, actual) {
70
70
  }
71
71
  function decodeLegacyDescriptor(value, platform) {
72
72
  const supportedPlatform = legacyPlatform(platform);
73
- if (!supportedPlatform || !value || value.mode === "unavailable" || typeof value.mcpCommand !== "string") {
73
+ if (!supportedPlatform || !value ||
74
+ !(value.mode === "embedded" || (supportedPlatform === "darwin" && value.mode === "standalone")) ||
75
+ (Object.hasOwn(value, "status") && value.status !== "ready") ||
76
+ typeof value.socketPath !== "string" || !value.socketPath ||
77
+ typeof value.mcpCommand !== "string" || !value.mcpCommand.trim()) {
74
78
  return null;
75
79
  }
76
- if (value.mcpArgs !== undefined && !Array.isArray(value.mcpArgs))
80
+ if (!Array.isArray(value.mcpArgs) || value.mcpArgs[0] !== "mcp")
77
81
  return null;
78
- if (value.mcpEnv !== undefined &&
79
- (!value.mcpEnv || typeof value.mcpEnv !== "object" || Array.isArray(value.mcpEnv))) {
82
+ if (!value.mcpEnv || typeof value.mcpEnv !== "object" || Array.isArray(value.mcpEnv))
80
83
  return null;
81
- }
82
- const args = value.mcpArgs ?? ["mcp"];
84
+ const args = value.mcpArgs;
83
85
  if (!args.every((arg) => typeof arg === "string"))
84
86
  return null;
85
- const env = value.mcpEnv ?? {};
87
+ const env = value.mcpEnv;
86
88
  if (!Object.values(env).every((entry) => typeof entry === "string"))
87
89
  return null;
88
90
  return {
@@ -288,31 +290,78 @@ function validateLegacyDescriptorRuntime(descriptorFile, platform, { uid = proce
288
290
  return false;
289
291
  }
290
292
  }
293
+ /** An older fallback cannot override a present but unavailable, malformed, or
294
+ * unreadable descriptor at a more specific location. */
295
+ function firstPresentCuaDescriptor(candidates) {
296
+ for (const file of new Set(candidates)) {
297
+ try {
298
+ lstatSync(file);
299
+ return file;
300
+ }
301
+ catch (error) {
302
+ if (error.code !== "ENOENT")
303
+ return file;
304
+ }
305
+ }
306
+ return null;
307
+ }
291
308
  export function readCuaConnection({ platform = process.platform, userData = process.env.OMB_USER_DATA, home = homedir(), validateLinuxRuntime = validateLinuxDescriptorRuntime, validateLegacyRuntime = validateLegacyDescriptorRuntime, } = {}) {
292
309
  const candidates = userData ? [join(userData, "cua-connection.json")] : [];
293
- if (platform === "darwin") {
294
- // Legacy/dev fallback. Packaged Electron passes its exact userData path.
310
+ if (platform === "darwin" && !userData) {
311
+ // Legacy/dev fallback only when Electron did not provide its exact path.
295
312
  for (const directory of ["OpenMausBot", "openmausbot", "OpenGrokBot", "opengrokbot"]) {
296
313
  candidates.push(join(home, "Library", "Application Support", directory, "cua-connection.json"));
297
314
  }
298
315
  }
299
- for (const file of new Set(candidates)) {
300
- try {
316
+ const file = firstPresentCuaDescriptor(candidates);
317
+ if (!file)
318
+ return null;
319
+ try {
320
+ const raw = JSON.parse(readFileSync(file, "utf8"));
321
+ if (platform === "linux") {
322
+ const decoded = decodeLinuxDescriptor(raw);
323
+ if (decoded && validateLinuxRuntime(file, raw))
324
+ return decoded;
325
+ }
326
+ else {
327
+ const decoded = decodeLegacyDescriptor(raw, platform);
328
+ if (decoded && validateLegacyRuntime(file, platform))
329
+ return decoded;
330
+ }
331
+ }
332
+ catch {
333
+ // Missing, invalid, tampered, or stale descriptors are unavailable.
334
+ }
335
+ return null;
336
+ }
337
+ /** An unavailable descriptor is diagnostic only. It never becomes a connection
338
+ * and only a private, well-formed macOS/Windows descriptor may supply text. */
339
+ export function readCuaUnavailableReason({ platform = process.platform, userData = process.env.OMB_USER_DATA, home = homedir(), } = {}) {
340
+ if (!legacyPlatform(platform))
341
+ return null;
342
+ const candidates = userData ? [join(userData, "cua-connection.json")] : [];
343
+ if (platform === "darwin" && !userData) {
344
+ for (const directory of ["OpenMausBot", "openmausbot", "OpenGrokBot", "opengrokbot"]) {
345
+ candidates.push(join(home, "Library", "Application Support", directory, "cua-connection.json"));
346
+ }
347
+ }
348
+ const file = firstPresentCuaDescriptor(candidates);
349
+ if (!file)
350
+ return null;
351
+ try {
352
+ if (validateLegacyDescriptorRuntime(file, platform)) {
301
353
  const raw = JSON.parse(readFileSync(file, "utf8"));
302
- if (platform === "linux") {
303
- const decoded = decodeLinuxDescriptor(raw);
304
- if (decoded && validateLinuxRuntime(file, raw))
305
- return decoded;
306
- }
307
- else {
308
- const decoded = decodeLegacyDescriptor(raw, platform);
309
- if (decoded && validateLegacyRuntime(file, platform))
310
- return decoded;
354
+ if (raw && typeof raw === "object" && !Array.isArray(raw) &&
355
+ Object.keys(raw).length === 2 &&
356
+ raw.mode === "unavailable") {
357
+ const reason = raw.reason;
358
+ if (typeof reason === "string" && reason.trim() && reason.length <= 2_000)
359
+ return reason.trim();
311
360
  }
312
361
  }
313
- catch {
314
- // Missing, invalid, tampered, or stale descriptors are unavailable.
315
- }
362
+ }
363
+ catch {
364
+ // No usable diagnostic; the caller still refuses computer control.
316
365
  }
317
366
  return null;
318
367
  }
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { lstatSync, mkdirSync } from "node:fs";
2
+ import { existsSync, lstatSync, mkdirSync, renameSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { z } from "zod";
5
5
  const uuid = z.string().uuid();
@@ -9,9 +9,18 @@ const connectionSchema = z.object({
9
9
  email: z.string().email().max(320), deviceId: uuid, token: z.string().regex(/^omg_[A-Za-z0-9_-]{43}$/),
10
10
  expiresAt: z.number().finite().int().positive(),
11
11
  providers: z.array(z.object({ id: z.enum(["anthropic", "openai", "openrouter"]), configured: z.boolean(), models: z.array(model).max(500) }).strict()).max(3),
12
+ /** The Admin is up but its operator licence lapsed: keep the instances
13
+ * listed as unavailable instead of treating it as a revocation. */
14
+ suspended: z.literal("license-expired").optional(),
12
15
  }).strict();
16
+ /** The saved enrollment's identity (never its token), sent before it is
17
+ * cleared or when it has already expired, so references to its old
18
+ * device-scoped ids still move to the stable ids. */
19
+ const identitySchema = z.object({ portalOrigin: z.string().max(2048), organizationId: uuid, email: z.string().email().max(320), deviceId: uuid }).strict();
20
+ const PROVIDER_IDS = ["anthropic", "openai", "openrouter"];
13
21
  const signature = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
14
22
  const expiredMessage = "Company access has ended. Reconnect your organization or explicitly choose a personal model; personal billing will not be used automatically.";
23
+ export const LICENSE_EXPIRED_MESSAGE = "Your organization's OpenMaus Admin license has expired. Contact your admin.";
15
24
  /** Validate only messages received over Electron's private utility-parent port.
16
25
  * Never expose this input to renderer HTTP or merge it into saved AppConfig.
17
26
  */
@@ -33,10 +42,18 @@ export function parseManagedDesktopConnection(raw, now = Date.now()) {
33
42
  return { ...value, portalOrigin: url.origin, email: value.email.toLowerCase(), providers: value.providers.map(provider => ({ ...provider, models: [...new Set(provider.models)] })).sort((a, b) => a.id.localeCompare(b.id)) };
34
43
  }
35
44
  export function companyInstanceId(connection, provider) {
36
- // Device identity prevents an old company's native resume cursor from being
37
- // inherited by a different enrollment using the same organization/email.
45
+ // Stable for one person in one organisation, so re-enrolling this computer
46
+ // keeps bots' Company model choices and native resume. Another account or
47
+ // organisation still gets separate ids, homes and cursors.
48
+ return `company.${signature([connection.portalOrigin, connection.organizationId, connection.email.toLowerCase()]).slice(0, 24)}.${provider}`;
49
+ }
50
+ /** The pre-0.1.86 id, which also hashed the enrollment's deviceId. */
51
+ export function legacyCompanyInstanceId(connection, provider) {
38
52
  return `company.${signature([connection.portalOrigin, connection.organizationId, connection.deviceId]).slice(0, 24)}.${provider}`;
39
53
  }
54
+ /** Everything that decides which native instances exist and what they hold.
55
+ * Expiry and licence suspension change availability only, never a process. */
56
+ const instanceSignature = (connection) => signature(connection && (({ expiresAt: _expiresAt, suspended: _suspended, ...identity }) => identity)(connection));
40
57
  export function companyInstanceConfigs(connection, runtimeDirectory) {
41
58
  const entries = {};
42
59
  for (const provider of connection.providers) {
@@ -86,9 +103,18 @@ export class ManagedDesktopProviders {
86
103
  known = new Set();
87
104
  owns(instanceId) { return this.known.has(instanceId); }
88
105
  apply(raw, force = false) {
89
- const connection = parseManagedDesktopConnection(raw, this.now()), nextSignature = signature(connection);
90
- if (!force && nextSignature === this.currentSignature)
106
+ const connection = parseManagedDesktopConnection(raw, this.now()), nextSignature = instanceSignature(connection);
107
+ if (!force && nextSignature === this.currentSignature) {
108
+ // A renewal or licence change: same processes, new availability.
109
+ const previous = this.connection;
110
+ this.connection = connection;
111
+ if (connection && previous && (connection.expiresAt !== previous.expiresAt || connection.suspended !== previous.suspended)) {
112
+ clearTimeout(this.timer);
113
+ this.scheduleExpiry(this.revision);
114
+ this.options.onAvailability?.();
115
+ }
91
116
  return this.tail;
117
+ }
92
118
  this.currentSignature = nextSignature;
93
119
  this.connection = connection;
94
120
  const revision = ++this.revision;
@@ -107,6 +133,7 @@ export class ManagedDesktopProviders {
107
133
  // workspace backups. Device-scoped ids prevent a new enrollment from
108
134
  // inheriting another account's native conversation or credentials.
109
135
  const directory = this.ensureDirectory(["providers", "company"]);
136
+ await this.migrateLegacyIds(connection);
110
137
  const configs = companyInstanceConfigs(connection, directory);
111
138
  for (const [id, entry] of Object.entries(configs)) {
112
139
  if (this.options.registry.entries().some(existing => existing.instanceId === id))
@@ -117,7 +144,9 @@ export class ManagedDesktopProviders {
117
144
  this.ids.add(id);
118
145
  this.known.add(id);
119
146
  }
120
- const active = () => revision === this.revision && connection.expiresAt > this.now();
147
+ // Read the current grant: renewals and licence changes update it in place.
148
+ const active = () => revision === this.revision && Boolean(this.connection && this.connection.expiresAt > this.now() && !this.connection.suspended);
149
+ const unavailable = () => this.connection?.suspended === "license-expired" ? LICENSE_EXPIRED_MESSAGE : expiredMessage;
121
150
  await this.options.registry.load(configs, instance => {
122
151
  const provider = connection.providers.find(provider => companyInstanceId(connection, provider.id) === instance.instanceId);
123
152
  const models = { default: provider.models[0], options: provider.models.map(id => ({ id, label: `${id} (Company)` })) };
@@ -127,17 +156,17 @@ export class ManagedDesktopProviders {
127
156
  startAuthentication: undefined, getAuthentication: undefined, completeAuthentication: undefined, cancelAuthentication: undefined, signOut: undefined,
128
157
  snapshot: async () => {
129
158
  if (!active())
130
- return { state: "unavailable", reason: expiredMessage };
159
+ return { state: "unavailable", reason: unavailable() };
131
160
  const snapshot = await instance.snapshot();
132
161
  if (!active())
133
- return { state: "unavailable", reason: expiredMessage };
162
+ return { state: "unavailable", reason: unavailable() };
134
163
  return { ...snapshot, authenticated: snapshot.state === "available", billing: "metered", account: { email: connection.email, organization: connection.organizationName, method: "api-key" } };
135
164
  },
136
165
  adapter: {
137
166
  ...instance.adapter,
138
167
  sendTurn: async (input) => {
139
168
  if (!active())
140
- throw new Error(expiredMessage);
169
+ throw new Error(unavailable());
141
170
  if (!input.model || !provider.models.includes(input.model))
142
171
  throw new Error("This model is not enabled by your Company administrator.");
143
172
  return instance.adapter.sendTurn(input);
@@ -147,7 +176,7 @@ export class ManagedDesktopProviders {
147
176
  });
148
177
  if (revision === this.revision) {
149
178
  this.options.afterReplace?.([...this.ids]);
150
- this.scheduleExpiry(connection, revision);
179
+ this.scheduleExpiry(revision);
151
180
  }
152
181
  };
153
182
  this.tail = this.tail.catch(() => { }).then(replace).catch(async (error) => {
@@ -188,17 +217,53 @@ export class ManagedDesktopProviders {
188
217
  }
189
218
  return directory;
190
219
  }
191
- scheduleExpiry(connection, revision) {
220
+ scheduleExpiry(revision) {
221
+ const expiresAt = this.connection?.expiresAt;
222
+ if (expiresAt === undefined)
223
+ return;
192
224
  this.timer = setTimeout(() => {
193
225
  if (revision !== this.revision)
194
226
  return;
195
- if (connection.expiresAt > this.now())
196
- this.scheduleExpiry(connection, revision);
227
+ if ((this.connection?.expiresAt ?? 0) > this.now())
228
+ this.scheduleExpiry(revision);
197
229
  else
198
230
  void this.apply(null).catch(() => { });
199
- }, Math.min(2_147_483_647, Math.max(1, connection.expiresAt - this.now())));
231
+ }, Math.min(2_147_483_647, Math.max(1, expiresAt - this.now())));
200
232
  this.timer.unref?.();
201
233
  }
234
+ /** Moves an enrollment's old device-scoped ids to the stable ids: native
235
+ * homes are renamed only when the stable one does not exist yet, then saved
236
+ * references follow. Best effort: a failure is logged and never keeps
237
+ * Company models down. */
238
+ async migrateLegacyIds(identity) {
239
+ try {
240
+ const directory = this.ensureDirectory(["providers", "company"]);
241
+ const aliases = PROVIDER_IDS.map(provider => ({ from: legacyCompanyInstanceId(identity, provider), to: companyInstanceId(identity, provider) }));
242
+ for (const { from, to } of aliases) {
243
+ const legacy = join(directory, from), stable = join(directory, to);
244
+ if (!existsSync(legacy) || existsSync(stable))
245
+ continue;
246
+ const stat = lstatSync(legacy);
247
+ if (stat.isDirectory() && !stat.isSymbolicLink())
248
+ renameSync(legacy, stable);
249
+ }
250
+ await this.options.migrate?.(aliases);
251
+ }
252
+ catch (error) {
253
+ console.warn(`[company] could not move saved Company model references to their stable ids: ${error instanceof Error ? error.message : String(error)}`);
254
+ }
255
+ }
256
+ /** Migrates a saved enrollment that is being cleared or has expired. */
257
+ async migrateIdentity(raw) {
258
+ const parsed = identitySchema.parse(raw), url = new URL(parsed.portalOrigin);
259
+ if (url.origin !== parsed.portalOrigin || (url.protocol !== "https:" && !(url.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)))) {
260
+ throw new Error("Company identity must use an HTTPS origin or loopback fixture.");
261
+ }
262
+ const identity = { ...parsed, email: parsed.email.toLowerCase() };
263
+ const migration = this.tail.catch(() => { }).then(() => this.migrateLegacyIds(identity));
264
+ this.tail = migration;
265
+ return migration;
266
+ }
202
267
  async close() {
203
268
  await this.apply(null, true);
204
269
  }
@@ -0,0 +1,153 @@
1
+ import { z } from "zod";
2
+ /** An organisation's desktop policy, received only over Electron's private
3
+ * utility-parent port while this desktop is enrolled. It lives in memory: it
4
+ * is never merged into config.json, and it adds no prompts or approvals —
5
+ * a disallowed action is simply refused with a sentence saying who manages it. */
6
+ const policySchema = z.object({
7
+ organizationId: z.string().uuid(),
8
+ organizationName: z.string().trim().min(1).max(100),
9
+ expiresAt: z.number().int().positive(),
10
+ version: z.number().int().min(0).max(2_147_483_647),
11
+ companyModelsOnly: z.boolean(),
12
+ allowedEngines: z.union([z.literal("all"), z.array(z.string().regex(/^[A-Za-z][A-Za-z0-9-]{0,63}$/)).max(64)]),
13
+ mcp: z.object({ allowCustom: z.boolean(), allowlist: z.array(z.string().trim().min(1).max(200)).max(100) }).strict(),
14
+ computers: z.object({ thisComputer: z.boolean(), localVm: z.boolean(), box: z.boolean(), vps: z.boolean() }).strict(),
15
+ remoteAccess: z.boolean(),
16
+ }).strict();
17
+ const computerLabels = { thisComputer: "this computer", localVm: "local virtual machines", box: "Box cloud computers", vps: "VPS computers" };
18
+ export function parseManagedPolicy(raw, now = Date.now()) {
19
+ if (raw === null)
20
+ return null;
21
+ const parsed = policySchema.safeParse(raw);
22
+ if (!parsed.success)
23
+ throw new Error("Invalid organisation policy from the desktop parent.");
24
+ return parsed.data.expiresAt > now ? parsed.data : null;
25
+ }
26
+ /** `*` in a host pattern stands for one or more whole labels, never part of
27
+ * one, so `*.example.com` cannot match `evil.test/x.example.com`. */
28
+ function hostMatches(pattern, host) {
29
+ if (!pattern.length)
30
+ return !host.length;
31
+ if (pattern[0] === "*") {
32
+ for (let taken = 1; taken <= host.length - (pattern.length - 1); taken++)
33
+ if (hostMatches(pattern.slice(1), host.slice(taken)))
34
+ return true;
35
+ return false;
36
+ }
37
+ return host.length > 0 && pattern[0] === host[0] && hostMatches(pattern.slice(1), host.slice(1));
38
+ }
39
+ /** A name entry matches the configured server name. An address entry
40
+ * (https:// only) is parsed like the server's URL: no credentials, the same
41
+ * port, the host compared label by label, and the path separately (`*` there
42
+ * matches any characters). Anything that does not parse never matches. */
43
+ export function mcpEntryMatches(entry, name, url) {
44
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(entry))
45
+ return entry.toLowerCase() === name.toLowerCase();
46
+ if (!url)
47
+ return false;
48
+ let pattern, target;
49
+ try {
50
+ pattern = new URL(entry);
51
+ target = new URL(url);
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ if (pattern.protocol !== "https:" || target.protocol !== "https:" || pattern.username || pattern.password || target.username || target.password ||
57
+ pattern.search || pattern.hash || pattern.port !== target.port)
58
+ return false;
59
+ const labels = pattern.hostname.split(".");
60
+ if (labels.some(label => !label || (label !== "*" && label.includes("*"))) || labels.every(label => label === "*"))
61
+ return false;
62
+ if (!hostMatches(labels, target.hostname.split(".")))
63
+ return false;
64
+ const path = pattern.pathname.split("*").map(part => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*");
65
+ return new RegExp(`^${path}$`).test(target.pathname);
66
+ }
67
+ /** A resource key claimed by bindTurnComputer, mapped to the policy's kind. */
68
+ export function computerKindForResource(resource) {
69
+ if (resource === "computer:host")
70
+ return "thisComputer";
71
+ if (resource.startsWith("computer:vm:"))
72
+ return "localVm";
73
+ if (resource.startsWith("computer:box:") || resource.startsWith("computer:box-bot:"))
74
+ return "box";
75
+ if (resource.startsWith("computer:vps:"))
76
+ return "vps";
77
+ }
78
+ export class ManagedDesktopPolicy {
79
+ policy = null;
80
+ timer;
81
+ now;
82
+ onChange;
83
+ constructor(options = {}) {
84
+ this.now = options.now ?? Date.now;
85
+ this.onChange = options.onChange ?? (() => { });
86
+ }
87
+ apply(raw) {
88
+ const next = parseManagedPolicy(raw, this.now());
89
+ const changed = JSON.stringify(next) !== JSON.stringify(this.policy);
90
+ this.policy = next;
91
+ clearTimeout(this.timer);
92
+ // The last applied policy holds only as long as the device credential.
93
+ if (next) {
94
+ this.timer = setTimeout(() => this.apply(null), Math.min(2_147_483_647, Math.max(1, next.expiresAt - this.now())));
95
+ this.timer.unref?.();
96
+ }
97
+ if (changed)
98
+ this.onChange();
99
+ }
100
+ current() {
101
+ return this.policy && this.policy.expiresAt > this.now() ? this.policy : null;
102
+ }
103
+ close() { clearTimeout(this.timer); this.policy = null; }
104
+ /** Why this organisation does not allow an instance to run a turn, or undefined. */
105
+ modelRefusal(instance, company) {
106
+ const policy = this.current();
107
+ if (!policy)
108
+ return;
109
+ if (policy.companyModelsOnly && !company)
110
+ return `${policy.organizationName} allows only company models on this computer. Choose a Company model for this bot.`;
111
+ if (policy.allowedEngines !== "all" && !policy.allowedEngines.includes(instance.driverKind)) {
112
+ return `${policy.organizationName} does not allow the ${instance.displayName ?? instance.driverKind} engine on this computer. Choose another model for this bot.`;
113
+ }
114
+ }
115
+ mcpAllowed(name, url) {
116
+ const policy = this.current();
117
+ return !policy || policy.mcp.allowCustom || policy.mcp.allowlist.some(entry => mcpEntryMatches(entry, name, url));
118
+ }
119
+ /** Only approved servers reach engines while custom servers are off. */
120
+ filterMcp(servers) {
121
+ if (!this.restrictsMcp())
122
+ return servers;
123
+ return Object.fromEntries(Object.entries(servers).filter(([name, server]) => this.mcpAllowed(name, "url" in server && typeof server.url === "string" ? server.url : undefined)));
124
+ }
125
+ restrictsMcp() { const policy = this.current(); return Boolean(policy && !policy.mcp.allowCustom); }
126
+ mcpRefusal(name, url) {
127
+ const policy = this.current();
128
+ if (!policy || this.mcpAllowed(name, url))
129
+ return;
130
+ return `${policy.organizationName} allows only MCP servers it has approved. Ask your administrator to add this server to the approved list.`;
131
+ }
132
+ computerAllowed(kind) { return this.current()?.computers[kind] ?? true; }
133
+ computerRefusal(kind) {
134
+ const policy = this.current();
135
+ if (!policy || policy.computers[kind])
136
+ return;
137
+ return `${policy.organizationName} does not allow bots to use ${computerLabels[kind]}.`;
138
+ }
139
+ remoteAccessRefusal() {
140
+ const policy = this.current();
141
+ if (!policy || policy.remoteAccess)
142
+ return;
143
+ return `${policy.organizationName} does not allow remote access to this computer.`;
144
+ }
145
+ /** Read-only view for the renderer; carries nothing secret. */
146
+ summary() {
147
+ const policy = this.current();
148
+ if (!policy)
149
+ return null;
150
+ const { organizationId: _id, expiresAt: _expires, ...visible } = policy;
151
+ return visible;
152
+ }
153
+ }
@@ -36,14 +36,14 @@ import { createToolListNormalizer } from "./mcp-tool-schema.js";
36
36
  // stdin/stdout/stderr resets the window.
37
37
  export const BRIDGE_INACTIVITY_MS = 45_000;
38
38
  const PROBE_TIMEOUT_MS = 10_000;
39
- /** Run the liveness command; alive means "exited 0 within the timeout". The
40
- * probe is its own short-lived process, so it cannot inherit the wedged
41
- * connection it is diagnosing. */
42
- export function runLivenessProbe(probe, timeoutMs = PROBE_TIMEOUT_MS) {
39
+ /** Run the liveness command; alive means "exited 0 within the timeout". A
40
+ * separate bounded command checks the transport without waiting for an
41
+ * answer on the possibly wedged MCP stream. */
42
+ export function runLivenessProbe(probe, timeoutMs = PROBE_TIMEOUT_MS, env) {
43
43
  return new Promise((resolve) => {
44
44
  const child = spawn(probe.command, probe.args, {
45
45
  shell: false,
46
- env: { ...process.env, PATH: augmentedPath() },
46
+ env: env ?? { ...process.env, PATH: augmentedPath() },
47
47
  stdio: ["ignore", "ignore", "ignore"],
48
48
  });
49
49
  const timer = setTimeout(() => {
@@ -265,7 +265,7 @@ export function runMcpBridge(options) {
265
265
  const liveness = options.liveness;
266
266
  watchdog = createInactivityWatchdog({
267
267
  inactivityMs: BRIDGE_INACTIVITY_MS,
268
- probe: () => runLivenessProbe(liveness),
268
+ probe: () => runLivenessProbe(liveness, PROBE_TIMEOUT_MS, options.env),
269
269
  onDead: () => {
270
270
  process.stderr.write(`${options.label} transport went silent and stopped answering liveness probes; ending the bridge\n`);
271
271
  process.exitCode = 1;
@@ -408,6 +408,25 @@ export function setActiveLeaf(threadId, leafId) {
408
408
  "ON CONFLICT(thread_id) DO UPDATE SET active_leaf_id = excluded.active_leaf_id")
409
409
  .run(threadId, leafId);
410
410
  }
411
+ /** Newest message timestamp per thread. One grouped read, chunked under
412
+ * SQLite's variable limit. Threads with no rows are absent. */
413
+ export function latestMessageAts(threadIds) {
414
+ const ids = [...new Set(threadIds.filter((id) => id.length > 0))];
415
+ const out = new Map();
416
+ const chunk = 400;
417
+ for (let i = 0; i < ids.length; i += chunk) {
418
+ const slice = ids.slice(i, i + chunk);
419
+ const placeholders = slice.map(() => "?").join(", ");
420
+ const rows = db()
421
+ .prepare(`SELECT thread_id, MAX(at) AS at FROM messages WHERE thread_id IN (${placeholders}) GROUP BY thread_id`)
422
+ .all(...slice);
423
+ for (const row of rows) {
424
+ if (typeof row.at === "number" && Number.isFinite(row.at))
425
+ out.set(row.thread_id, row.at);
426
+ }
427
+ }
428
+ return out;
429
+ }
411
430
  export function deleteThread(threadId) {
412
431
  writeFollowups((connection) => {
413
432
  connection.prepare("DELETE FROM chat_followups WHERE thread_id = ?").run(threadId);
@@ -415,6 +434,15 @@ export function deleteThread(threadId) {
415
434
  connection.prepare("DELETE FROM thread_state WHERE thread_id = ?").run(threadId);
416
435
  });
417
436
  }
437
+ /** Every thread whose stored messages mention `fragment` anywhere (an
438
+ * attachment's file name, say). A scan, like search; used only to decide
439
+ * whether a member on a workspace with a restricted bot may fetch a file. */
440
+ export function threadsReferencing(fragment) {
441
+ if (!fragment)
442
+ return [];
443
+ const rows = db().prepare("SELECT DISTINCT thread_id FROM messages WHERE instr(json, ?) > 0").all(fragment);
444
+ return rows.map((row) => row.thread_id);
445
+ }
418
446
  /** Case-insensitive substring search over text messages, newest first.
419
447
  * A LIKE scan, deliberately: local transcripts are megabytes at most, a
420
448
  * scan is milliseconds, and it needs no FTS extension to exist. */
@@ -6,6 +6,11 @@ export function modelContextWindow(model) {
6
6
  if (!model)
7
7
  return undefined;
8
8
  const id = model.toLowerCase();
9
+ // Opus 5.5 is 1M without a `[1m]` suffix. Only the exact slug, or that
10
+ // slug after a slash or a provider dot. A longer suffix such as
11
+ // `claude-opus-5-5-local` stays on the generic Claude rule, as does `host::`.
12
+ if (/(?:^|[/.])claude-opus-5(?:-5|\.5)$/.test(id))
13
+ return 1_000_000;
9
14
  if (/claude/.test(id) || /^(opus|sonnet|haiku|fable)\b/.test(id))
10
15
  return /\[1m\]|-1m\b|1m-context/.test(id) ? 1_000_000 : 200_000;
11
16
  if (/^(gpt-5|o[34]\b|codex)/.test(id))
@@ -14,6 +19,8 @@ export function modelContextWindow(model) {
14
19
  return 1_000_000;
15
20
  if (/gemini/.test(id))
16
21
  return 1_000_000;
22
+ if (id === "grok-4.7")
23
+ return 500_000;
17
24
  if (id.startsWith("grok"))
18
25
  return 256_000;
19
26
  return undefined;