openmausbot 0.1.90 → 0.1.92

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 (177) hide show
  1. package/dist/assets/DesktopViewer-Do6lBa4C.js +5 -0
  2. package/dist/assets/{abnfDiagram-O67JEVCF-DUyXd0kL.js → abnfDiagram-O67JEVCF-Cd3X7Hxc.js} +1 -1
  3. package/dist/assets/{arc-q73qM-At.js → arc-DIYc7Ymx.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-D33O3Wur.js → architectureDiagram-NJMV4G6O-DBmRGVyP.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-DLsJeoXC.js → blockDiagram-BEXU5L5S-CPFePSEE.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-DnaljZnu.js → c4Diagram-YGBWAQC7-DD_DO1qy.js} +1 -1
  7. package/dist/assets/channel-Dtk5Mtic.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-Da0h2i79.js → chunk-2Q5K7J3B-BNEeNSjX.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-CfzHi54f.js → chunk-3FUC2YCW-BbinLaqH.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-LjE4d9Jf.js → chunk-5VM5RSS4-BBgYrgDR.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-DY-UGmvc.js → chunk-GTNCS2PH-ezXj-kA-.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-BqeGhyhP.js → chunk-GWA4HPMP-9FxZ3diV.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-BYa9EBAg.js → chunk-JWPE2WC7-CO-40JpI.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-vTrDtWyb.js → chunk-NETBCI7D-BEhjslYE.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-ic5tXadM.js → chunk-WEXAMYUT-CNeq4n7_.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-DvRCY-c6.js → chunk-XXDRQBXY-OsCY7KSi.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-DVbmgz_L.js → classDiagram-v2-NBCMYWYE-CloRUEtk.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-vdUs2n57.js → cose-bilkent-JH36ORCC-D1BGZhp4.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-BvLGlV3U.js → cynefin-OW5HDTMX-CUf4T2CN.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-C4GcJQDt.js → cynefinDiagram-VND7K2PF-DPEZrFGA.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-BMSaHpt_.js → dagre-6A5THRUB-bDe6GIv5.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-D5b9UBhC.js → diagram-22UHCM2B-CdcUQz55.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-CIKLlB7Y.js → diagram-3UASUU5V-BZpqGg7c.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-BzXb-7k7.js → diagram-ATOU4E4O-DvGM6ugp.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-HsTvJ7uT.js → diagram-CDSNMT55-DSTmMm0s.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-Db9ySNbQ.js → diagram-MLGK6HIB-15cucnMn.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-D6BKbpqY.js → diagram-MPIPVDR6-tMbuaE6n.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-Bq067L0t.js → ebnfDiagram-ZINNZB2B-BYxYzjHI.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-DhaD7fNJ.js → elk-276RUBZZ-v6bj852W.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-fwIhfz36.js → erDiagram-OPXOYQCR-5lBBlnd2.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-DygIyu0V.js → flowDiagram-KWPJA3E3-CZyl1zIj.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-DPYfSO96.js → ganttDiagram-FUAMR5RP-BiAI-ltZ.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-DhkHeCle.js → gitGraphDiagram-X574FWY7-BAZISkj3.js} +1 -1
  34. package/dist/assets/index-Cdmjrl28.css +1 -0
  35. package/dist/assets/{index-G1PHKGyh.js → index-DGBaEXrM.js} +1 -1
  36. package/dist/assets/index-DR7YPMkW.js +597 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-DstBLxME.js → infoDiagram-VRGFBTTK-B6ooOy-Z.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-CrIsGgch.js → ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-CYypGDbB.js → journeyDiagram-ZHPQQLJL-DWqlzIEe.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-DGjPt8E_.js → kanban-definition-PNTS6WVX-JX6piDwL.js} +1 -1
  41. package/dist/assets/{linear-Dwc3Hrru.js → linear-CmOWzX0P.js} +1 -1
  42. package/dist/assets/{mermaid.core-Dc4OePBL.js → mermaid.core-CF1zwoQ-.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-DV-vepj-.js → mindmap-definition-NLK3R4M7-CGh2K4fW.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-lx5Kj-n5.js → pegDiagram-GJSIUBJH-CDwYN0s7.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-Bq7Vmm15.js → pieDiagram-5QR66LMP-1rfjJocB.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-DeqeqOBj.js → quadrantDiagram-O4NWA36T-tKPvPiok.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-CXwRQzSC.js → railroadDiagram-XR7U4H2S-BHLm5vXR.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-8tAeoB88.js → requirementDiagram-PLB6GJNP-CB45STp7.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-BG5zjGWU.js → sankeyDiagram-IPEJSGJF-55QA_huC.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-DKF3iLcw.js → sequenceDiagram-PO4LG4MO-BckFmQHn.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-B-y1qUgT.js → sizeCapture-INFHLROL-BFCOJF11.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-MkzPgxAc.js → stateDiagram-v2-GCMORJYK-CZvmiVVN.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DnbaOEwH.js → swimlanes-2SLR337P-BpDUCEKy.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-M0GYW9Y3.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-Bb4xG_Bt.js → timeline-definition-EJHVYXUP-BpxHq6IK.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-JgkR9pFf.js → usecaseDiagram-POWQR4AR-Cj66vm02.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-CIPIrAYe.js → vennDiagram-UO4OBE2U-C9nRkWE_.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-BjCLuUvJ.js → wardleyDiagram-VNRHLVJA-D-me_nWj.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-DKgiaYxT.js → xychartDiagram-PMCCYNJV-Dc_fL4mk.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 +128 -15
  63. package/dist-server/container-mcp.js +277 -45
  64. package/dist-server/drivers/agents-proxy.js +16 -4
  65. package/dist-server/enterprise/server/index.js +5 -1
  66. package/dist-server/index.js +46703 -39955
  67. package/dist-server/local-computer-proxy.js +128 -7
  68. package/dist-server/mcp-gate.js +11 -5
  69. package/dist-server/model-catalog/models-dev.snapshot.json +7299 -0
  70. package/dist-server/openmausbot.js +4099 -1217
  71. package/dist-server/pair-cli.js +4099 -1217
  72. package/dist-server/server/boat.js +67 -22
  73. package/dist-server/server/bot-overview.js +7 -4
  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 +136 -26
  80. package/dist-server/server/cloud-home.js +64 -5
  81. package/dist-server/server/cloud-lending.js +167 -0
  82. package/dist-server/server/cloud-move-http.js +268 -0
  83. package/dist-server/server/cloud-move.js +418 -0
  84. package/dist-server/server/cloud-owner.js +196 -0
  85. package/dist-server/server/cloud-secrets-boot.js +6 -0
  86. package/dist-server/server/cloud-secrets.js +65 -0
  87. package/dist-server/server/config.js +215 -17
  88. package/dist-server/server/container-computer.js +3 -2
  89. package/dist-server/server/decider/index.js +238 -0
  90. package/dist-server/server/decider/jev.js +183 -0
  91. package/dist-server/server/decider/log.js +62 -0
  92. package/dist-server/server/decider/relay.js +52 -0
  93. package/dist-server/server/decider/room-routing.js +88 -0
  94. package/dist-server/server/decider/types.js +5 -0
  95. package/dist-server/server/desktop-viewer-targets.js +26 -0
  96. package/dist-server/server/drivers/acp/core.js +167 -21
  97. package/dist-server/server/drivers/acp/opencode-go.js +614 -85
  98. package/dist-server/server/drivers/agents-catalog.js +22 -6
  99. package/dist-server/server/drivers/antigravity-acp.js +69 -11
  100. package/dist-server/server/drivers/antigravity-temp.js +360 -0
  101. package/dist-server/server/drivers/antigravity.js +21 -0
  102. package/dist-server/server/drivers/boatagent.js +20 -12
  103. package/dist-server/server/drivers/chatgpt-plan-auth.js +481 -0
  104. package/dist-server/server/drivers/claude.js +313 -34
  105. package/dist-server/server/drivers/codex.js +228 -18
  106. package/dist-server/server/drivers/grok.js +2 -2
  107. package/dist-server/server/drivers/mistral.js +4 -3
  108. package/dist-server/server/drivers/openai-chat-protocol.js +4 -3
  109. package/dist-server/server/drivers/openai-chat.js +41 -3
  110. package/dist-server/server/drivers/openai-compat.js +73 -23
  111. package/dist-server/server/engine-install.js +46 -12
  112. package/dist-server/server/env-path.js +15 -3
  113. package/dist-server/server/group-goal-run.js +6 -4
  114. package/dist-server/server/harness/registry.js +2 -2
  115. package/dist-server/server/harness-capabilities.js +2 -0
  116. package/dist-server/server/included-services.js +70 -0
  117. package/dist-server/server/index.js +1753 -263
  118. package/dist-server/server/lending-memory.js +479 -0
  119. package/dist-server/server/local-vm-idle.js +24 -6
  120. package/dist-server/server/managed-desktop.js +3 -0
  121. package/dist-server/server/mcp-bridge.js +5 -3
  122. package/dist-server/server/mcp-http.js +6 -3
  123. package/dist-server/server/mcp-oauth-discovery.js +133 -0
  124. package/dist-server/server/mcp-oauth-store.js +70 -0
  125. package/dist-server/server/mcp-oauth.js +550 -0
  126. package/dist-server/server/mcp-probe.js +14 -1
  127. package/dist-server/server/mcp-registry.js +43 -0
  128. package/dist-server/server/memory-upkeep.js +94 -84
  129. package/dist-server/server/model-catalog/catalog.js +374 -0
  130. package/dist-server/server/model-catalog/presets.js +176 -0
  131. package/dist-server/server/model-catalog/trim.js +219 -0
  132. package/dist-server/server/model-context-window.js +4 -4
  133. package/dist-server/server/package-export.js +17 -10
  134. package/dist-server/server/package-import.js +6 -3
  135. package/dist-server/server/package-parts.js +3 -1
  136. package/dist-server/server/provider-key-check.js +4 -2
  137. package/dist-server/server/recent-work.js +1 -1
  138. package/dist-server/server/request-auth.js +25 -0
  139. package/dist-server/server/routes/antigravity-leftovers.js +45 -0
  140. package/dist-server/server/routes/bot-memory.js +31 -6
  141. package/dist-server/server/routes/decider.js +19 -0
  142. package/dist-server/server/routes/desktop-viewer.js +194 -0
  143. package/dist-server/server/routine-requests.js +1 -1
  144. package/dist-server/server/routines.js +1 -1
  145. package/dist-server/server/sessions.js +56 -9
  146. package/dist-server/server/shared-computers.js +116 -30
  147. package/dist-server/server/skills.js +25 -10
  148. package/dist-server/server/store.js +79 -124
  149. package/dist-server/server/surface.js +13 -0
  150. package/dist-server/server/system-prompt.js +11 -0
  151. package/dist-server/server/team-backup.js +6 -2
  152. package/dist-server/server/team-manifest.js +11 -3
  153. package/dist-server/server/thread-events.js +2 -0
  154. package/dist-server/server/thread-starters.js +33 -0
  155. package/dist-server/server/thread-title.js +46 -0
  156. package/dist-server/server/tts/elevenlabs.js +11 -6
  157. package/dist-server/server/tts/fish.js +4 -5
  158. package/dist-server/server/tts/index.js +20 -11
  159. package/dist-server/server/vps-computer.js +79 -12
  160. package/dist-server/server/webhooks.js +18 -1
  161. package/dist-server/server/workspace-backup-policy.js +25 -3
  162. package/dist-server/server/workspace-backup.js +28 -8
  163. package/dist-server/server/workspace.js +65 -15
  164. package/dist-server/shared/cloud-home.js +4 -0
  165. package/dist-server/shared/package-format.js +30 -4
  166. package/dist-server/shared/team-backup.js +20 -3
  167. package/dist-server/shared/workspace-backup-client.js +1 -0
  168. package/dist-server/src/lib/citations.js +177 -0
  169. package/dist-server/src/lib/composer-attachments.js +7 -1
  170. package/dist-server/vps-container-mcp.js +278 -45
  171. package/dist-server/workspace-backup.worker.js +184 -38
  172. package/enterprise/server/index.js +5 -1
  173. package/package.json +1 -1
  174. package/dist/assets/channel-BCuyHjFV.js +0 -1
  175. package/dist/assets/index-CRvo-DS2.css +0 -1
  176. package/dist/assets/index-CWYGlA63.js +0 -588
  177. package/dist/assets/swimlanesDiagram-TC7HE7FX-BZQ07DGE.js +0 -8
@@ -0,0 +1,176 @@
1
+ // Hand-kept facts about model providers that models.dev does not carry:
2
+ // - which providers we recommend, in what order, and under what name;
3
+ // - a pinned OpenAI-compatible base URL for each (models.dev lists some
4
+ // providers without one, and a pinned URL means a catalog refresh can never
5
+ // change where a newly pasted key for a recommended provider is sent);
6
+ // - the Anthropic-compatible endpoint Claude Code can use, where one exists;
7
+ // - whether the provider documents Codex support, and its exceptions;
8
+ // - how to prove a key when the provider's model list is public.
9
+ // Every row was checked by hand on its `checkedAt` date.
10
+ //
11
+ // Codex rules (see codexVerdict): Codex is offered only when the provider's
12
+ // /responses route passed the check AND either the provider documents Codex
13
+ // or the person turned on the experimental switch. Documented exceptions
14
+ // (Fireworks: no MiniMax models, no Fire Pass keys) always apply.
15
+ const CHECKED = "2026-09-30";
16
+ /** The AI SDK package for any OpenAI-compatible base URL. */
17
+ export const OPENAI_COMPATIBLE_NPM = "@ai-sdk/openai-compatible";
18
+ export const PROVIDER_PRESETS = [
19
+ {
20
+ id: "openrouter",
21
+ catalogId: "openrouter",
22
+ label: "OpenRouter",
23
+ recommended: 1,
24
+ api: "https://openrouter.ai/api/v1",
25
+ anthropic: {
26
+ baseUrl: "https://openrouter.ai/api",
27
+ auth: "bearer",
28
+ doc: "https://openrouter.ai/docs/guides/guides/claude-code-integration",
29
+ },
30
+ codex: { kind: "documented", doc: "https://openrouter.ai/docs/cookbook/coding-agents/codex-cli" },
31
+ // /models is public; /key answers only with a valid key.
32
+ keyCheck: "/key",
33
+ publicModels: true,
34
+ checkedAt: CHECKED,
35
+ },
36
+ {
37
+ id: "fireworks-ai",
38
+ catalogId: "fireworks-ai",
39
+ label: "Fireworks AI",
40
+ recommended: 2,
41
+ api: "https://api.fireworks.ai/inference/v1",
42
+ anthropic: {
43
+ baseUrl: "https://api.fireworks.ai/inference",
44
+ auth: "bearer",
45
+ doc: "https://docs.fireworks.ai/tools-sdks/anthropic-compatibility",
46
+ },
47
+ codex: {
48
+ kind: "documented",
49
+ doc: "https://docs.fireworks.ai/ecosystem/fireconnect/codex",
50
+ // Fireworks: MiniMax models and Fire Pass keys do not work with Codex.
51
+ denyModels: /minimax/i,
52
+ denyKeyPrefixes: ["fpk_"],
53
+ },
54
+ checkedAt: CHECKED,
55
+ },
56
+ {
57
+ id: "deepseek",
58
+ catalogId: "deepseek",
59
+ label: "DeepSeek",
60
+ recommended: 3,
61
+ api: "https://api.deepseek.com",
62
+ anthropic: { baseUrl: "https://api.deepseek.com/anthropic", doc: "https://api-docs.deepseek.com/guides/anthropic_api" },
63
+ codex: { kind: "unsupported", reason: "no-responses-api" },
64
+ checkedAt: CHECKED,
65
+ },
66
+ {
67
+ id: "cline-pass",
68
+ catalogId: "cline-pass",
69
+ label: "Cline (ClinePass)",
70
+ recommended: 4,
71
+ api: "https://api.cline.bot/api/v1",
72
+ codex: { kind: "unsupported", reason: "not-supported" },
73
+ publicModels: true,
74
+ checkedAt: CHECKED,
75
+ },
76
+ {
77
+ // Cline's pay-as-you-go credits: the same API as ClinePass with a larger,
78
+ // OpenRouter-style model list. Not in models.dev.
79
+ id: "cline-credits",
80
+ label: "Cline (credits)",
81
+ recommended: 5,
82
+ api: "https://api.cline.bot/api/v1",
83
+ env: "CLINE_API_KEY",
84
+ doc: "https://docs.cline.bot/api/overview",
85
+ codex: { kind: "unsupported", reason: "not-supported" },
86
+ publicModels: true,
87
+ checkedAt: CHECKED,
88
+ },
89
+ { id: "groq", catalogId: "groq", label: "Groq", recommended: 6, api: "https://api.groq.com/openai/v1", checkedAt: CHECKED },
90
+ { id: "togetherai", catalogId: "togetherai", label: "Together AI", recommended: 7, api: "https://api.together.xyz/v1", checkedAt: CHECKED },
91
+ { id: "mistral", catalogId: "mistral", label: "Mistral", recommended: 8, api: "https://api.mistral.ai/v1", checkedAt: CHECKED },
92
+ { id: "xai", catalogId: "xai", label: "xAI", recommended: 9, api: "https://api.x.ai/v1", checkedAt: CHECKED },
93
+ {
94
+ id: "moonshotai",
95
+ catalogId: "moonshotai",
96
+ label: "Moonshot (Kimi)",
97
+ recommended: 10,
98
+ api: "https://api.moonshot.ai/v1",
99
+ anthropic: { baseUrl: "https://api.moonshot.ai/anthropic" },
100
+ checkedAt: CHECKED,
101
+ },
102
+ {
103
+ id: "zai",
104
+ catalogId: "zai",
105
+ label: "Z.ai",
106
+ recommended: 11,
107
+ api: "https://api.z.ai/api/paas/v4",
108
+ anthropic: { baseUrl: "https://api.z.ai/api/anthropic" },
109
+ checkedAt: CHECKED,
110
+ },
111
+ {
112
+ // models.dev lists MiniMax's Anthropic endpoint as its `api` (with
113
+ // @ai-sdk/anthropic); the chat engine and OpenCode need the OpenAI one,
114
+ // and OpenCode needs the matching SDK with it.
115
+ id: "minimax",
116
+ catalogId: "minimax",
117
+ label: "MiniMax",
118
+ recommended: 12,
119
+ api: "https://api.minimax.io/v1",
120
+ npm: OPENAI_COMPATIBLE_NPM,
121
+ anthropic: { baseUrl: "https://api.minimax.io/anthropic" },
122
+ chat: "after-check",
123
+ checkedAt: CHECKED,
124
+ },
125
+ { id: "cerebras", catalogId: "cerebras", label: "Cerebras", recommended: 13, api: "https://api.cerebras.ai/v1", checkedAt: CHECKED },
126
+ {
127
+ id: "openai",
128
+ catalogId: "openai",
129
+ label: "OpenAI",
130
+ api: "https://api.openai.com/v1",
131
+ codex: { kind: "documented", doc: "https://github.com/openai/codex" },
132
+ checkedAt: CHECKED,
133
+ },
134
+ {
135
+ id: "opencode",
136
+ catalogId: "opencode",
137
+ label: "OpenCode Zen",
138
+ api: "https://opencode.ai/zen/v1",
139
+ anthropic: { baseUrl: "https://opencode.ai/zen" },
140
+ checkedAt: CHECKED,
141
+ },
142
+ ];
143
+ const BY_ID = new Map(PROVIDER_PRESETS.map((preset) => [preset.id, preset]));
144
+ export function providerPreset(id) {
145
+ return BY_ID.get(id);
146
+ }
147
+ export function codexKeyAllowed(preset, key) {
148
+ const codex = preset?.codex;
149
+ return !(codex?.kind === "documented" && codex.denyKeyPrefixes?.some((prefix) => key.startsWith(prefix)));
150
+ }
151
+ export function codexModelAllowed(preset, model) {
152
+ const codex = preset?.codex;
153
+ return !(codex?.kind === "documented" && codex.denyModels?.test(model));
154
+ }
155
+ /** The models Codex may be offered for this provider. */
156
+ export function codexModels(preset, models) {
157
+ return models.filter((model) => codexModelAllowed(preset, model));
158
+ }
159
+ /** Whether Codex can use a provider. `preset` is undefined for custom providers. */
160
+ export function codexVerdict(input) {
161
+ const codex = input.preset?.codex;
162
+ if (codex?.kind === "unsupported") {
163
+ return { use: "no", reason: codex.reason === "no-responses-api" ? "provider-no-responses-api" : "provider-unsupported" };
164
+ }
165
+ if (input.key !== undefined && !codexKeyAllowed(input.preset, input.key))
166
+ return { use: "no", reason: "key-type" };
167
+ if (input.responses === "missing")
168
+ return { use: "no", reason: "route-missing" };
169
+ if (input.responses === "key-refused")
170
+ return { use: "no", reason: "key-refused" };
171
+ if (input.responses !== "works")
172
+ return { use: "no", reason: "not-checked" };
173
+ if (codex?.kind === "documented")
174
+ return { use: "yes" };
175
+ return input.experimental ? { use: "experimental" } : { use: "no", reason: "experimental-off" };
176
+ }
@@ -0,0 +1,219 @@
1
+ // Trims models.dev's api.json down to what OpenMausBot needs and drops
2
+ // anything unsafe to hand to OpenCode. Both the build script that makes the
3
+ // bundled snapshot (scripts/build-model-catalog.mjs) and the runtime refresh
4
+ // (catalog.ts) run their data through trimModelsDevCatalog, so a catalog
5
+ // fetched later passes exactly the same filter as the one we ship.
6
+ //
7
+ // Why braces matter: OpenCode expands `{env:NAME}` and `{file:path}` anywhere
8
+ // in its config text, and writing the brace as { in JSON does not stop it
9
+ // (verified on OpenCode 1.18.27). A provider or model string carrying a brace
10
+ // could make OpenCode read another provider's key or any file the user can
11
+ // read into a model name. So any provider or model with a `{` or `}` in any of
12
+ // its strings (or object keys) is dropped here, before anything else sees it.
13
+ //
14
+ // The trimmed shape keeps models.dev's own field names, so trimming is
15
+ // idempotent: a trimmed catalog passes through unchanged. That lets catalog.ts
16
+ // run every source (our cache, OpenCode's raw cache, the snapshot) through the
17
+ // same function. On disk each model also leaves out `id` (it is the key) and
18
+ // `tool_call` (every kept model has it); packCatalog/unpackCatalog convert,
19
+ // which keeps the shipped snapshot under its 150 KB gzipped budget.
20
+ import { normalizeApiUrl } from "../cli-api-setup.js";
21
+ export const MODEL_CATALOG_SCHEMA = 1;
22
+ // Generous caps: models.dev's largest provider lists about 600 models today.
23
+ const MAX_PROVIDERS = 2_000;
24
+ const MAX_MODELS_PER_PROVIDER = 5_000;
25
+ const MAX_DEPTH = 8;
26
+ const PROVIDER_ID = /^[a-z0-9][a-z0-9._-]{0,63}$/;
27
+ const ENV_NAME = /^[A-Za-z0-9_]{1,128}$/;
28
+ // eslint-disable-next-line no-control-regex
29
+ const CONTROL = /[\u0000-\u001f\u007f]/u;
30
+ const RELEASE_DATE = /^\d{4}-\d{2}(?:-\d{2})?$/;
31
+ /** True when the text holds a brace OpenCode could treat as a substitution. */
32
+ export function hasBrace(value) {
33
+ return value.includes("{") || value.includes("}");
34
+ }
35
+ /** True when any string or object key inside `value` holds a brace. Values
36
+ * nested deeper than models.dev ever goes count as unsafe too. */
37
+ export function containsBrace(value, depth = 0) {
38
+ if (typeof value === "string")
39
+ return hasBrace(value);
40
+ if (value === null || typeof value !== "object")
41
+ return false;
42
+ if (depth > MAX_DEPTH)
43
+ return true;
44
+ if (Array.isArray(value))
45
+ return value.some((item) => containsBrace(item, depth + 1));
46
+ return Object.entries(value).some(([key, item]) => hasBrace(key) || containsBrace(item, depth + 1));
47
+ }
48
+ const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
49
+ /** A map keyed by provider or model id. It has no prototype, so an id such as
50
+ * `constructor` or `toString` finds nothing unless the catalog lists it, and a
51
+ * `__proto__` key could not replace the prototype (it is also rejected). */
52
+ const idMap = () => Object.create(null);
53
+ /** Never a real id; as a plain-object key it would set the prototype. */
54
+ const RESERVED_KEY = "__proto__";
55
+ const finite = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
56
+ /** A display name: control characters removed, trimmed, capped. */
57
+ function displayName(value, fallback) {
58
+ if (typeof value !== "string")
59
+ return fallback;
60
+ const clean = value.replace(new RegExp(CONTROL.source, "gu"), "").trim().slice(0, 128);
61
+ return clean || fallback;
62
+ }
63
+ function httpsUrl(value) {
64
+ if (typeof value !== "string")
65
+ return undefined;
66
+ try {
67
+ const url = new URL(value);
68
+ return url.protocol === "https:" && !url.username && !url.password ? url.href : undefined;
69
+ }
70
+ catch {
71
+ return undefined;
72
+ }
73
+ }
74
+ function apiUrl(value) {
75
+ if (typeof value !== "string")
76
+ return undefined;
77
+ try {
78
+ return normalizeApiUrl(value);
79
+ }
80
+ catch {
81
+ return undefined;
82
+ }
83
+ }
84
+ function trimModel(key, raw) {
85
+ if (!isRecord(raw))
86
+ return "invalid";
87
+ if (raw.id !== undefined && raw.id !== key)
88
+ return "invalid";
89
+ // The caller already drops a key with a brace; this is the backstop.
90
+ if (!key || key.length > 256 || key === RESERVED_KEY || hasBrace(key) || CONTROL.test(key) || /\s/u.test(key))
91
+ return "invalid";
92
+ // Only models an agent can drive: current, tool-calling, text in.
93
+ if (raw.status === "deprecated" || raw.tool_call !== true)
94
+ return "skip";
95
+ const modalities = isRecord(raw.modalities) ? raw.modalities : undefined;
96
+ const input = Array.isArray(modalities?.input)
97
+ ? modalities.input.filter((item) => typeof item === "string" && item.length <= 32).slice(0, 8)
98
+ : [];
99
+ if (!input.includes("text"))
100
+ return "skip";
101
+ const model = {
102
+ id: key,
103
+ name: displayName(raw.name, key),
104
+ tool_call: true,
105
+ reasoning: raw.reasoning === true,
106
+ attachment: raw.attachment === true,
107
+ modalities: { input },
108
+ };
109
+ if (typeof raw.release_date === "string" && RELEASE_DATE.test(raw.release_date))
110
+ model.release_date = raw.release_date;
111
+ const context = finite(isRecord(raw.limit) ? raw.limit.context : undefined);
112
+ if (context !== undefined)
113
+ model.limit = { context };
114
+ if (isRecord(raw.cost)) {
115
+ const cost = {};
116
+ for (const field of ["input", "output"]) {
117
+ const price = finite(raw.cost[field]);
118
+ if (price !== undefined)
119
+ cost[field] = price;
120
+ }
121
+ if (Object.keys(cost).length)
122
+ model.cost = cost;
123
+ }
124
+ if (raw.status === "alpha" || raw.status === "beta")
125
+ model.status = raw.status;
126
+ return model;
127
+ }
128
+ function trimProvider(key, raw, options) {
129
+ const drop = (reason) => {
130
+ options.onDrop?.({ provider: key, reason });
131
+ return undefined;
132
+ };
133
+ if (!isRecord(raw))
134
+ return drop("invalid");
135
+ // Every provider-level string, not only the fields kept below: a brace
136
+ // anywhere means the entry was written for a templating system.
137
+ const { models: rawModels, ...fields } = raw;
138
+ if (hasBrace(key) || containsBrace(fields))
139
+ return drop("brace");
140
+ if (!PROVIDER_ID.test(key) || (raw.id !== undefined && raw.id !== key))
141
+ return drop("invalid");
142
+ const env = Array.isArray(raw.env) ? raw.env : [];
143
+ if (!env.every((name) => typeof name === "string" && ENV_NAME.test(name)))
144
+ return drop("invalid");
145
+ const models = idMap();
146
+ if (isRecord(rawModels)) {
147
+ let kept = 0;
148
+ for (const modelKey of Object.keys(rawModels).sort()) {
149
+ const rawModel = rawModels[modelKey];
150
+ if (hasBrace(modelKey) || containsBrace(rawModel)) {
151
+ options.onDrop?.({ provider: key, model: modelKey, reason: "brace" });
152
+ continue;
153
+ }
154
+ const model = trimModel(modelKey, rawModel);
155
+ if (model === "invalid")
156
+ options.onDrop?.({ provider: key, model: modelKey, reason: "invalid" });
157
+ if (typeof model === "string")
158
+ continue;
159
+ models[modelKey] = model;
160
+ if (++kept >= MAX_MODELS_PER_PROVIDER)
161
+ break;
162
+ }
163
+ }
164
+ // Field order is fixed (models last) so the snapshot text is stable.
165
+ const provider = { id: key, name: displayName(raw.name, key), env: env.slice(0, 8) };
166
+ if (typeof raw.npm === "string" && raw.npm.length <= 214 && !CONTROL.test(raw.npm))
167
+ provider.npm = raw.npm;
168
+ const api = apiUrl(raw.api);
169
+ if (api)
170
+ provider.api = api;
171
+ const doc = httpsUrl(raw.doc);
172
+ if (doc)
173
+ provider.doc = doc;
174
+ return { ...provider, models };
175
+ }
176
+ /** models.dev api.json (or an already-trimmed catalog) → the trimmed,
177
+ * brace-free provider map, sorted by provider and model id. Anything that is
178
+ * not an object yields an empty map. The provider map and every model map
179
+ * have no prototype, so looking up an id the catalog lacks gives undefined. */
180
+ export function trimModelsDevCatalog(raw, options = {}) {
181
+ const providers = idMap();
182
+ if (!isRecord(raw))
183
+ return providers;
184
+ for (const key of Object.keys(raw).sort().slice(0, MAX_PROVIDERS)) {
185
+ const provider = trimProvider(key, raw[key], options);
186
+ if (provider)
187
+ providers[key] = provider;
188
+ }
189
+ return providers;
190
+ }
191
+ /** Counts for logs and the build script's report. */
192
+ export function catalogStats(providers) {
193
+ let models = 0;
194
+ for (const provider of Object.values(providers))
195
+ models += Object.keys(provider.models).length;
196
+ return { providers: Object.keys(providers).length, models };
197
+ }
198
+ /** The on-disk form of a trimmed catalog. */
199
+ export function packCatalog(providers) {
200
+ return Object.fromEntries(Object.entries(providers).map(([id, provider]) => [id, {
201
+ ...provider,
202
+ models: Object.fromEntries(Object.entries(provider.models).map(([modelId, model]) => {
203
+ const { id: _id, tool_call: _toolCall, ...stored } = model;
204
+ return [modelId, stored];
205
+ })),
206
+ }]));
207
+ }
208
+ /** A stored catalog back in models.dev's shape, ready for trimModelsDevCatalog.
209
+ * It only restores the two packed fields; trimming still validates the rest. */
210
+ export function unpackCatalog(stored) {
211
+ if (!isRecord(stored))
212
+ return stored;
213
+ return Object.fromEntries(Object.entries(stored).map(([id, provider]) => {
214
+ if (!isRecord(provider) || !isRecord(provider.models))
215
+ return [id, provider];
216
+ const models = Object.fromEntries(Object.entries(provider.models).map(([modelId, model]) => [modelId, isRecord(model) ? { ...model, id: modelId, tool_call: true } : model]));
217
+ return [id, { ...provider, models }];
218
+ }));
219
+ }
@@ -6,10 +6,10 @@ 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))
9
+ // Opus 5.5 and Sonnet 5.5 are 1M without a `[1m]` suffix. Only the exact
10
+ // slug, or that slug after a slash or a provider dot. A longer suffix such
11
+ // as `claude-sonnet-5-5-local` stays on the generic Claude rule, as does `host::`.
12
+ if (/(?:^|[/.])claude-(?:opus|sonnet)-5(?:-5|\.5)$/.test(id))
13
13
  return 1_000_000;
14
14
  if (/claude/.test(id) || /^(opus|sonnet|haiku|fable)\b/.test(id))
15
15
  return /\[1m\]|-1m\b|1m-context/.test(id) ? 1_000_000 : 200_000;
@@ -15,6 +15,21 @@ function portableKey(value, fallback, used) {
15
15
  used.add(key);
16
16
  return key;
17
17
  }
18
+ /** A room's responder in package terms. An Auto room is written as its lead
19
+ * (its fallback, else its first exported member): that is how it behaves on
20
+ * any install without the decision model, and every older app can read it,
21
+ * where a new `auto` kind would make them refuse the whole package. */
22
+ function packageRoomResponder(responder, keys, members) {
23
+ if (responder.kind === "member" && keys.has(responder.botId))
24
+ return { kind: "agent", agent: keys.get(responder.botId) };
25
+ if (responder.kind === "everyone")
26
+ return { kind: "everyone" };
27
+ if (responder.kind === "auto") {
28
+ const lead = responder.fallbackBotId ? keys.get(responder.fallbackBotId) : undefined;
29
+ return { kind: "agent", agent: lead ?? members[0] };
30
+ }
31
+ return { kind: "mentions" };
32
+ }
18
33
  function samePlaybook(a, b) {
19
34
  return a.name === b.name && a.summary === b.summary && a.instructions === b.instructions &&
20
35
  a.triggers.join("\n") === b.triggers.join("\n");
@@ -122,11 +137,7 @@ export function createBotPackageExport(input) {
122
137
  const members = group.memberIds.flatMap((id) => idToKey.has(id) ? [idToKey.get(id)] : []);
123
138
  if (!members.length)
124
139
  continue;
125
- const defaultResponder = group.defaultResponder.kind === "member" && idToKey.has(group.defaultResponder.botId)
126
- ? { kind: "agent", agent: idToKey.get(group.defaultResponder.botId) }
127
- : group.defaultResponder.kind === "everyone"
128
- ? { kind: "everyone" }
129
- : { kind: "mentions" };
140
+ const defaultResponder = packageRoomResponder(group.defaultResponder, idToKey, members);
130
141
  rooms.push({
131
142
  key: portableKey(group.name, `room-${index + 1}`, roomKeys),
132
143
  name: group.name,
@@ -587,11 +598,7 @@ export function createTeamPackageExport(input) {
587
598
  const members = group.memberIds.flatMap((id) => botKeys.has(id) ? [botKeys.get(id)] : []);
588
599
  if (!members.length)
589
600
  continue;
590
- const defaultResponder = group.defaultResponder.kind === "member" && botKeys.has(group.defaultResponder.botId)
591
- ? { kind: "agent", agent: botKeys.get(group.defaultResponder.botId) }
592
- : group.defaultResponder.kind === "everyone"
593
- ? { kind: "everyone" }
594
- : { kind: "mentions" };
601
+ const defaultResponder = packageRoomResponder(group.defaultResponder, botKeys, members);
595
602
  const key = roomKeys.get(group.id);
596
603
  rooms.push({ key, name: group.name, members, ...(group.bulletin ? { bulletin: group.bulletin } : {}), defaultResponder });
597
604
  roomMembers.set(group.id, new Set(members));
@@ -318,9 +318,12 @@ function runImport(source, options, deps) {
318
318
  const ids = room.members.map((key) => botIds.get(key));
319
319
  let created = store.createGroup(room.name, ids, false, section);
320
320
  createdGroups.push(created);
321
- const defaultResponder = room.defaultResponder.kind === "agent"
322
- ? { kind: "member", botId: botIds.get(room.defaultResponder.agent) }
323
- : { kind: room.defaultResponder.kind };
321
+ const responder = room.defaultResponder;
322
+ const defaultResponder = responder.kind === "agent"
323
+ ? { kind: "member", botId: botIds.get(responder.agent) }
324
+ : responder.kind === "auto"
325
+ ? { kind: "auto", ...(responder.agent ? { fallbackBotId: botIds.get(responder.agent) } : {}) }
326
+ : { kind: responder.kind };
324
327
  created = store.patchGroup(created.id, {
325
328
  bulletin: room.bulletin ?? "",
326
329
  defaultResponder,
@@ -53,7 +53,9 @@ export const roomReleaseValues = (room) => ({
53
53
  members: [...room.members].sort(),
54
54
  defaultResponder: room.defaultResponder.kind === "agent"
55
55
  ? { kind: "agent", agent: room.defaultResponder.agent }
56
- : { kind: room.defaultResponder.kind },
56
+ : room.defaultResponder.kind === "auto" && room.defaultResponder.agent
57
+ ? { kind: "auto", agent: room.defaultResponder.agent }
58
+ : { kind: room.defaultResponder.kind },
57
59
  });
58
60
  /** One normal form for a routine's timing on both sides, so an untouched
59
61
  * routine compares equal: absent cap → null, absent overlap → "skip". */
@@ -1,6 +1,8 @@
1
- export const PROVIDER_KEY_KINDS = ["anthropic", "openaiCompat", "xai", "mistral"];
1
+ export const PROVIDER_KEY_KINDS = ["anthropic", "openai", "openrouter", "openaiCompat", "xai", "mistral"];
2
2
  const DEFAULT_URLS = {
3
3
  anthropic: "https://api.anthropic.com",
4
+ openai: "https://api.openai.com/v1",
5
+ openrouter: "https://openrouter.ai/api/v1",
4
6
  openaiCompat: "https://openrouter.ai/api/v1",
5
7
  xai: "https://api.x.ai/v1",
6
8
  mistral: "https://api.mistral.ai/v1",
@@ -44,7 +46,7 @@ export async function checkProviderKey(input, fetchImpl = fetch, timeoutMs = 10_
44
46
  if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopback(url.hostname))) {
45
47
  return { ok: false, reason: "unexpected" };
46
48
  }
47
- const authenticate = input.provider === "openaiCompat"
49
+ const authenticate = (input.provider === "openaiCompat" || input.provider === "openrouter")
48
50
  && url.origin === "https://openrouter.ai" && url.pathname === "/api/v1/models";
49
51
  if (authenticate)
50
52
  url.pathname = "/api/v1/key";
@@ -64,7 +64,7 @@ export function recentWorkLines(threads, latest) {
64
64
  }
65
65
  /** The bot's recent work outside the conversation it is in now. */
66
66
  export function recentWork(store, bot, opts) {
67
- const threads = botThreads(store, bot, opts.userName).filter((thread) => thread.threadId !== opts.currentThreadId);
67
+ const threads = botThreads(store, bot, opts.userName).filter((thread) => thread.threadId !== opts.currentThreadId && (!opts.include || opts.include(thread.threadId)));
68
68
  if (!threads.length)
69
69
  return [];
70
70
  const since = (opts.now ?? Date.now()) - (opts.windowMs ?? RECENT_WORK_WINDOW_MS);
@@ -60,6 +60,9 @@ export function serviceAllowed(method, path) {
60
60
  export function resolveLoopbackTrust(input) {
61
61
  const raw = (input.env ?? process.env).OMB_LOOPBACK_TRUST;
62
62
  const requested = raw?.trim().toLowerCase();
63
+ if (input.cloudHome) {
64
+ return { trust: "service", reason: "OMB Cloud home", ...(raw !== undefined && requested !== "service" ? { warning: "OMB_LOOPBACK_TRUST is ignored on an OMB Cloud home: a local request is always a service" } : {}) };
65
+ }
63
66
  if (input.desktopManaged) {
64
67
  return { trust: "owner", reason: "desktop app", ...(raw !== undefined ? { warning: "OMB_LOOPBACK_TRUST is ignored in the desktop app" } : {}) };
65
68
  }
@@ -156,6 +159,14 @@ export function isSameOrigin(req) {
156
159
  const own = requestOrigin(req);
157
160
  return own !== null && origin.trim().toLowerCase() === own;
158
161
  }
162
+ /** A browser says this request came from this origin's own page: an
163
+ * `Origin` equal to this request's origin, or `Sec-Fetch-Site: same-origin`.
164
+ * Unlike isSameOrigin, a request that carries neither does not pass. */
165
+ export function provesSameOrigin(req) {
166
+ if (headerValue(req.headers.origin))
167
+ return isSameOrigin(req);
168
+ return headerValue(req.headers["sec-fetch-site"])?.trim().toLowerCase() === "same-origin";
169
+ }
159
170
  /** Who to count a pairing attempt against. The server binds loopback, so a
160
171
  * remote client always arrives through a proxy or tunnel on this machine;
161
172
  * that proxy's X-Forwarded-For (Caddy overwrites any the client sent) names
@@ -239,6 +250,8 @@ export const CLIENT_ALLOW = [
239
250
  // with the feature off these paths are as unlisted as any other, so a
240
251
  // client session is refused exactly the way an unknown route refuses it.
241
252
  { methods: ["POST"], path: /^\/api\/shared-computers\/(?:connect|[\w-]+\/(?:poll|lease|result|disconnect))$/, feature: "sharedComputers" },
253
+ // What this person has lent (owner-scoped status, no secrets). Same gate.
254
+ { methods: ["GET"], path: /^\/api\/shared-computers$/, feature: "sharedComputers" },
242
255
  // liveness, identity, the stream
243
256
  { methods: ["GET"], path: /^\/api\/health$/ },
244
257
  { methods: ["GET"], path: /^\/api\/edition$/ },
@@ -264,6 +277,7 @@ export const CLIENT_ALLOW = [
264
277
  { methods: ["DELETE"], path: /^\/api\/bots\/[\w-]+\/queue\/[\w-]+$/ },
265
278
  { methods: ["POST"], path: /^\/api\/bots\/[\w-]+\/tasks$/ },
266
279
  { methods: ["POST", "PATCH", "DELETE"], path: /^\/api\/bots\/[\w-]+\/tasks\/[\w-]+$/ },
280
+ { methods: ["POST"], path: /^\/api\/bots\/[\w-]+\/tasks\/[\w-]+\/title$/ }, // Regenerate title: a rename by the bot's own engine
267
281
  { methods: ["PATCH"], path: /^\/api\/bots\/[\w-]+\/profile$/ },
268
282
  { methods: ["PATCH"], path: /^\/api\/bots\/[\w-]+$/ }, // display fields only: see clientBotPatchViolation
269
283
  // approvals and cards
@@ -347,6 +361,10 @@ function mutatingPublicRoute(method, path) {
347
361
  if (upper === "GET" &&
348
362
  /^\/api\/bots\/[\w-]+\/connector-cards\/[\w-]+\/status$/.test(path))
349
363
  return true;
364
+ // Remote viewer config and its WebSocket upgrade hand out a desktop's VNC
365
+ // password and control. Native owners open the direct viewer instead.
366
+ if (path.startsWith("/api/desktop-viewer/"))
367
+ return true;
350
368
  if (["GET", "HEAD", "OPTIONS"].includes(upper))
351
369
  return false;
352
370
  // Agent integrations have their own high-entropy, per-boot authorization
@@ -392,6 +410,13 @@ export function resolveRequestAuth(req, options) {
392
410
  if (session && via) {
393
411
  if (via === "cookie" && !isSameOrigin(req))
394
412
  return deny(403, "forbidden: cross-origin request");
413
+ // A browser sign-in's session is its browser's cookie, never a bearer token, and
414
+ // its changes come from this origin's own page, which a browser says so.
415
+ if (via === "bearer" && session.cookieOnly)
416
+ return deny(401, "unauthorized: this session belongs to a browser; sign in with a pairing code");
417
+ if (via === "cookie" && session.cookieOnly && !["GET", "HEAD", "OPTIONS"].includes(method) && !provesSameOrigin(req)) {
418
+ return deny(403, "forbidden: this browser's session makes changes only from its own page");
419
+ }
395
420
  const needed = requiredScope(method, path, options.features ?? {});
396
421
  if (!session.scopes.includes(needed)) {
397
422
  return deny(403, `forbidden: this session lacks the ${needed} scope`);
@@ -0,0 +1,45 @@
1
+ import { HOSTED_PROVIDER_SETTINGS_ERROR } from "../hosted-models.js";
2
+ import { PASS } from "./table.js";
3
+ const ROUTE = /^\/api\/instances\/([\w.-]+)\/leftover-files(\/remove)?$/u;
4
+ export const BUSY_LEFTOVER_ERROR = "Already looking for leftover files. Try again in a moment.";
5
+ export function createAntigravityLeftoverRoutes(deps) {
6
+ // One scan or delete at a time: both walk thousands of files.
7
+ let busy = false;
8
+ return async ({ req, res, path, method, json, readBody }) => {
9
+ const match = ROUTE.exec(path);
10
+ if (!match)
11
+ return PASS;
12
+ const removing = Boolean(match[2]);
13
+ if (method !== (removing ? "POST" : "GET"))
14
+ return PASS;
15
+ if (deps.hosted)
16
+ return json(res, 403, { error: HOSTED_PROVIDER_SETTINGS_ERROR });
17
+ if (removing) {
18
+ // Same non-simple-request gate as the other instance actions.
19
+ if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
20
+ return json(res, 415, { error: "content-type must be application/json" });
21
+ }
22
+ await readBody(req, 4096);
23
+ }
24
+ if (!deps.isAntigravity(match[1]))
25
+ return json(res, 404, { error: "Only Antigravity has leftover files to clear." });
26
+ if (busy)
27
+ return json(res, 409, { error: BUSY_LEFTOVER_ERROR });
28
+ busy = true;
29
+ res.setHeader("cache-control", "no-store");
30
+ try {
31
+ if (!removing) {
32
+ const scan = await deps.find();
33
+ return json(res, 200, { bytes: scan.bytes, folders: scan.folders.length, complete: scan.complete });
34
+ }
35
+ const removed = await deps.remove();
36
+ return json(res, 200, { freedBytes: removed.freedBytes, removed: removed.removed, remaining: removed.remaining });
37
+ }
38
+ catch (error) {
39
+ return json(res, 500, { error: error instanceof Error ? error.message : String(error) });
40
+ }
41
+ finally {
42
+ busy = false;
43
+ }
44
+ };
45
+ }