@stage-labs/metro 0.1.0-beta.21 → 0.1.0-beta.210

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 (436) hide show
  1. package/dist/api.js +9 -101
  2. package/dist/claude-settings.js +40 -0
  3. package/dist/claude.js +152 -0
  4. package/dist/cli.js +52 -109
  5. package/dist/control.js +47 -75
  6. package/dist/hold.js +210 -0
  7. package/dist/local.js +43 -0
  8. package/dist/mcp-config.js +25 -0
  9. package/dist/node-name.js +71 -0
  10. package/dist/onboarding.js +44 -0
  11. package/dist/provider-flags.js +9 -0
  12. package/dist/route.js +58 -0
  13. package/dist/runtime-install.js +96 -0
  14. package/dist/runtime.js +29 -52
  15. package/dist/serve.js +157 -0
  16. package/dist/service.js +304 -0
  17. package/dist/tail.js +119 -0
  18. package/dist/update.js +13 -1
  19. package/package.json +2 -23
  20. package/runtime/marketplace/.claude-plugin/marketplace.json +12 -0
  21. package/runtime/marketplace/plugin/.claude-plugin/plugin.json +17 -0
  22. package/runtime/marketplace/plugin/.mcp.json +1 -0
  23. package/runtime/marketplace/plugin/bin/guard.mjs +71 -0
  24. package/runtime/marketplace/plugin/bin/metro-plugin.mjs +30 -0
  25. package/runtime/marketplace/plugin/bin/policy.mjs +97 -0
  26. package/runtime/marketplace/plugin/bin/session-start.mjs +31 -0
  27. package/runtime/marketplace/plugin/hooks/hooks.json +25 -0
  28. package/runtime/marketplace/plugin/orchestrator.md +26 -0
  29. package/runtime/node_modules/@metro-labs/core/package.json +47 -0
  30. package/runtime/node_modules/@metro-labs/core/src/endpoints.ts +7 -0
  31. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → core/src}/events.ts +11 -7
  32. package/runtime/node_modules/@metro-labs/{mcp/src/db → core/src}/ids.ts +5 -0
  33. package/runtime/node_modules/@metro-labs/core/src/is-record.ts +2 -0
  34. package/runtime/node_modules/@metro-labs/{mcp/src/stations → core/src}/lines.ts +24 -28
  35. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → core/src}/protocol.ts +7 -4
  36. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → core/src}/secure-fs.ts +17 -0
  37. package/runtime/node_modules/@metro-labs/core/src/station-names.ts +13 -0
  38. package/runtime/node_modules/@metro-labs/core/src/stations/account-files.ts +41 -0
  39. package/runtime/node_modules/@metro-labs/core/src/stations/account-store.ts +83 -0
  40. package/runtime/node_modules/@metro-labs/{mcp → core}/src/stations/attachments.ts +0 -1
  41. package/runtime/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +16 -0
  42. package/runtime/node_modules/@metro-labs/core/src/stations/profile.ts +58 -0
  43. package/runtime/node_modules/@metro-labs/core/src/stations/sender-profile.ts +64 -0
  44. package/runtime/node_modules/@metro-labs/{mcp → core}/src/stations/station-runtime.ts +6 -11
  45. package/runtime/node_modules/@metro-labs/core/src/stations/train-boot.ts +42 -0
  46. package/runtime/node_modules/@metro-labs/core/src/stations/train-events.ts +84 -0
  47. package/runtime/node_modules/@metro-labs/{mcp → core}/src/stations/types.ts +18 -1
  48. package/runtime/node_modules/@metro-labs/core/src/tickets.ts +37 -0
  49. package/runtime/node_modules/@metro-labs/core/src/version.ts +2 -0
  50. package/runtime/node_modules/@metro-labs/daemon/package.json +48 -0
  51. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/agent-fs.ts +134 -0
  52. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/files-api.ts +12 -0
  53. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/files.ts +89 -0
  54. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/home-fs.ts +113 -0
  55. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/job-env.ts +51 -0
  56. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/provision.ts +80 -0
  57. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedule-detail.ts +99 -0
  58. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules-api.ts +27 -0
  59. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules.ts +144 -0
  60. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/unit-files.ts +37 -0
  61. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/user.ts +89 -0
  62. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/view.ts +87 -0
  63. package/runtime/node_modules/@metro-labs/daemon/src/agents/account-attach.ts +19 -0
  64. package/runtime/node_modules/@metro-labs/daemon/src/agents/account-lookups.ts +55 -0
  65. package/runtime/node_modules/@metro-labs/daemon/src/agents/account-routes.ts +76 -0
  66. package/runtime/node_modules/@metro-labs/daemon/src/agents/accounts-api.ts +357 -0
  67. package/runtime/node_modules/@metro-labs/daemon/src/agents/admin.ts +30 -0
  68. package/runtime/node_modules/@metro-labs/daemon/src/agents/allowlist.ts +59 -0
  69. package/runtime/node_modules/@metro-labs/daemon/src/agents/api.ts +131 -0
  70. package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +132 -0
  71. package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +305 -0
  72. package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +142 -0
  73. package/runtime/node_modules/@metro-labs/{mcp/src/db/key-map.ts → daemon/src/agents/keys.ts} +1 -11
  74. package/runtime/node_modules/@metro-labs/{mcp/src/db/agent-map.ts → daemon/src/agents/map.ts} +58 -1
  75. package/runtime/node_modules/@metro-labs/daemon/src/agents/policy-route.ts +27 -0
  76. package/runtime/node_modules/@metro-labs/{mcp/src/db/agent-scope.ts → daemon/src/agents/scope.ts} +2 -2
  77. package/runtime/node_modules/@metro-labs/daemon/src/agents/sender-cards.ts +94 -0
  78. package/runtime/node_modules/@metro-labs/daemon/src/agents/senders.ts +38 -0
  79. package/runtime/node_modules/@metro-labs/daemon/src/approvals/api.ts +55 -0
  80. package/runtime/node_modules/@metro-labs/daemon/src/approvals/pending.ts +150 -0
  81. package/runtime/node_modules/@metro-labs/daemon/src/approvals/preview.ts +66 -0
  82. package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +218 -0
  83. package/runtime/node_modules/@metro-labs/daemon/src/boot/connect-hint.ts +36 -0
  84. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/boot}/crash-guard.ts +1 -1
  85. package/runtime/node_modules/@metro-labs/daemon/src/boot/local-owner.ts +21 -0
  86. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/boot}/paths.ts +6 -34
  87. package/runtime/node_modules/@metro-labs/daemon/src/channels/addressed.ts +26 -0
  88. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/channels/inbound.ts +70 -45
  89. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/channels/media-note.ts +11 -7
  90. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/channels/pending.ts +4 -1
  91. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/channels/relay.ts +2 -2
  92. package/runtime/node_modules/@metro-labs/daemon/src/claude/api.ts +355 -0
  93. package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +359 -0
  94. package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +208 -0
  95. package/runtime/node_modules/@metro-labs/daemon/src/claude/memory.ts +32 -0
  96. package/runtime/node_modules/@metro-labs/daemon/src/claude/onboarding.ts +60 -0
  97. package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-install.ts +149 -0
  98. package/runtime/node_modules/@metro-labs/daemon/src/claude/session-files.ts +75 -0
  99. package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +284 -0
  100. package/runtime/node_modules/@metro-labs/daemon/src/claude/settings.ts +109 -0
  101. package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +273 -0
  102. package/runtime/node_modules/@metro-labs/daemon/src/claude/skills.ts +120 -0
  103. package/runtime/node_modules/@metro-labs/daemon/src/claude/version.ts +113 -0
  104. package/runtime/node_modules/@metro-labs/{mcp/src/daemon/connector-api.ts → daemon/src/connectors/api.ts} +79 -113
  105. package/runtime/node_modules/@metro-labs/{mcp/src/db/connector-config.ts → daemon/src/connectors/config.ts} +67 -41
  106. package/runtime/node_modules/@metro-labs/daemon/src/connectors/gates.ts +72 -0
  107. package/runtime/node_modules/@metro-labs/daemon/src/connectors/health.ts +18 -0
  108. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/connectors}/oauth-client.ts +14 -17
  109. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/connectors}/oauth-discovery.ts +33 -20
  110. package/runtime/node_modules/@metro-labs/{mcp/src/daemon/connector-oauth-routes.ts → daemon/src/connectors/oauth-routes.ts} +30 -32
  111. package/runtime/node_modules/@metro-labs/{mcp/src/daemon/connector-oauth.ts → daemon/src/connectors/oauth.ts} +61 -22
  112. package/runtime/node_modules/@metro-labs/daemon/src/connectors/plugin-sync.ts +131 -0
  113. package/runtime/node_modules/@metro-labs/daemon/src/connectors/reach.ts +27 -0
  114. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-policy.ts +90 -0
  115. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +347 -0
  116. package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +385 -0
  117. package/runtime/node_modules/@metro-labs/daemon/src/connectors/tools.ts +81 -0
  118. package/runtime/node_modules/@metro-labs/daemon/src/connectors/url.ts +37 -0
  119. package/runtime/node_modules/@metro-labs/daemon/src/connectors/verify.ts +106 -0
  120. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/files}/attach-grant.ts +4 -4
  121. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/files}/attach-owner.ts +3 -3
  122. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-reaper.ts +174 -0
  123. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/files}/attach-serve.ts +12 -13
  124. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/files}/upload-api.ts +10 -27
  125. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/files}/upload-store.ts +4 -4
  126. package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +233 -0
  127. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +208 -0
  128. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-device.ts +91 -0
  129. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-stream.ts +173 -0
  130. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-translate.ts +195 -0
  131. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-version.ts +44 -0
  132. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +128 -0
  133. package/runtime/node_modules/@metro-labs/daemon/src/gateway/effort.ts +77 -0
  134. package/runtime/node_modules/@metro-labs/daemon/src/gateway/eventstream.ts +123 -0
  135. package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +237 -0
  136. package/runtime/node_modules/@metro-labs/daemon/src/gateway/frames.ts +118 -0
  137. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +274 -0
  138. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-auth.ts +160 -0
  139. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-client.ts +46 -0
  140. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-setup.ts +150 -0
  141. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-stream.ts +139 -0
  142. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-translate.ts +224 -0
  143. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini.ts +208 -0
  144. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +199 -0
  145. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +278 -0
  146. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-signin.ts +193 -0
  147. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-store.ts +70 -0
  148. package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +90 -0
  149. package/runtime/node_modules/@metro-labs/daemon/src/gateway/pkce.ts +36 -0
  150. package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +66 -0
  151. package/runtime/node_modules/@metro-labs/daemon/src/gateway/served.ts +18 -0
  152. package/runtime/node_modules/@metro-labs/daemon/src/gateway/subscription.ts +120 -0
  153. package/runtime/node_modules/@metro-labs/daemon/src/gateway/text.ts +14 -0
  154. package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage.ts +244 -0
  155. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/accounts.ts +28 -17
  156. package/runtime/node_modules/@metro-labs/daemon/src/mcp/attachment-schema.ts +91 -0
  157. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/call-tools.ts +49 -20
  158. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/ctx.ts +4 -4
  159. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/event-store.ts +15 -16
  160. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/group-tools.ts +2 -2
  161. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/index.ts +24 -79
  162. package/runtime/node_modules/@metro-labs/daemon/src/mcp/instructions.ts +19 -0
  163. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/member-tools.ts +2 -2
  164. package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-prompt.ts +87 -0
  165. package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-relay.ts +79 -0
  166. package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-gate.ts +82 -0
  167. package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-snapshot.ts +79 -0
  168. package/runtime/node_modules/@metro-labs/daemon/src/mcp/profile-lookup.ts +60 -0
  169. package/runtime/node_modules/@metro-labs/daemon/src/mcp/profile-tool.ts +96 -0
  170. package/runtime/node_modules/@metro-labs/daemon/src/mcp/read-tool.ts +87 -0
  171. package/runtime/node_modules/@metro-labs/daemon/src/mcp/request-identity.ts +46 -0
  172. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-slot.ts +48 -0
  173. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/session.ts +33 -42
  174. package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-catalog.ts +88 -0
  175. package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-def.ts +9 -0
  176. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/tool-dispatch.ts +53 -40
  177. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/tool-schemas.ts +68 -119
  178. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/upload-tool.ts +6 -11
  179. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/helper-script.ts +126 -0
  180. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/install.ts +37 -0
  181. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/privilege.ts +48 -0
  182. package/runtime/node_modules/@metro-labs/daemon/src/monitor/api.ts +132 -0
  183. package/runtime/node_modules/@metro-labs/daemon/src/net/tunnel.ts +316 -0
  184. package/runtime/node_modules/@metro-labs/daemon/src/policy/policy.ts +118 -0
  185. package/runtime/node_modules/@metro-labs/daemon/src/routes/bearer.ts +35 -0
  186. package/runtime/node_modules/@metro-labs/daemon/src/routes/body.ts +24 -0
  187. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/routes}/http.ts +59 -51
  188. package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +205 -0
  189. package/runtime/node_modules/@metro-labs/daemon/src/routes/session-apis.ts +66 -0
  190. package/runtime/node_modules/@metro-labs/daemon/src/routes/session.ts +29 -0
  191. package/runtime/node_modules/@metro-labs/daemon/src/routes/threema-callback.ts +103 -0
  192. package/runtime/node_modules/@metro-labs/daemon/src/server/control.ts +39 -0
  193. package/runtime/node_modules/@metro-labs/daemon/src/server/machine.ts +58 -0
  194. package/runtime/node_modules/@metro-labs/daemon/src/server/owner.ts +26 -0
  195. package/runtime/node_modules/@metro-labs/daemon/src/server/update.ts +90 -0
  196. package/runtime/node_modules/@metro-labs/daemon/src/server.ts +2 -0
  197. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-driver.ts +42 -0
  198. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/stations/attach-inline.ts +29 -1
  199. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/stations/attach-interactive.ts +9 -36
  200. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-outlook.ts +63 -0
  201. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/stations/attach-resolve.ts +38 -13
  202. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/stations}/attach-session.ts +32 -22
  203. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/stations/attach-xmtp.ts +8 -9
  204. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/stations/attach.ts +75 -9
  205. package/runtime/node_modules/@metro-labs/daemon/src/stations/materialize.ts +265 -0
  206. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/stations/registry.ts +22 -6
  207. package/runtime/node_modules/@metro-labs/daemon/src/stations/runtime-deps.ts +82 -0
  208. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/stations}/supervisor-io.ts +3 -3
  209. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/stations}/supervisor.ts +6 -9
  210. package/runtime/node_modules/@metro-labs/daemon/src/stations/threema-callbacks.ts +32 -0
  211. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/stations}/train-call.ts +1 -1
  212. package/runtime/node_modules/@metro-labs/daemon/src/stations/webhook-endpoints.ts +51 -0
  213. package/runtime/node_modules/@metro-labs/daemon/src/terminal/api.ts +84 -0
  214. package/runtime/node_modules/@metro-labs/daemon/src/terminal/socket.ts +124 -0
  215. package/runtime/node_modules/@metro-labs/daemon/src/terminal/tickets.ts +15 -0
  216. package/runtime/node_modules/@metro-labs/daemon/src/vault/api.ts +45 -0
  217. package/runtime/node_modules/@metro-labs/daemon/src/vault/config.ts +51 -0
  218. package/runtime/node_modules/@metro-labs/daemon/src/vault/firewall.ts +9 -0
  219. package/runtime/node_modules/@metro-labs/daemon/src/vault/index.ts +96 -0
  220. package/runtime/node_modules/@metro-labs/daemon/src/vault/install.ts +56 -0
  221. package/runtime/node_modules/@metro-labs/daemon/src/vault/paths.ts +26 -0
  222. package/runtime/node_modules/@metro-labs/daemon/src/vault/proxy.ts +147 -0
  223. package/runtime/node_modules/@metro-labs/daemon/src/vault/store.ts +129 -0
  224. package/runtime/node_modules/@metro-labs/daemon/src/vault/trust.ts +44 -0
  225. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/package.json +3 -5
  226. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/accounts.ts +8 -28
  227. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/actions.ts +19 -187
  228. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/attachments.ts +3 -3
  229. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/format.ts +31 -72
  230. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/group-actions.ts +2 -2
  231. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/group.ts +2 -2
  232. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/index.ts +15 -21
  233. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/members.ts +2 -2
  234. package/runtime/node_modules/@metro-labs/discord-bot/src/profile.ts +68 -0
  235. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/send-files.ts +12 -13
  236. package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +23 -0
  237. package/runtime/node_modules/@metro-labs/http/package.json +29 -0
  238. package/runtime/node_modules/@metro-labs/http/src/api-http.ts +135 -0
  239. package/runtime/node_modules/@metro-labs/{mcp/src/daemon/session-api.ts → http/src/mode-api.ts} +13 -9
  240. package/runtime/node_modules/@metro-labs/http/src/return-to.ts +21 -0
  241. package/runtime/node_modules/@metro-labs/http/src/workos-token.ts +149 -0
  242. package/runtime/node_modules/@metro-labs/{telegram-user → outlook}/package.json +2 -5
  243. package/runtime/node_modules/@metro-labs/outlook/src/accounts.ts +93 -0
  244. package/runtime/node_modules/@metro-labs/outlook/src/actions.ts +14 -0
  245. package/runtime/node_modules/@metro-labs/outlook/src/attachments.ts +97 -0
  246. package/runtime/node_modules/@metro-labs/outlook/src/auth.ts +150 -0
  247. package/runtime/node_modules/@metro-labs/outlook/src/browser.ts +53 -0
  248. package/runtime/node_modules/@metro-labs/outlook/src/config.ts +36 -0
  249. package/runtime/node_modules/@metro-labs/outlook/src/format.ts +140 -0
  250. package/runtime/node_modules/@metro-labs/outlook/src/graph.ts +60 -0
  251. package/runtime/node_modules/@metro-labs/outlook/src/html.ts +72 -0
  252. package/runtime/node_modules/@metro-labs/outlook/src/inbound.ts +104 -0
  253. package/runtime/node_modules/@metro-labs/outlook/src/index.ts +12 -0
  254. package/runtime/node_modules/@metro-labs/outlook/src/login.ts +150 -0
  255. package/runtime/node_modules/@metro-labs/outlook/src/me.ts +41 -0
  256. package/runtime/node_modules/@metro-labs/outlook/src/outbound.ts +116 -0
  257. package/runtime/node_modules/@metro-labs/outlook/src/query.ts +119 -0
  258. package/runtime/node_modules/@metro-labs/outlook/src/state.ts +43 -0
  259. package/runtime/node_modules/@metro-labs/outlook/src/station.ts +15 -0
  260. package/runtime/node_modules/@metro-labs/outlook/src/trust.ts +70 -0
  261. package/runtime/node_modules/@metro-labs/telegram/package.json +3 -2
  262. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +38 -96
  263. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +214 -253
  264. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +67 -108
  265. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/client.ts +6 -6
  266. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +101 -147
  267. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/group.ts +4 -4
  268. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/history.ts +1 -1
  269. package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +57 -0
  270. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +6 -102
  271. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/login.ts +20 -20
  272. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +64 -151
  273. package/runtime/node_modules/@metro-labs/telegram/src/members.ts +61 -33
  274. package/runtime/node_modules/@metro-labs/telegram/src/profile.ts +70 -0
  275. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/reactions.ts +3 -3
  276. package/runtime/node_modules/@metro-labs/telegram/src/station.ts +11 -1
  277. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +5 -47
  278. package/runtime/node_modules/@metro-labs/telegram-bot/package.json +25 -0
  279. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +86 -0
  280. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +240 -0
  281. package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
  282. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +170 -0
  283. package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +103 -0
  284. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +90 -0
  285. package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
  286. package/runtime/node_modules/@metro-labs/telegram-bot/src/profile.ts +85 -0
  287. package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +21 -0
  288. package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +56 -0
  289. package/runtime/node_modules/@metro-labs/threema/package.json +26 -0
  290. package/runtime/node_modules/@metro-labs/threema/src/accounts.ts +95 -0
  291. package/runtime/node_modules/@metro-labs/threema/src/actions.ts +124 -0
  292. package/runtime/node_modules/@metro-labs/threema/src/api.ts +152 -0
  293. package/runtime/node_modules/@metro-labs/threema/src/crypto.ts +100 -0
  294. package/runtime/node_modules/@metro-labs/threema/src/files.ts +47 -0
  295. package/runtime/node_modules/@metro-labs/threema/src/format.ts +164 -0
  296. package/runtime/node_modules/@metro-labs/threema/src/groups.ts +96 -0
  297. package/runtime/node_modules/@metro-labs/threema/src/ids.ts +21 -0
  298. package/runtime/node_modules/@metro-labs/threema/src/inbound.ts +89 -0
  299. package/runtime/node_modules/@metro-labs/threema/src/index.ts +24 -0
  300. package/runtime/node_modules/@metro-labs/threema/src/messages.ts +264 -0
  301. package/runtime/node_modules/@metro-labs/threema/src/outbound.ts +160 -0
  302. package/runtime/node_modules/@metro-labs/threema/src/station.ts +13 -0
  303. package/runtime/node_modules/@metro-labs/threema/src/verify.ts +117 -0
  304. package/runtime/node_modules/@metro-labs/webhook/package.json +1 -1
  305. package/runtime/node_modules/@metro-labs/webhook/src/station.ts +5 -8
  306. package/runtime/node_modules/@metro-labs/whatsapp/package.json +2 -5
  307. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +6 -28
  308. package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +1 -1
  309. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +54 -11
  310. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +2 -2
  311. package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +1 -1
  312. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +86 -4
  313. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +7 -58
  314. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +11 -28
  315. package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +4 -37
  316. package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +1 -1
  317. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +1 -1
  318. package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +1 -1
  319. package/runtime/node_modules/@metro-labs/whatsapp/src/names.ts +75 -0
  320. package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +44 -0
  321. package/runtime/node_modules/@metro-labs/whatsapp/src/resolve.ts +52 -0
  322. package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +16 -1
  323. package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +5 -9
  324. package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +0 -1
  325. package/runtime/node_modules/@metro-labs/xmtp/package.json +3 -2
  326. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +10 -20
  327. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +2 -2
  328. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +15 -204
  329. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +4 -43
  330. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +41 -219
  331. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +3 -3
  332. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +3 -3
  333. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +7 -24
  334. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +8 -18
  335. package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +1 -1
  336. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +2 -2
  337. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +30 -16
  338. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +12 -20
  339. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +1 -19
  340. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +1 -1
  341. package/runtime/node_modules/@metro-labs/xmtp/src/names.ts +61 -0
  342. package/runtime/node_modules/@metro-labs/xmtp/src/profile.ts +153 -0
  343. package/runtime/node_modules/@metro-labs/xmtp/src/sender.ts +112 -0
  344. package/runtime/node_modules/@metro-labs/xmtp/src/smart.ts +72 -0
  345. package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +5 -1
  346. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +4 -51
  347. package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +30 -44
  348. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +4 -6
  349. package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +19 -5
  350. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +14 -4
  351. package/runtime/runtime.json +1 -1
  352. package/runtime/server.ts +1 -0
  353. package/runtime/stations.json +37 -0
  354. package/dist/prompt.js +0 -10
  355. package/dist/store.js +0 -70
  356. package/runtime/node_modules/@metro-labs/discord/src/station.ts +0 -19
  357. package/runtime/node_modules/@metro-labs/discord/src/voice-speak.ts +0 -195
  358. package/runtime/node_modules/@metro-labs/discord/src/voice-transcribe.ts +0 -292
  359. package/runtime/node_modules/@metro-labs/discord/src/voice.ts +0 -197
  360. package/runtime/node_modules/@metro-labs/discord/src/wire.ts +0 -5
  361. package/runtime/node_modules/@metro-labs/mcp/package.json +0 -66
  362. package/runtime/node_modules/@metro-labs/mcp/src/channels/dedupe.ts +0 -24
  363. package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +0 -311
  364. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +0 -335
  365. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +0 -129
  366. package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +0 -322
  367. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +0 -111
  368. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair.ts +0 -20
  369. package/runtime/node_modules/@metro-labs/mcp/src/daemon/collection-api.ts +0 -202
  370. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-json.ts +0 -31
  371. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +0 -100
  372. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +0 -57
  373. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +0 -333
  374. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-auth.ts +0 -164
  375. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-oauth.ts +0 -208
  376. package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +0 -79
  377. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +0 -65
  378. package/runtime/node_modules/@metro-labs/mcp/src/daemon/pair-codes.ts +0 -58
  379. package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +0 -214
  380. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +0 -125
  381. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-pair.ts +0 -18
  382. package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-source.ts +0 -105
  383. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +0 -41
  384. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +0 -218
  385. package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +0 -169
  386. package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +0 -91
  387. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +0 -215
  388. package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +0 -37
  389. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-collections.ts +0 -218
  390. package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +0 -329
  391. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +0 -364
  392. package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +0 -300
  393. package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +0 -31
  394. package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +0 -105
  395. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +0 -131
  396. package/runtime/node_modules/@metro-labs/mcp/src/db/token-carry.ts +0 -45
  397. package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +0 -57
  398. package/runtime/node_modules/@metro-labs/mcp/src/db/whatsapp-login.ts +0 -18
  399. package/runtime/node_modules/@metro-labs/mcp/src/mcp/channel-owner.ts +0 -29
  400. package/runtime/node_modules/@metro-labs/mcp/src/mcp/frame-scope.ts +0 -21
  401. package/runtime/node_modules/@metro-labs/mcp/src/mcp/permission-relay.ts +0 -66
  402. package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +0 -73
  403. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-registry.ts +0 -172
  404. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +0 -43
  405. package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +0 -240
  406. package/runtime/node_modules/@metro-labs/mcp/src/server.ts +0 -2
  407. package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +0 -80
  408. package/runtime/node_modules/@metro-labs/mcp/src/stations/messaging-normalize.ts +0 -88
  409. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +0 -3
  410. package/runtime/node_modules/@metro-labs/telegram-user/src/accounts.ts +0 -81
  411. package/runtime/node_modules/@metro-labs/telegram-user/src/actions.ts +0 -254
  412. package/runtime/node_modules/@metro-labs/telegram-user/src/attachments.ts +0 -87
  413. package/runtime/node_modules/@metro-labs/telegram-user/src/format.ts +0 -141
  414. package/runtime/node_modules/@metro-labs/telegram-user/src/inbound.ts +0 -78
  415. package/runtime/node_modules/@metro-labs/telegram-user/src/index.ts +0 -46
  416. package/runtime/node_modules/@metro-labs/telegram-user/src/media-actions.ts +0 -80
  417. package/runtime/node_modules/@metro-labs/telegram-user/src/members.ts +0 -75
  418. package/runtime/node_modules/@metro-labs/telegram-user/src/normalize.ts +0 -22
  419. package/runtime/node_modules/@metro-labs/telegram-user/src/station.ts +0 -24
  420. package/runtime/node_modules/@metro-labs/telegram-user/src/types.ts +0 -7
  421. package/runtime/node_modules/@metro-labs/telegram-user/src/wire.ts +0 -1
  422. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +0 -22
  423. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +0 -5
  424. package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +0 -87
  425. package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +0 -55
  426. package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +0 -349
  427. /package/runtime/node_modules/@metro-labs/{mcp/src/daemon → core/src}/log.ts +0 -0
  428. /package/runtime/node_modules/@metro-labs/{mcp/src/mcp → core/src}/str.ts +0 -0
  429. /package/runtime/node_modules/@metro-labs/{mcp/src/daemon → core/src}/train-error.ts +0 -0
  430. /package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/channels/webhook-note.ts +0 -0
  431. /package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/raw-get-stream.ts +0 -0
  432. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/api-base.ts +0 -0
  433. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/verify.ts +0 -0
  434. /package/runtime/node_modules/@metro-labs/{mcp/src/daemon → http/src}/api-error.ts +0 -0
  435. /package/runtime/node_modules/@metro-labs/{mcp/src/daemon → http/src}/cors.ts +0 -0
  436. /package/runtime/node_modules/@metro-labs/{telegram → telegram-bot}/src/verify.ts +0 -0
@@ -0,0 +1,208 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { isRecord } from '@metro-labs/core/is-record';
3
+ import { errMsg, log } from '@metro-labs/core/log';
4
+ import { refreshTokens, tokensStale, type GeminiTokens } from './gemini-auth.js';
5
+ import { API, clientHeaders, generateBases, machineSessionId, type Bases } from './gemini-client.js';
6
+ import { GeminiStreamTranslator } from './gemini-stream.js';
7
+ import { toGeminiRequest } from './gemini-translate.js';
8
+ import { ToolNames } from './codex-translate.js';
9
+ import { GatewayError, providerStatus, sendError, upstreamMessage, type Watch } from './forward.js';
10
+ import type { Connection } from './model-config.js';
11
+ import { answerWhole, currentOf, errorKind, reach, refreshed, relayTranslated, sessionHeader, type TokenSource, type TokenState } from './subscription.js';
12
+ import { effortToApply, withoutEffort } from './effort.js';
13
+
14
+ export interface GeminiDeps {
15
+ base?: Bases;
16
+ tokenBase?: string;
17
+ fetchImpl?: typeof fetch;
18
+ save: (id: string, tokens: GeminiTokens) => void;
19
+ }
20
+
21
+ export type GeminiState = TokenState<GeminiTokens>;
22
+
23
+ export const sharedGeminiState: GeminiState = new Map();
24
+
25
+ const sourceOf = (deps: GeminiDeps): TokenSource<GeminiTokens> => ({
26
+ label: 'Google',
27
+ stale: (tokens) => tokensStale(tokens),
28
+ refresh: (tokens) => refreshTokens(tokens, deps.tokenBase, deps.fetchImpl),
29
+ save: deps.save,
30
+ });
31
+
32
+ export const currentGeminiTokens = (conn: Connection, deps: GeminiDeps, state: GeminiState): Promise<GeminiTokens> =>
33
+ currentOf(state, conn.id, conn.gemini, sourceOf(deps), 'Gemini is not connected: sign in with Google on the Model page.');
34
+
35
+ interface Call {
36
+ body: Record<string, unknown>;
37
+ model: string;
38
+ promptId: string;
39
+ watch: Watch;
40
+ deps: GeminiDeps;
41
+ names: ToolNames;
42
+ conn: Connection;
43
+ }
44
+
45
+ const headersFor = (token: string, stream: boolean): Record<string, string> => ({
46
+ authorization: `Bearer ${token}`,
47
+ 'content-type': 'application/json',
48
+ ...clientHeaders(),
49
+ 'x-machine-session-id': machineSessionId(),
50
+ ...(stream ? { accept: 'text/event-stream' } : {}),
51
+ });
52
+
53
+ async function sendTo(base: string, call: Call, tokens: GeminiTokens, stream: boolean): Promise<Response> {
54
+ const request = toGeminiRequest(call.body, call.model, tokens.project, call.promptId, call.names);
55
+ const method = stream ? 'streamGenerateContent?alt=sse' : 'generateContent';
56
+ return (call.deps.fetchImpl ?? fetch)(`${base}/${API}:${method}`, {
57
+ method: 'POST',
58
+ headers: headersFor(tokens.accessToken, stream),
59
+ body: JSON.stringify(request),
60
+ signal: call.watch.signal,
61
+ redirect: 'manual',
62
+ });
63
+ }
64
+
65
+ async function send(call: Call, tokens: GeminiTokens, stream: boolean): Promise<Response> {
66
+ const bases = generateBases(call.deps.base);
67
+ let last: Response | null = null;
68
+ for (const base of bases) {
69
+ if (last !== null) await last.body?.cancel();
70
+ try {
71
+ last = await sendTo(base, call, tokens, stream);
72
+ } catch (err) {
73
+ if (base === bases.at(-1) || call.watch.signal.aborted) throw err;
74
+ continue;
75
+ }
76
+ if (last.status < 500 && last.status !== 429) return last;
77
+ }
78
+ if (last === null) throw new GatewayError(502, 'api_error', 'no Code Assist endpoint to call');
79
+ return last;
80
+ }
81
+
82
+ const REFUSAL_LOG_MAX = 2000;
83
+
84
+ const detailText = (detail: Record<string, unknown>): string[] => {
85
+ const out: string[] = [];
86
+ if (typeof detail.reason === 'string') out.push(detail.reason);
87
+ if (isRecord(detail.metadata)) out.push(...Object.entries(detail.metadata).map(([k, v]) => `${k}=${String(v)}`));
88
+ if (typeof detail.retryDelay === 'string') out.push(`retry after ${detail.retryDelay}`);
89
+ if (Array.isArray(detail.violations))
90
+ out.push(...detail.violations.filter(isRecord).map((v) => [v.subject, v.description].filter((x) => typeof x === 'string').join(': ')));
91
+ return out.filter((x) => x !== '');
92
+ };
93
+
94
+ function refusalMessage(text: string, status: number): string {
95
+ const message = upstreamMessage(text, `Gemini answered ${String(status)}`);
96
+ let details: unknown = null;
97
+ try {
98
+ const parsed: unknown = JSON.parse(text);
99
+ details = isRecord(parsed) && isRecord(parsed.error) ? parsed.error.details : null;
100
+ } catch {
101
+ return message;
102
+ }
103
+ const notes = Array.isArray(details) ? details.filter(isRecord).flatMap(detailText) : [];
104
+ return notes.length === 0 ? message : `${message} (${notes.join('; ')})`;
105
+ }
106
+
107
+ const reachWith = (call: Call, state: GeminiState, stream: boolean): Promise<Response> =>
108
+ reach(
109
+ () => currentGeminiTokens(call.conn, call.deps, state),
110
+ (tokens) => refreshed(state, call.conn.id, tokens, sourceOf(call.deps)),
111
+ (tokens) => send(call, tokens, stream),
112
+ );
113
+
114
+ export interface GeminiModel {
115
+ id: string;
116
+ name: string;
117
+ remaining: number | null;
118
+ resetAt: string | null;
119
+ }
120
+
121
+ const modelOf = (id: string, raw: unknown): GeminiModel => {
122
+ const entry = isRecord(raw) ? raw : {};
123
+ const quota = isRecord(entry.quotaInfo) ? entry.quotaInfo : null;
124
+ const fraction = quota !== null && typeof quota.remainingFraction === 'number' ? quota.remainingFraction : null;
125
+ const resetAt = quota !== null && typeof quota.resetTime === 'string' ? quota.resetTime : null;
126
+ return { id, name: typeof entry.displayName === 'string' ? entry.displayName : id, remaining: fraction ?? (resetAt === null ? null : 0), resetAt };
127
+ };
128
+
129
+ export async function listGeminiModels(tokens: GeminiTokens, deps: GeminiDeps): Promise<GeminiModel[]> {
130
+ let last = 'no Code Assist endpoint to call';
131
+ for (const base of generateBases(deps.base)) {
132
+ try {
133
+ const res = await (deps.fetchImpl ?? fetch)(`${base}/${API}:fetchAvailableModels`, {
134
+ method: 'POST',
135
+ headers: headersFor(tokens.accessToken, false),
136
+ body: JSON.stringify({ project: tokens.project }),
137
+ signal: AbortSignal.timeout(30_000),
138
+ });
139
+ const body: unknown = await res.json().catch(() => null);
140
+ if (!res.ok) {
141
+ last = upstreamMessage(JSON.stringify(body), `Google answered ${String(res.status)}`);
142
+ continue;
143
+ }
144
+ const models = isRecord(body) && isRecord(body.models) ? body.models : {};
145
+ return Object.entries(models)
146
+ .filter(([id]) => id.startsWith('gemini'))
147
+ .map(([id, raw]) => modelOf(id, raw));
148
+ } catch (err) {
149
+ last = errMsg(err);
150
+ }
151
+ }
152
+ throw new GatewayError(502, 'api_error', `could not list the Gemini models: ${last}`);
153
+ }
154
+
155
+ const parseData = (raw: string): Record<string, unknown> | null => {
156
+ try {
157
+ const parsed: unknown = JSON.parse(raw);
158
+ return isRecord(parsed) ? parsed : null;
159
+ } catch {
160
+ return null;
161
+ }
162
+ };
163
+
164
+ const NAMES_THINKING = /thinking|thought/i;
165
+
166
+ function refuse(res: ServerResponse, model: string, status: number, text: string): void {
167
+ log.warn({ provider: 'gemini', model, status, body: text.slice(0, REFUSAL_LOG_MAX) }, 'gateway: the provider refused the request');
168
+ sendError(res, providerStatus(status), errorKind(status), refusalMessage(text, status));
169
+ }
170
+
171
+ export async function geminiMessages(
172
+ req: IncomingMessage,
173
+ res: ServerResponse,
174
+ body: Record<string, unknown>,
175
+ model: string,
176
+ conn: Connection,
177
+ deps: GeminiDeps,
178
+ state: GeminiState,
179
+ watch: Watch,
180
+ ): Promise<void> {
181
+ const stream = body.stream === true;
182
+ let call: Call = { body, model, promptId: sessionHeader(req) || 'metro', watch, deps, names: new ToolNames(), conn };
183
+ let upstream = await reachWith(call, state, stream);
184
+ if (upstream.status === 400 && effortToApply(body) !== null) {
185
+ const text = await upstream.text();
186
+ if (!NAMES_THINKING.test(text)) {
187
+ refuse(res, model, 400, text);
188
+ return;
189
+ }
190
+ log.warn({ provider: 'gemini', model }, 'gateway: Gemini refused the thinking level, so metro asked again without it');
191
+ call = { ...call, body: withoutEffort(body) };
192
+ upstream = await reachWith(call, state, stream);
193
+ }
194
+ if (!upstream.ok) {
195
+ refuse(res, model, upstream.status, await upstream.text());
196
+ return;
197
+ }
198
+ const translator = new GeminiStreamTranslator(model, (name) => call.names.restore(name));
199
+ if (stream) {
200
+ await relayTranslated(upstream, res, watch, conn.id, translator, (raw) => {
201
+ const data = parseData(raw.data);
202
+ return data === null ? '' : translator.push(data);
203
+ });
204
+ return;
205
+ }
206
+ const data = parseData(await upstream.text());
207
+ answerWhole(res, (data === null ? '' : translator.push(data)) + translator.close(), conn.id);
208
+ }
@@ -0,0 +1,199 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { ApiError } from '@metro-labs/http/api-error';
3
+ import { apiFailure, apiSession, cors, readJsonBody, sendJson } from '@metro-labs/http/api-http';
4
+ import { isRecord } from '@metro-labs/core/is-record';
5
+ import { log } from '@metro-labs/core/log';
6
+ import { openrouterModels, openrouterZdrModels } from './openrouter.js';
7
+ import { anthropicModels, bedrockModels } from './provider-models.js';
8
+ import { routeOf, syncAvailableModelsQuietly } from '../claude/setup.js';
9
+ import { sessionRunning, stopSession } from '../claude/session.js';
10
+ import { forgetOne } from './usage.js';
11
+ import {
12
+ addConnection,
13
+ parseModelConfig,
14
+ readModelConfig,
15
+ removeConnection,
16
+ setRoute,
17
+ updateConnection,
18
+ writeModelConfig,
19
+ type ModelConfig,
20
+ } from './model-config.js';
21
+ import { asApiError, BODY_MAX, connectionFor, settingsBody, type ModelApiDeps, type Route, type Store } from './model-store.js';
22
+ import { CODEX_ROUTES, codexDeviceRoute, GEMINI_ROUTES, refreshUsage } from './model-signin.js';
23
+
24
+ const PATH = '/api/model';
25
+ const CONNECTIONS = '/api/model/connections';
26
+ const CODEX = '/api/model/codex/';
27
+ const GEMINI = '/api/model/gemini/';
28
+ const OPENROUTER = '/api/model/openrouter/';
29
+ const ANTHROPIC = '/api/model/anthropic/';
30
+ const BEDROCK = '/api/model/bedrock/';
31
+ const BUNDLE = '/api/model/bundle';
32
+ const RESTORE = '/api/model/restore';
33
+ const RESTORE_MAX = 64 * 1024;
34
+ const DEVICE_PREFIX = 'device/';
35
+ const DEVICE_ID_RE = /^[A-Za-z0-9_-]{16,64}$/;
36
+
37
+ export type { ModelApiDeps } from './model-store.js';
38
+
39
+ function restartRunningSession(): boolean {
40
+ if (!sessionRunning()) return false;
41
+ stopSession();
42
+ return true;
43
+ }
44
+
45
+ function kept(store: Store, next: ModelConfig, deps: ModelApiDeps, note: string, fields: Record<string, unknown>): unknown {
46
+ const before = routeOf(store.read());
47
+ store.write(next);
48
+ syncAvailableModelsQuietly(deps.setup ?? {}, next);
49
+ log.info(fields, note);
50
+ if (routeOf(next) !== before && (deps.restartSession ?? restartRunningSession)())
51
+ log.info({ was: before, now: routeOf(next) }, 'model-api: the model changed, so the Claude session restarts on it');
52
+ return settingsBody(next);
53
+ }
54
+
55
+ async function withBody(req: IncomingMessage, run: (body: Record<string, unknown>) => ModelConfig): Promise<ModelConfig> {
56
+ const body = await readJsonBody(req, BODY_MAX);
57
+ if (!isRecord(body)) throw new ApiError('body must be a JSON object', 400);
58
+ try {
59
+ return run(body);
60
+ } catch (err) {
61
+ return asApiError(err);
62
+ }
63
+ }
64
+
65
+ async function chooseRoute(req: IncomingMessage, deps: ModelApiDeps, store: Store): Promise<unknown> {
66
+ const next = await withBody(req, (body) => setRoute(store.read(), typeof body.route === 'string' ? body.route : ''));
67
+ return kept(store, next, deps, 'model-api: route changed', { route: next.route });
68
+ }
69
+
70
+ async function create(req: IncomingMessage, deps: ModelApiDeps, store: Store): Promise<unknown> {
71
+ const next = await withBody(req, (body) => addConnection(store.read(), body));
72
+ return kept(store, next, deps, 'model-api: connection added', { connection: next.connections.at(-1)?.id ?? '' });
73
+ }
74
+
75
+ async function change(req: IncomingMessage, deps: ModelApiDeps, store: Store, id: string): Promise<unknown> {
76
+ const next = await withBody(req, (body) => updateConnection(store.read(), id, body));
77
+ return kept(store, next, deps, 'model-api: connection changed', { connection: id });
78
+ }
79
+
80
+ function drop(deps: ModelApiDeps, store: Store, id: string): unknown {
81
+ let next: ModelConfig;
82
+ try {
83
+ next = removeConnection(store.read(), id);
84
+ } catch (err) {
85
+ asApiError(err);
86
+ }
87
+ forgetOne(id);
88
+ return kept(store, next, deps, 'model-api: connection removed', { connection: id });
89
+ }
90
+
91
+ async function settingsWithUsage(deps: ModelApiDeps, store: Store): Promise<Record<string, unknown>> {
92
+ await refreshUsage(deps, store);
93
+ return settingsBody(store.read());
94
+ }
95
+
96
+ const OPENROUTER_ROUTES: Record<string, Route> = {
97
+ models: { method: 'GET', run: async (_req, deps) => ({ models: await openrouterModels(deps.openrouterBase, deps.fetchImpl).catch(asApiError) }) },
98
+ zdr: { method: 'GET', run: async (_req, deps) => ({ models: await openrouterZdrModels(deps.openrouterBase, deps.fetchImpl).catch(asApiError) }) },
99
+ };
100
+
101
+ const ANTHROPIC_ROUTES: Record<string, Route> = {
102
+ models: {
103
+ method: 'GET',
104
+ run: async (req, deps, store) => ({ models: await anthropicModels(connectionFor(store.read(), req, 'anthropic'), deps.anthropicBase, deps.fetchImpl).catch(asApiError) }),
105
+ },
106
+ };
107
+
108
+ const BEDROCK_ROUTES: Record<string, Route> = {
109
+ models: {
110
+ method: 'GET',
111
+ run: async (req, deps, store) => ({ models: await bedrockModels(connectionFor(store.read(), req, 'bedrock'), deps.bedrockControlBase, deps.fetchImpl).catch(asApiError) }),
112
+ },
113
+ };
114
+
115
+ const named = (table: Record<string, Route>, name: string, method: string | undefined): Route | number => {
116
+ const route = table[name];
117
+ if (route === undefined) return 404;
118
+ return route.method === method ? route : 405;
119
+ };
120
+
121
+ async function restore(req: IncomingMessage, store: Store, deps: ModelApiDeps): Promise<unknown> {
122
+ const body = await readJsonBody(req, RESTORE_MAX);
123
+ if (!isRecord(body)) throw new ApiError('body must be a JSON object', 400);
124
+ return kept(store, parseModelConfig(body), deps, 'model-api: model setup restored from a file', {});
125
+ }
126
+
127
+ function bundleRoute(path: string, method: string | undefined): Route | number {
128
+ if (path === BUNDLE) return method === 'GET' ? { method: 'GET', run: (_req, _deps, store) => Promise.resolve(store.read()) } : 405;
129
+ return method === 'POST' ? { method: 'POST', run: (req, deps, store) => restore(req, store, deps) } : 405;
130
+ }
131
+
132
+ function settingsRoute(method: string | undefined): Route | number {
133
+ if (method === 'GET') return { method: 'GET', run: (_req, deps, store) => settingsWithUsage(deps, store) };
134
+ if (method === 'PUT') return { method: 'POST', run: chooseRoute };
135
+ return 405;
136
+ }
137
+
138
+ function connectionsRoute(path: string, method: string | undefined): Route | number {
139
+ if (path === CONNECTIONS) return method === 'POST' ? { method: 'POST', run: create } : 405;
140
+ const id = path.slice(CONNECTIONS.length + 1);
141
+ if (id === '' || id.includes('/')) return 404;
142
+ if (method === 'PUT') return { method: 'POST', run: (req, deps, store) => change(req, deps, store, id) };
143
+ if (method === 'DELETE') return { method: 'GET', run: (_req, deps, store) => Promise.resolve(drop(deps, store, id)) };
144
+ return 405;
145
+ }
146
+
147
+ function codexRoute(rest: string, method: string | undefined): Route | number {
148
+ if (rest in CODEX_ROUTES) return named(CODEX_ROUTES, rest, method);
149
+ const id = rest.startsWith(DEVICE_PREFIX) ? rest.slice(DEVICE_PREFIX.length) : '';
150
+ if (!DEVICE_ID_RE.test(id)) return 404;
151
+ return codexDeviceRoute(id, method);
152
+ }
153
+
154
+ const mine = (path: string): boolean =>
155
+ path === PATH ||
156
+ path === BUNDLE ||
157
+ path === RESTORE ||
158
+ path === CONNECTIONS ||
159
+ path.startsWith(`${CONNECTIONS}/`) ||
160
+ path.startsWith(CODEX) ||
161
+ path.startsWith(GEMINI) ||
162
+ path.startsWith(OPENROUTER) ||
163
+ path.startsWith(ANTHROPIC) ||
164
+ path.startsWith(BEDROCK);
165
+
166
+ function routeFor(path: string, method: string | undefined): Route | number {
167
+ if (path === PATH) return settingsRoute(method);
168
+ if (path === BUNDLE || path === RESTORE) return bundleRoute(path, method);
169
+ if (path === CONNECTIONS || path.startsWith(`${CONNECTIONS}/`)) return connectionsRoute(path, method);
170
+ if (path.startsWith(OPENROUTER)) return named(OPENROUTER_ROUTES, path.slice(OPENROUTER.length), method);
171
+ if (path.startsWith(ANTHROPIC)) return named(ANTHROPIC_ROUTES, path.slice(ANTHROPIC.length), method);
172
+ if (path.startsWith(BEDROCK)) return named(BEDROCK_ROUTES, path.slice(BEDROCK.length), method);
173
+ if (path.startsWith(GEMINI)) return named(GEMINI_ROUTES, path.slice(GEMINI.length), method);
174
+ return codexRoute(path.slice(CODEX.length), method);
175
+ }
176
+
177
+ export function handleModelRequest(req: IncomingMessage, res: ServerResponse, deps: ModelApiDeps): boolean {
178
+ const path = (req.url ?? '').split('?')[0] ?? '';
179
+ if (!mine(path)) return false;
180
+ if (req.method === 'OPTIONS') {
181
+ res.writeHead(204, cors(req)).end();
182
+ return true;
183
+ }
184
+ const route = routeFor(path, req.method);
185
+ if (typeof route === 'number') {
186
+ sendJson(req, res, route, { error: route === 404 ? 'not found' : 'method not allowed' });
187
+ return true;
188
+ }
189
+ const store: Store = { read: deps.read ?? readModelConfig, write: deps.write ?? writeModelConfig };
190
+ apiSession(req)
191
+ .then(async (session) => {
192
+ if (!session) throw new ApiError('unauthorized', 401);
193
+ sendJson(req, res, 200, await route.run(req, deps, store));
194
+ })
195
+ .catch((err: unknown) => {
196
+ apiFailure(req, res, err, 'model-api');
197
+ });
198
+ return true;
199
+ }
@@ -0,0 +1,278 @@
1
+ import { join } from 'node:path';
2
+ import { newId } from '@metro-labs/core/ids';
3
+ import { readJson, writeSecure } from '@metro-labs/core/secure-fs';
4
+ import { agentsDir } from '../agents/files.js';
5
+ import { isRecord } from '@metro-labs/core/is-record';
6
+ import type { CodexTokens } from './codex-auth.js';
7
+ import { tokensFromDisk as geminiTokensFromDisk, type GeminiTokens } from './gemini-auth.js';
8
+
9
+ export const PROVIDERS = ['anthropic', 'bedrock', 'openrouter', 'codex', 'gemini'] as const;
10
+ export type Provider = (typeof PROVIDERS)[number];
11
+
12
+ export interface Connection {
13
+ id: string;
14
+ provider: Provider;
15
+ label: string;
16
+ model: string;
17
+ apiKey: string;
18
+ region: string;
19
+ zdr: boolean;
20
+ codex: CodexTokens | null;
21
+ gemini: GeminiTokens | null;
22
+ }
23
+
24
+ export interface ModelConfig {
25
+ version: 2;
26
+ route: string;
27
+ connections: Connection[];
28
+ }
29
+
30
+ export interface Route {
31
+ connection: Connection;
32
+ model: string;
33
+ }
34
+
35
+ export class ModelConfigError extends Error {}
36
+
37
+ const MODEL_FILE = 'model.json';
38
+ const MAX_FIELD = 512;
39
+ const MAX_LABEL = 60;
40
+ const MAX_CONNECTIONS = 20;
41
+ const PREFIX_RE = /^(anthropic|bedrock|openrouter|codex|gemini):(.+)$/;
42
+ const SMALL_RE = /haiku/i;
43
+
44
+ const LABELS: Record<Provider, string> = {
45
+ anthropic: 'Anthropic',
46
+ bedrock: 'Amazon Bedrock',
47
+ openrouter: 'OpenRouter',
48
+ codex: 'Codex (ChatGPT)',
49
+ gemini: 'Gemini (Google)',
50
+ };
51
+
52
+ const isProvider = (value: unknown): value is Provider => typeof value === 'string' && (PROVIDERS as readonly string[]).includes(value);
53
+
54
+ const text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '');
55
+ const maybe = (value: unknown): string | null => (typeof value === 'string' && value !== '' ? value : null);
56
+
57
+ export const empty = (): ModelConfig => ({ version: 2, route: '', connections: [] });
58
+
59
+ const newConnection = (provider: Provider, label: string): Connection => ({
60
+ id: newId(),
61
+ provider,
62
+ label,
63
+ model: '',
64
+ apiKey: '',
65
+ region: '',
66
+ zdr: false,
67
+ codex: null,
68
+ gemini: null,
69
+ });
70
+
71
+ function codexFromDisk(raw: unknown): CodexTokens | null {
72
+ if (!isRecord(raw)) return null;
73
+ const accessToken = text(raw.accessToken);
74
+ const refreshToken = text(raw.refreshToken);
75
+ const accountId = text(raw.accountId);
76
+ if (accessToken === '' || refreshToken === '' || accountId === '') return null;
77
+ return {
78
+ accessToken,
79
+ refreshToken,
80
+ idToken: text(raw.idToken),
81
+ accountId,
82
+ email: maybe(raw.email),
83
+ plan: maybe(raw.plan),
84
+ savedAt: text(raw.savedAt) === '' ? new Date(0).toISOString() : text(raw.savedAt),
85
+ };
86
+ }
87
+
88
+ function connectionFromDisk(raw: unknown): Connection | null {
89
+ if (!isRecord(raw) || !isProvider(raw.provider)) return null;
90
+ const id = text(raw.id);
91
+ const provider = raw.provider;
92
+ return {
93
+ id: id === '' ? newId() : id,
94
+ provider,
95
+ label: text(raw.label) === '' ? LABELS[provider] : text(raw.label).slice(0, MAX_LABEL),
96
+ model: text(raw.model),
97
+ apiKey: text(raw.apiKey),
98
+ region: text(raw.region),
99
+ zdr: raw.zdr === true,
100
+ codex: codexFromDisk(raw.codex),
101
+ gemini: geminiTokensFromDisk(raw.gemini),
102
+ };
103
+ }
104
+
105
+ export function parseModelConfig(raw: unknown): ModelConfig {
106
+ if (!isRecord(raw) || !Array.isArray(raw.connections)) return empty();
107
+ const connections = raw.connections.flatMap((c: unknown) => connectionFromDisk(c) ?? []).slice(0, MAX_CONNECTIONS);
108
+ const route = text(raw.route);
109
+ return { version: 2, route: connections.some((c) => c.id === route) ? route : (connections[0]?.id ?? ''), connections };
110
+ }
111
+
112
+ const needsIds = (raw: unknown): boolean =>
113
+ isRecord(raw) && Array.isArray(raw.connections) && raw.connections.some((c: unknown) => isRecord(c) && text(c.id) === '');
114
+
115
+ export function readModelConfig(dir = agentsDir()): ModelConfig {
116
+ const raw = readJson<unknown>(join(dir, MODEL_FILE), null, { warn: 'model-config: model.json is unreadable, so every request goes to Anthropic until it is fixed' });
117
+ const cfg = parseModelConfig(raw);
118
+ if (needsIds(raw)) writeModelConfig(cfg, dir);
119
+ return cfg;
120
+ }
121
+
122
+ export function writeModelConfig(cfg: ModelConfig, dir = agentsDir()): void {
123
+ writeSecure(join(dir, MODEL_FILE), JSON.stringify(cfg, null, 2));
124
+ }
125
+
126
+ export const connectionOf = (cfg: ModelConfig, id: string): Connection | null => cfg.connections.find((c) => c.id === id) ?? null;
127
+
128
+ export const routedConnection = (cfg: ModelConfig): Connection | null => connectionOf(cfg, cfg.route);
129
+
130
+ function requireConnection(cfg: ModelConfig, id: string): Connection {
131
+ const found = connectionOf(cfg, id);
132
+ if (found === null) throw new ModelConfigError('no such connection');
133
+ return found;
134
+ }
135
+
136
+ const PASSTHROUGH = 'Nothing is connected: the request carries the Claude Code login of the session that sent it.';
137
+
138
+ const CHECKS: Record<Provider, [(c: Connection) => boolean, string][]> = {
139
+ anthropic: [],
140
+ bedrock: [
141
+ [(c) => c.apiKey === '', 'Bedrock needs an API key: add it on the Model page.'],
142
+ [(c) => c.region === '', 'Bedrock needs a region: add it on the Model page.'],
143
+ ],
144
+ openrouter: [
145
+ [(c) => c.apiKey === '', 'OpenRouter needs an API key: add it on the Model page.'],
146
+ [(c) => c.model === '', 'OpenRouter needs a model id: choose one on the Model page.'],
147
+ ],
148
+ codex: [
149
+ [(c) => c.codex === null, 'Codex is not connected: sign in with ChatGPT on the Model page.'],
150
+ [(c) => c.model === '', 'Codex needs a model id: choose one on the Model page.'],
151
+ ],
152
+ gemini: [
153
+ [(c) => c.gemini === null, 'Gemini is not connected: sign in with Google on the Model page.'],
154
+ [(c) => c.model === '', 'Gemini needs a model id: choose one on the Model page.'],
155
+ ],
156
+ };
157
+
158
+ export function notReady(cfg: ModelConfig, conn = routedConnection(cfg)): string | null {
159
+ if (conn === null) return cfg.connections.length === 0 ? null : PASSTHROUGH;
160
+ return CHECKS[conn.provider].find(([missing]) => missing(conn))?.[1] ?? null;
161
+ }
162
+
163
+ const isSmallModel = (requested: string): boolean => SMALL_RE.test(requested);
164
+
165
+ const DEFAULTS: Record<Provider, (requested: string, c: Connection) => string> = {
166
+ openrouter: (requested, c) => (requested.includes('/') ? requested : c.model),
167
+ bedrock: (requested, c) => (c.model === '' ? requested : c.model),
168
+ codex: (requested, c) => (requested.startsWith('gpt-') ? requested : c.model),
169
+ gemini: (requested, c) => (requested.startsWith('gemini-') ? requested : c.model),
170
+ anthropic: (requested, c) => (c.model === '' || isSmallModel(requested) ? requested : c.model),
171
+ };
172
+
173
+ function forProvider(cfg: ModelConfig, provider: Provider): Connection | null {
174
+ const routed = routedConnection(cfg);
175
+ if (routed?.provider === provider) return routed;
176
+ return cfg.connections.find((c) => c.provider === provider) ?? null;
177
+ }
178
+
179
+ export function resolveRoute(requested: string, cfg: ModelConfig): Route | null {
180
+ const explicit = PREFIX_RE.exec(requested);
181
+ if (explicit !== null && isProvider(explicit[1])) {
182
+ const conn = forProvider(cfg, explicit[1]);
183
+ return conn === null ? null : { connection: conn, model: (explicit[2] ?? '').trim() };
184
+ }
185
+ const routed = routedConnection(cfg);
186
+ return routed === null ? null : { connection: routed, model: DEFAULTS[routed.provider](requested, routed) };
187
+ }
188
+
189
+ export const routeLabel = (route: Route): string =>
190
+ route.connection.provider === 'anthropic' ? route.model : `${route.connection.provider}:${route.model}`;
191
+
192
+ function publicConnection(c: Connection): Record<string, unknown> {
193
+ return {
194
+ id: c.id,
195
+ provider: c.provider,
196
+ label: c.label,
197
+ model: c.model,
198
+ hasKey: c.apiKey !== '',
199
+ region: c.region,
200
+ zdr: c.zdr,
201
+ signedIn: c.codex !== null || c.gemini !== null,
202
+ account: c.codex?.email ?? c.gemini?.email ?? null,
203
+ plan: c.codex?.plan ?? c.gemini?.tier ?? null,
204
+ };
205
+ }
206
+
207
+ export function publicModelConfig(cfg: ModelConfig): Record<string, unknown> {
208
+ const reason = notReady(cfg);
209
+ return { route: cfg.route, ready: reason === null, reason, connections: cfg.connections.map(publicConnection) };
210
+ }
211
+
212
+ const withConnection = (cfg: ModelConfig, id: string, change: (c: Connection) => Connection): ModelConfig => ({
213
+ ...cfg,
214
+ connections: cfg.connections.map((c) => (c.id === id ? change(c) : c)),
215
+ });
216
+
217
+ export const setCodexAuth = (cfg: ModelConfig, id: string, auth: CodexTokens | null): ModelConfig =>
218
+ withConnection(cfg, id, (c) => ({ ...c, codex: auth }));
219
+
220
+ export const setGeminiAuth = (cfg: ModelConfig, id: string, auth: GeminiTokens | null): ModelConfig =>
221
+ withConnection(cfg, id, (c) => ({ ...c, gemini: auth }));
222
+
223
+ function field(patch: Record<string, unknown>, key: string, current: string, what: string, max = MAX_FIELD): string {
224
+ if (!(key in patch)) return current;
225
+ const value = patch[key];
226
+ if (typeof value !== 'string') throw new ModelConfigError(`${what} must be a string`);
227
+ if (value.length > max) throw new ModelConfigError(`${what} is too long`);
228
+ return value.trim();
229
+ }
230
+
231
+ function flag(patch: Record<string, unknown>, key: string, current: boolean, what: string): boolean {
232
+ if (!(key in patch)) return current;
233
+ const value = patch[key];
234
+ if (typeof value !== 'boolean') throw new ModelConfigError(`${what} must be true or false`);
235
+ return value;
236
+ }
237
+
238
+ function labelFor(cfg: ModelConfig, provider: Provider, given: string): string {
239
+ if (given !== '') return given.slice(0, MAX_LABEL);
240
+ const taken = cfg.connections.filter((c) => c.provider === provider).length;
241
+ return taken === 0 ? LABELS[provider] : `${LABELS[provider]} ${String(taken + 1)}`;
242
+ }
243
+
244
+ export function addConnection(cfg: ModelConfig, patch: unknown): ModelConfig {
245
+ if (!isRecord(patch) || !isProvider(patch.provider)) throw new ModelConfigError(`provider must be one of ${PROVIDERS.join(', ')}`);
246
+ if (cfg.connections.length >= MAX_CONNECTIONS) throw new ModelConfigError(`a box keeps at most ${String(MAX_CONNECTIONS)} connections`);
247
+ const made = newConnection(patch.provider, labelFor(cfg, patch.provider, field(patch, 'label', '', 'label', MAX_LABEL)));
248
+ const filled = applyToConnection(made, patch);
249
+ return { ...cfg, route: cfg.route === '' ? filled.id : cfg.route, connections: [...cfg.connections, filled] };
250
+ }
251
+
252
+ function applyToConnection(c: Connection, patch: Record<string, unknown>): Connection {
253
+ return {
254
+ ...c,
255
+ label: field(patch, 'label', c.label, 'label', MAX_LABEL),
256
+ model: field(patch, 'model', c.model, 'model'),
257
+ apiKey: field(patch, 'apiKey', c.apiKey, 'API key'),
258
+ region: field(patch, 'region', c.region, 'region'),
259
+ zdr: flag(patch, 'zdr', c.zdr, 'zero data retention'),
260
+ };
261
+ }
262
+
263
+ export function updateConnection(cfg: ModelConfig, id: string, patch: unknown): ModelConfig {
264
+ if (!isRecord(patch)) throw new ModelConfigError('body must be a JSON object');
265
+ requireConnection(cfg, id);
266
+ return withConnection(cfg, id, (c) => applyToConnection(c, patch));
267
+ }
268
+
269
+ export function removeConnection(cfg: ModelConfig, id: string): ModelConfig {
270
+ requireConnection(cfg, id);
271
+ const connections = cfg.connections.filter((c) => c.id !== id);
272
+ return { ...cfg, route: cfg.route === id ? (connections[0]?.id ?? '') : cfg.route, connections };
273
+ }
274
+
275
+ export function setRoute(cfg: ModelConfig, id: string): ModelConfig {
276
+ requireConnection(cfg, id);
277
+ return { ...cfg, route: id };
278
+ }