openmausbot 0.1.91 → 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 (150) hide show
  1. package/dist/assets/DesktopViewer-Do6lBa4C.js +5 -0
  2. package/dist/assets/{abnfDiagram-O67JEVCF-yP-w8pRq.js → abnfDiagram-O67JEVCF-Cd3X7Hxc.js} +1 -1
  3. package/dist/assets/{arc-DsivfZEh.js → arc-DIYc7Ymx.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CILw7lzT.js → architectureDiagram-NJMV4G6O-DBmRGVyP.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-DEQUV3HF.js → blockDiagram-BEXU5L5S-CPFePSEE.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-lSXCZoKA.js → c4Diagram-YGBWAQC7-DD_DO1qy.js} +1 -1
  7. package/dist/assets/channel-Dtk5Mtic.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-QCD5bgWP.js → chunk-2Q5K7J3B-BNEeNSjX.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-C5-iZJuL.js → chunk-3FUC2YCW-BbinLaqH.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-cxsawMpY.js → chunk-5VM5RSS4-BBgYrgDR.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-DV8tV8H7.js → chunk-GTNCS2PH-ezXj-kA-.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-CJFZyfbW.js → chunk-GWA4HPMP-9FxZ3diV.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-BigbMXco.js → chunk-JWPE2WC7-CO-40JpI.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-oVxTfpDy.js → chunk-NETBCI7D-BEhjslYE.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-CH4BiroW.js → chunk-WEXAMYUT-CNeq4n7_.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-KEDa6gax.js → chunk-XXDRQBXY-OsCY7KSi.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-BiPjfqVv.js → classDiagram-v2-NBCMYWYE-CloRUEtk.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-Caxew3OY.js → cose-bilkent-JH36ORCC-D1BGZhp4.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-B_Pui369.js → cynefin-OW5HDTMX-CUf4T2CN.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-X64ydIhP.js → cynefinDiagram-VND7K2PF-DPEZrFGA.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-TqYCI1E1.js → dagre-6A5THRUB-bDe6GIv5.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-TeOt3rAQ.js → diagram-22UHCM2B-CdcUQz55.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-D70BLhDA.js → diagram-3UASUU5V-BZpqGg7c.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-BbQDLwUC.js → diagram-ATOU4E4O-DvGM6ugp.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-ChNJZ1u-.js → diagram-CDSNMT55-DSTmMm0s.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-DMBl2APa.js → diagram-MLGK6HIB-15cucnMn.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-CEqDchOp.js → diagram-MPIPVDR6-tMbuaE6n.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-BVDKRp_-.js → ebnfDiagram-ZINNZB2B-BYxYzjHI.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-CRkofO9B.js → elk-276RUBZZ-v6bj852W.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-DruOMh5e.js → erDiagram-OPXOYQCR-5lBBlnd2.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-8GMQeB7t.js → flowDiagram-KWPJA3E3-CZyl1zIj.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-ORE9jcsM.js → ganttDiagram-FUAMR5RP-BiAI-ltZ.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-BmsedsWh.js → gitGraphDiagram-X574FWY7-BAZISkj3.js} +1 -1
  34. package/dist/assets/index-Cdmjrl28.css +1 -0
  35. package/dist/assets/{index-DKNEM8Q6.js → index-DGBaEXrM.js} +1 -1
  36. package/dist/assets/index-DR7YPMkW.js +597 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-E_p7AooJ.js → infoDiagram-VRGFBTTK-B6ooOy-Z.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-Y-B2mKda.js → ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-eA8RELcK.js → journeyDiagram-ZHPQQLJL-DWqlzIEe.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-DqIL2e8e.js → kanban-definition-PNTS6WVX-JX6piDwL.js} +1 -1
  41. package/dist/assets/{linear-yC_29R6e.js → linear-CmOWzX0P.js} +1 -1
  42. package/dist/assets/{mermaid.core-VhEMkSND.js → mermaid.core-CF1zwoQ-.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-oJ7kyQZy.js → mindmap-definition-NLK3R4M7-CGh2K4fW.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-BmfYcY0O.js → pegDiagram-GJSIUBJH-CDwYN0s7.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-DKvgP7w9.js → pieDiagram-5QR66LMP-1rfjJocB.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-BEKL3W0L.js → quadrantDiagram-O4NWA36T-tKPvPiok.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-BEsiqR5s.js → railroadDiagram-XR7U4H2S-BHLm5vXR.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-DWBcWvAP.js → requirementDiagram-PLB6GJNP-CB45STp7.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-rpZ2_O4s.js → sankeyDiagram-IPEJSGJF-55QA_huC.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-B99HQdJR.js → sequenceDiagram-PO4LG4MO-BckFmQHn.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-BjN0qT5T.js → sizeCapture-INFHLROL-BFCOJF11.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-BbsVgGsC.js → stateDiagram-v2-GCMORJYK-CZvmiVVN.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DG3gKkeR.js → swimlanes-2SLR337P-BpDUCEKy.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-M0GYW9Y3.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-BTHOdtF_.js → timeline-definition-EJHVYXUP-BpxHq6IK.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-acLSLFwc.js → usecaseDiagram-POWQR4AR-Cj66vm02.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-C5JzNX9G.js → vennDiagram-UO4OBE2U-C9nRkWE_.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-DL1-PrR_.js → wardleyDiagram-VNRHLVJA-D-me_nWj.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-Bvr1477p.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 +112 -14
  63. package/dist-server/container-mcp.js +195 -15
  64. package/dist-server/drivers/agents-proxy.js +5 -4
  65. package/dist-server/enterprise/server/index.js +5 -1
  66. package/dist-server/index.js +9300 -4155
  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 +3910 -1182
  71. package/dist-server/pair-cli.js +3910 -1182
  72. package/dist-server/server/boat.js +2 -2
  73. package/dist-server/server/browser-engine.js +10 -1
  74. package/dist-server/server/browser-live.js +46 -2
  75. package/dist-server/server/browser-runtime.js +74 -8
  76. package/dist-server/server/browser-tool-shape.js +6 -4
  77. package/dist-server/server/chief-of-staff.js +1 -1
  78. package/dist-server/server/cloud-home-start.js +118 -26
  79. package/dist-server/server/cloud-home.js +39 -3
  80. package/dist-server/server/cloud-lending.js +74 -10
  81. package/dist-server/server/cloud-owner.js +196 -0
  82. package/dist-server/server/cloud-secrets-boot.js +6 -0
  83. package/dist-server/server/cloud-secrets.js +65 -0
  84. package/dist-server/server/config.js +174 -14
  85. package/dist-server/server/container-computer.js +3 -2
  86. package/dist-server/server/desktop-viewer-targets.js +26 -0
  87. package/dist-server/server/drivers/acp/core.js +167 -21
  88. package/dist-server/server/drivers/acp/opencode-go.js +614 -85
  89. package/dist-server/server/drivers/agents-catalog.js +6 -6
  90. package/dist-server/server/drivers/antigravity-acp.js +69 -11
  91. package/dist-server/server/drivers/antigravity-temp.js +360 -0
  92. package/dist-server/server/drivers/antigravity.js +21 -0
  93. package/dist-server/server/drivers/chatgpt-plan-auth.js +481 -0
  94. package/dist-server/server/drivers/claude.js +312 -34
  95. package/dist-server/server/drivers/codex.js +228 -18
  96. package/dist-server/server/drivers/grok.js +2 -2
  97. package/dist-server/server/drivers/mistral.js +4 -3
  98. package/dist-server/server/drivers/openai-chat-protocol.js +4 -3
  99. package/dist-server/server/drivers/openai-chat.js +41 -3
  100. package/dist-server/server/drivers/openai-compat.js +73 -23
  101. package/dist-server/server/engine-install.js +46 -12
  102. package/dist-server/server/env-path.js +15 -3
  103. package/dist-server/server/harness/registry.js +2 -2
  104. package/dist-server/server/index.js +1338 -184
  105. package/dist-server/server/lending-memory.js +479 -0
  106. package/dist-server/server/local-vm-idle.js +24 -6
  107. package/dist-server/server/managed-desktop.js +3 -0
  108. package/dist-server/server/mcp-bridge.js +5 -3
  109. package/dist-server/server/mcp-http.js +6 -3
  110. package/dist-server/server/mcp-oauth-discovery.js +133 -0
  111. package/dist-server/server/mcp-oauth-store.js +70 -0
  112. package/dist-server/server/mcp-oauth.js +550 -0
  113. package/dist-server/server/mcp-probe.js +14 -1
  114. package/dist-server/server/mcp-registry.js +43 -0
  115. package/dist-server/server/memory-upkeep.js +94 -84
  116. package/dist-server/server/model-catalog/catalog.js +374 -0
  117. package/dist-server/server/model-catalog/presets.js +176 -0
  118. package/dist-server/server/model-catalog/trim.js +219 -0
  119. package/dist-server/server/provider-key-check.js +4 -2
  120. package/dist-server/server/recent-work.js +1 -1
  121. package/dist-server/server/request-auth.js +23 -0
  122. package/dist-server/server/routes/antigravity-leftovers.js +45 -0
  123. package/dist-server/server/routes/bot-memory.js +31 -6
  124. package/dist-server/server/routes/desktop-viewer.js +194 -0
  125. package/dist-server/server/routine-requests.js +1 -1
  126. package/dist-server/server/routines.js +1 -1
  127. package/dist-server/server/sessions.js +56 -9
  128. package/dist-server/server/skills.js +25 -10
  129. package/dist-server/server/store.js +62 -122
  130. package/dist-server/server/thread-events.js +2 -0
  131. package/dist-server/server/thread-starters.js +33 -0
  132. package/dist-server/server/thread-title.js +46 -0
  133. package/dist-server/server/tts/fish.js +4 -5
  134. package/dist-server/server/tts/index.js +5 -3
  135. package/dist-server/server/vps-computer.js +79 -12
  136. package/dist-server/server/webhooks.js +18 -1
  137. package/dist-server/server/workspace-backup-policy.js +3 -0
  138. package/dist-server/server/workspace-backup.js +3 -0
  139. package/dist-server/server/workspace.js +65 -15
  140. package/dist-server/shared/workspace-backup-client.js +1 -0
  141. package/dist-server/src/lib/citations.js +177 -0
  142. package/dist-server/src/lib/composer-attachments.js +7 -1
  143. package/dist-server/vps-container-mcp.js +196 -15
  144. package/dist-server/workspace-backup.worker.js +73 -9
  145. package/enterprise/server/index.js +5 -1
  146. package/package.json +1 -1
  147. package/dist/assets/channel-DrfGMu7X.js +0 -1
  148. package/dist/assets/index-BlnCEcU6.js +0 -588
  149. package/dist/assets/index-DVxjOvID.css +0 -1
  150. package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.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
+ }
@@ -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
@@ -266,6 +277,7 @@ export const CLIENT_ALLOW = [
266
277
  { methods: ["DELETE"], path: /^\/api\/bots\/[\w-]+\/queue\/[\w-]+$/ },
267
278
  { methods: ["POST"], path: /^\/api\/bots\/[\w-]+\/tasks$/ },
268
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
269
281
  { methods: ["PATCH"], path: /^\/api\/bots\/[\w-]+\/profile$/ },
270
282
  { methods: ["PATCH"], path: /^\/api\/bots\/[\w-]+$/ }, // display fields only: see clientBotPatchViolation
271
283
  // approvals and cards
@@ -349,6 +361,10 @@ function mutatingPublicRoute(method, path) {
349
361
  if (upper === "GET" &&
350
362
  /^\/api\/bots\/[\w-]+\/connector-cards\/[\w-]+\/status$/.test(path))
351
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;
352
368
  if (["GET", "HEAD", "OPTIONS"].includes(upper))
353
369
  return false;
354
370
  // Agent integrations have their own high-entropy, per-boot authorization
@@ -394,6 +410,13 @@ export function resolveRequestAuth(req, options) {
394
410
  if (session && via) {
395
411
  if (via === "cookie" && !isSameOrigin(req))
396
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
+ }
397
420
  const needed = requiredScope(method, path, options.features ?? {});
398
421
  if (!session.scopes.includes(needed)) {
399
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
+ }
@@ -41,6 +41,7 @@ export function createBotMemoryRoutes(deps) {
41
41
  return async ({ req, res, url, path, method, auth, json, readBody }) => {
42
42
  /** scratch for route matches, shared by every `path.match` below */
43
43
  let m = null;
44
+ const write = (botId, change) => deps.ownersWrite ? deps.ownersWrite(auth, botId, change) : change();
44
45
  m = path.match(/^\/api\/bots\/([\w-]+)\/memory$/);
45
46
  if (m && method === "GET") {
46
47
  if (!deps.bot(m[1]))
@@ -49,7 +50,9 @@ export function createBotMemoryRoutes(deps) {
49
50
  const overview = memoryOverview(m[1]);
50
51
  // `text` and `truncated` ride along one release for clients of the
51
52
  // old whole-file shape; the panel reads the file through /memory/file
52
- return json(res, 200, { ...overview, text: readMemoryDoc(m[1], MEMORY_INDEX).text, truncated: overview.index.truncated });
53
+ const review = deps.lendingReview?.needed(m[1]);
54
+ return json(res, 200, { ...overview, text: readMemoryDoc(m[1], MEMORY_INDEX).text, truncated: overview.index.truncated,
55
+ ...(review ? { lendingReview: review } : {}) });
53
56
  }
54
57
  catch (error) {
55
58
  return replyMemoryError(res, error);
@@ -65,7 +68,8 @@ export function createBotMemoryRoutes(deps) {
65
68
  if (!parsed.success)
66
69
  return json(res, 400, { error: "text must be a string" });
67
70
  try {
68
- const { doc } = journalMemoryWrite(m[1], MEMORY_INDEX, parsed.data.text, { actor: "person", via: "api" });
71
+ const botId = m[1];
72
+ const { doc } = write(botId, () => journalMemoryWrite(botId, MEMORY_INDEX, parsed.data.text, { actor: "person", via: "api" }));
69
73
  return json(res, 200, { ok: true, hash: doc.hash, truncated: memoryCapacity(doc.text).truncated });
70
74
  }
71
75
  catch (error) {
@@ -76,6 +80,24 @@ export function createBotMemoryRoutes(deps) {
76
80
  return replyMemoryError(res, error);
77
81
  }
78
82
  }
83
+ m = path.match(/^\/api\/bots\/([\w-]+)\/memory\/reviewed$/);
84
+ if (m && method === "POST" && deps.lendingReview) {
85
+ if (!deps.bot(m[1]))
86
+ return json(res, 404, { error: "no such bot" });
87
+ if (!deps.lendingReview.owner(auth)) {
88
+ return json(res, 403, { error: "Only the owner of this Cloud can review this bot's memory, from one of their own devices." });
89
+ }
90
+ // Bound to what the owner was shown: the token names the files exactly
91
+ // as the panel read them. Anything changed since is shown again.
92
+ const parsed = z.object({ token: z.string().regex(/^[a-f0-9]{64}$/) }).safeParse(await readBody(req));
93
+ if (!parsed.success)
94
+ return json(res, 400, { error: "token is required: the lendingReview token the Memory panel was shown" });
95
+ if (!deps.lendingReview.accept(m[1], parsed.data.token)) {
96
+ return json(res, 409, { error: "This bot's memory changed since you looked. Review it again.", code: "changed",
97
+ lendingReview: deps.lendingReview.needed(m[1]) });
98
+ }
99
+ return json(res, 200, { ok: true, lendingReview: false });
100
+ }
79
101
  m = path.match(/^\/api\/bots\/([\w-]+)\/memory\/file$/);
80
102
  if (m && method === "GET") {
81
103
  if (!deps.bot(m[1]))
@@ -96,11 +118,12 @@ export function createBotMemoryRoutes(deps) {
96
118
  if (!parsed.success)
97
119
  return json(res, 400, { error: "text must be a string; path and expectedHash are optional strings" });
98
120
  try {
99
- const { doc, entry } = journalMemoryWrite(m[1], parsed.data.path, parsed.data.text, {
121
+ const botId = m[1];
122
+ const { doc, entry } = write(botId, () => journalMemoryWrite(botId, parsed.data.path, parsed.data.text, {
100
123
  actor: "person",
101
124
  via: "ui",
102
125
  expectedHash: parsed.data.expectedHash,
103
- });
126
+ }));
104
127
  return json(res, 200, { ok: true, ...doc, entry: entry ? journalEntryForClient(m[1], entry) : null, overview: memoryOverview(m[1]) });
105
128
  }
106
129
  catch (error) {
@@ -112,7 +135,8 @@ export function createBotMemoryRoutes(deps) {
112
135
  return json(res, 404, { error: "no such bot" });
113
136
  const file = url.searchParams.get("path") ?? "";
114
137
  try {
115
- const entry = journalMemoryDelete(m[1], file, { actor: "person", via: "ui" });
138
+ const botId = m[1];
139
+ const entry = write(botId, () => journalMemoryDelete(botId, file, { actor: "person", via: "ui" }));
116
140
  return json(res, 200, { ok: true, path: file, entry: entry ? journalEntryForClient(m[1], entry) : null, overview: memoryOverview(m[1]) });
117
141
  }
118
142
  catch (error) {
@@ -134,7 +158,8 @@ export function createBotMemoryRoutes(deps) {
134
158
  if (!deps.bot(m[1]))
135
159
  return json(res, 404, { error: "no such bot" });
136
160
  await flushMemoryJournal(m[1]);
137
- const result = revertMemoryChange(m[1], m[2]);
161
+ const botId = m[1], changeId = m[2];
162
+ const result = write(botId, () => revertMemoryChange(botId, changeId));
138
163
  if (!result.ok)
139
164
  return json(res, result.status, { error: result.error });
140
165
  return json(res, 200, { ok: true, ...result.doc, entry: result.entry ? journalEntryForClient(m[1], result.entry) : null, overview: memoryOverview(m[1]) });