@stage-labs/metro 0.1.0-beta.15 → 0.1.0-beta.151

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 (327) hide show
  1. package/dist/api.js +9 -0
  2. package/dist/claude-settings.js +40 -0
  3. package/dist/claude.js +182 -0
  4. package/dist/cli.js +86 -0
  5. package/dist/control.js +74 -0
  6. package/dist/hold.js +211 -0
  7. package/dist/local.js +77 -0
  8. package/dist/mcp-config.js +25 -0
  9. package/dist/node-name.js +71 -0
  10. package/dist/onboarding.js +43 -0
  11. package/dist/provider-flags.js +9 -0
  12. package/dist/route.js +50 -0
  13. package/dist/runtime-install.js +97 -0
  14. package/dist/runtime.js +74 -0
  15. package/dist/serve.js +155 -0
  16. package/dist/service.js +242 -0
  17. package/dist/tail.js +119 -0
  18. package/dist/update.js +45 -0
  19. package/dist/version.js +97 -0
  20. package/package.json +14 -28
  21. package/runtime/marketplace/.claude-plugin/marketplace.json +12 -0
  22. package/runtime/marketplace/plugin/.claude-plugin/plugin.json +18 -0
  23. package/runtime/marketplace/plugin/.mcp.json +1 -0
  24. package/runtime/marketplace/plugin/bin/guard.mjs +55 -0
  25. package/runtime/marketplace/plugin/bin/metro-plugin.mjs +40 -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 +16 -0
  29. package/runtime/node_modules/@metro-labs/core/package.json +43 -0
  30. package/runtime/node_modules/@metro-labs/core/src/address.ts +6 -0
  31. package/runtime/node_modules/@metro-labs/core/src/endpoints.ts +7 -0
  32. package/runtime/node_modules/@metro-labs/core/src/events.ts +120 -0
  33. package/runtime/node_modules/@metro-labs/core/src/identity.ts +7 -0
  34. package/runtime/node_modules/@metro-labs/core/src/ids.ts +21 -0
  35. package/runtime/node_modules/@metro-labs/core/src/is-record.ts +2 -0
  36. package/runtime/node_modules/@metro-labs/core/src/lines.ts +87 -0
  37. package/runtime/node_modules/@metro-labs/core/src/log.ts +42 -0
  38. package/runtime/node_modules/@metro-labs/core/src/protocol.ts +226 -0
  39. package/runtime/node_modules/@metro-labs/core/src/secure-fs.ts +57 -0
  40. package/runtime/node_modules/@metro-labs/core/src/station-names.ts +13 -0
  41. package/runtime/node_modules/@metro-labs/core/src/stations/account-store.ts +60 -0
  42. package/runtime/node_modules/@metro-labs/core/src/stations/attachments.ts +196 -0
  43. package/runtime/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +88 -0
  44. package/runtime/node_modules/@metro-labs/core/src/stations/station-runtime.ts +64 -0
  45. package/runtime/node_modules/@metro-labs/core/src/stations/types.ts +108 -0
  46. package/runtime/node_modules/@metro-labs/core/src/str.ts +5 -0
  47. package/runtime/node_modules/@metro-labs/core/src/train-error.ts +53 -0
  48. package/runtime/node_modules/@metro-labs/core/src/version.ts +2 -0
  49. package/runtime/node_modules/@metro-labs/daemon/package.json +47 -0
  50. package/runtime/node_modules/@metro-labs/daemon/src/agents/account-attach.ts +19 -0
  51. package/runtime/node_modules/@metro-labs/daemon/src/agents/account-routes.ts +70 -0
  52. package/runtime/node_modules/@metro-labs/daemon/src/agents/accounts-api.ts +365 -0
  53. package/runtime/node_modules/@metro-labs/daemon/src/agents/admin.ts +39 -0
  54. package/runtime/node_modules/@metro-labs/daemon/src/agents/allowlist.ts +37 -0
  55. package/runtime/node_modules/@metro-labs/daemon/src/agents/api.ts +225 -0
  56. package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +122 -0
  57. package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +364 -0
  58. package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +185 -0
  59. package/runtime/node_modules/@metro-labs/daemon/src/agents/keys.ts +40 -0
  60. package/runtime/node_modules/@metro-labs/daemon/src/agents/map.ts +95 -0
  61. package/runtime/node_modules/@metro-labs/daemon/src/agents/scope.ts +64 -0
  62. package/runtime/node_modules/@metro-labs/daemon/src/agents/senders.ts +38 -0
  63. package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +200 -0
  64. package/runtime/node_modules/@metro-labs/daemon/src/boot/connect-hint.ts +39 -0
  65. package/runtime/node_modules/@metro-labs/daemon/src/boot/crash-guard.ts +49 -0
  66. package/runtime/node_modules/@metro-labs/daemon/src/boot/local-owner.ts +30 -0
  67. package/runtime/node_modules/@metro-labs/daemon/src/boot/paths.ts +106 -0
  68. package/runtime/node_modules/@metro-labs/daemon/src/channels/addressed.ts +25 -0
  69. package/runtime/node_modules/@metro-labs/daemon/src/channels/dedupe.ts +24 -0
  70. package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +380 -0
  71. package/runtime/node_modules/@metro-labs/daemon/src/channels/media-note.ts +85 -0
  72. package/runtime/node_modules/@metro-labs/daemon/src/channels/pending.ts +75 -0
  73. package/runtime/node_modules/@metro-labs/daemon/src/channels/relay.ts +129 -0
  74. package/runtime/node_modules/@metro-labs/daemon/src/channels/webhook-note.ts +51 -0
  75. package/runtime/node_modules/@metro-labs/daemon/src/claude/api.ts +354 -0
  76. package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +358 -0
  77. package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +169 -0
  78. package/runtime/node_modules/@metro-labs/daemon/src/claude/onboarding.ts +56 -0
  79. package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-install.ts +140 -0
  80. package/runtime/node_modules/@metro-labs/daemon/src/claude/session-files.ts +90 -0
  81. package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +253 -0
  82. package/runtime/node_modules/@metro-labs/daemon/src/claude/settings.ts +117 -0
  83. package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +263 -0
  84. package/runtime/node_modules/@metro-labs/daemon/src/claude/skills.ts +118 -0
  85. package/runtime/node_modules/@metro-labs/daemon/src/claude/version.ts +109 -0
  86. package/runtime/node_modules/@metro-labs/daemon/src/connectors/api.ts +313 -0
  87. package/runtime/node_modules/@metro-labs/daemon/src/connectors/config.ts +157 -0
  88. package/runtime/node_modules/@metro-labs/daemon/src/connectors/health.ts +18 -0
  89. package/runtime/node_modules/@metro-labs/daemon/src/connectors/model.ts +69 -0
  90. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-client.ts +166 -0
  91. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-discovery.ts +139 -0
  92. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-pending.ts +65 -0
  93. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-routes.ts +171 -0
  94. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth.ts +155 -0
  95. package/runtime/node_modules/@metro-labs/daemon/src/connectors/plugin-sync.ts +122 -0
  96. package/runtime/node_modules/@metro-labs/daemon/src/connectors/reach.ts +27 -0
  97. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-target.ts +39 -0
  98. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +304 -0
  99. package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +299 -0
  100. package/runtime/node_modules/@metro-labs/daemon/src/connectors/tools.ts +108 -0
  101. package/runtime/node_modules/@metro-labs/daemon/src/connectors/url.ts +59 -0
  102. package/runtime/node_modules/@metro-labs/daemon/src/connectors/verify.ts +101 -0
  103. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-grant.ts +99 -0
  104. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-owner.ts +32 -0
  105. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-serve.ts +121 -0
  106. package/runtime/node_modules/@metro-labs/daemon/src/files/upload-api.ts +294 -0
  107. package/runtime/node_modules/@metro-labs/daemon/src/files/upload-store.ts +181 -0
  108. package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +259 -0
  109. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +231 -0
  110. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-device.ts +91 -0
  111. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-stream.ts +288 -0
  112. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-translate.ts +203 -0
  113. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +231 -0
  114. package/runtime/node_modules/@metro-labs/daemon/src/gateway/eventstream.ts +123 -0
  115. package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +187 -0
  116. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +247 -0
  117. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-auth.ts +182 -0
  118. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-client.ts +46 -0
  119. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-setup.ts +150 -0
  120. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-stream.ts +148 -0
  121. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-translate.ts +201 -0
  122. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini.ts +257 -0
  123. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +353 -0
  124. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +232 -0
  125. package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +90 -0
  126. package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +65 -0
  127. package/runtime/node_modules/@metro-labs/daemon/src/gateway/served.ts +17 -0
  128. package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage.ts +242 -0
  129. package/runtime/node_modules/@metro-labs/daemon/src/mcp/accounts.ts +151 -0
  130. package/runtime/node_modules/@metro-labs/daemon/src/mcp/call-tools.ts +216 -0
  131. package/runtime/node_modules/@metro-labs/daemon/src/mcp/channel-owner.ts +29 -0
  132. package/runtime/node_modules/@metro-labs/daemon/src/mcp/ctx.ts +62 -0
  133. package/runtime/node_modules/@metro-labs/daemon/src/mcp/event-store.ts +91 -0
  134. package/runtime/node_modules/@metro-labs/daemon/src/mcp/frame-scope.ts +21 -0
  135. package/runtime/node_modules/@metro-labs/daemon/src/mcp/group-tools.ts +129 -0
  136. package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +186 -0
  137. package/runtime/node_modules/@metro-labs/daemon/src/mcp/member-tools.ts +72 -0
  138. package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-relay.ts +66 -0
  139. package/runtime/node_modules/@metro-labs/daemon/src/mcp/raw-get-stream.ts +207 -0
  140. package/runtime/node_modules/@metro-labs/daemon/src/mcp/request-identity.ts +47 -0
  141. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-registry.ts +172 -0
  142. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-route.ts +43 -0
  143. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +215 -0
  144. package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-dispatch.ts +215 -0
  145. package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-schemas.ts +400 -0
  146. package/runtime/node_modules/@metro-labs/daemon/src/mcp/upload-tool.ts +66 -0
  147. package/runtime/node_modules/@metro-labs/daemon/src/monitor/api.ts +237 -0
  148. package/runtime/node_modules/@metro-labs/daemon/src/net/tunnel.ts +389 -0
  149. package/runtime/node_modules/@metro-labs/daemon/src/routes/bearer.ts +35 -0
  150. package/runtime/node_modules/@metro-labs/daemon/src/routes/body.ts +24 -0
  151. package/runtime/node_modules/@metro-labs/daemon/src/routes/http.ts +370 -0
  152. package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +210 -0
  153. package/runtime/node_modules/@metro-labs/daemon/src/routes/session-apis.ts +58 -0
  154. package/runtime/node_modules/@metro-labs/daemon/src/routes/session.ts +29 -0
  155. package/runtime/node_modules/@metro-labs/daemon/src/routes/threema-callback.ts +103 -0
  156. package/runtime/node_modules/@metro-labs/daemon/src/server/control.ts +65 -0
  157. package/runtime/node_modules/@metro-labs/daemon/src/server/machine.ts +80 -0
  158. package/runtime/node_modules/@metro-labs/daemon/src/server/owner.ts +48 -0
  159. package/runtime/node_modules/@metro-labs/daemon/src/server/update.ts +117 -0
  160. package/runtime/node_modules/@metro-labs/daemon/src/server.ts +2 -0
  161. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-inline.ts +100 -0
  162. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-interactive.ts +161 -0
  163. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-resolve.ts +202 -0
  164. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-session.ts +280 -0
  165. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-xmtp.ts +111 -0
  166. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach.ts +248 -0
  167. package/runtime/node_modules/@metro-labs/daemon/src/stations/materialize.ts +261 -0
  168. package/runtime/node_modules/@metro-labs/daemon/src/stations/registry.ts +37 -0
  169. package/runtime/node_modules/@metro-labs/daemon/src/stations/runtime-deps.ts +82 -0
  170. package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor-io.ts +97 -0
  171. package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor.ts +320 -0
  172. package/runtime/node_modules/@metro-labs/daemon/src/stations/threema-callbacks.ts +32 -0
  173. package/runtime/node_modules/@metro-labs/daemon/src/stations/train-call.ts +22 -0
  174. package/runtime/node_modules/@metro-labs/daemon/src/terminal/api.ts +97 -0
  175. package/runtime/node_modules/@metro-labs/daemon/src/terminal/socket.ts +123 -0
  176. package/runtime/node_modules/@metro-labs/daemon/src/terminal/tickets.ts +40 -0
  177. package/runtime/node_modules/@metro-labs/discord-bot/package.json +28 -0
  178. package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +118 -0
  179. package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +388 -0
  180. package/runtime/node_modules/@metro-labs/discord-bot/src/api-base.ts +1 -0
  181. package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +29 -0
  182. package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +220 -0
  183. package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +50 -0
  184. package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +115 -0
  185. package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +127 -0
  186. package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +215 -0
  187. package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +43 -0
  188. package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +19 -0
  189. package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +82 -0
  190. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-speak.ts +195 -0
  191. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-transcribe.ts +292 -0
  192. package/runtime/node_modules/@metro-labs/discord-bot/src/voice.ts +197 -0
  193. package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +5 -0
  194. package/runtime/node_modules/@metro-labs/http/package.json +29 -0
  195. package/runtime/node_modules/@metro-labs/http/src/api-error.ts +8 -0
  196. package/runtime/node_modules/@metro-labs/http/src/api-http.ts +106 -0
  197. package/runtime/node_modules/@metro-labs/http/src/cors.ts +29 -0
  198. package/runtime/node_modules/@metro-labs/http/src/mode-api.ts +32 -0
  199. package/runtime/node_modules/@metro-labs/http/src/return-to.ts +21 -0
  200. package/runtime/node_modules/@metro-labs/http/src/workos-token.ts +150 -0
  201. package/runtime/node_modules/@metro-labs/telegram/package.json +26 -0
  202. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +80 -0
  203. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +254 -0
  204. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +87 -0
  205. package/runtime/node_modules/@metro-labs/telegram/src/client.ts +77 -0
  206. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +156 -0
  207. package/runtime/node_modules/@metro-labs/telegram/src/group.ts +261 -0
  208. package/runtime/node_modules/@metro-labs/telegram/src/history.ts +52 -0
  209. package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +78 -0
  210. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +46 -0
  211. package/runtime/node_modules/@metro-labs/telegram/src/login.ts +166 -0
  212. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +80 -0
  213. package/runtime/node_modules/@metro-labs/telegram/src/members.ts +75 -0
  214. package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +22 -0
  215. package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +167 -0
  216. package/runtime/node_modules/@metro-labs/telegram/src/station.ts +24 -0
  217. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +7 -0
  218. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -0
  219. package/runtime/node_modules/@metro-labs/telegram-bot/package.json +25 -0
  220. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +120 -0
  221. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +295 -0
  222. package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
  223. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +201 -0
  224. package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +109 -0
  225. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
  226. package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
  227. package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +17 -0
  228. package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +58 -0
  229. package/runtime/node_modules/@metro-labs/telegram-bot/src/verify.ts +38 -0
  230. package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +3 -0
  231. package/runtime/node_modules/@metro-labs/threema/package.json +26 -0
  232. package/runtime/node_modules/@metro-labs/threema/src/accounts.ts +100 -0
  233. package/runtime/node_modules/@metro-labs/threema/src/actions.ts +223 -0
  234. package/runtime/node_modules/@metro-labs/threema/src/api.ts +130 -0
  235. package/runtime/node_modules/@metro-labs/threema/src/crypto.ts +132 -0
  236. package/runtime/node_modules/@metro-labs/threema/src/format.ts +123 -0
  237. package/runtime/node_modules/@metro-labs/threema/src/ids.ts +21 -0
  238. package/runtime/node_modules/@metro-labs/threema/src/index.ts +29 -0
  239. package/runtime/node_modules/@metro-labs/threema/src/station.ts +10 -0
  240. package/runtime/node_modules/@metro-labs/threema/src/verify.ts +117 -0
  241. package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
  242. package/runtime/node_modules/@metro-labs/webhook/src/station.ts +37 -0
  243. package/runtime/node_modules/@metro-labs/whatsapp/package.json +27 -0
  244. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +66 -0
  245. package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
  246. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +190 -0
  247. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
  248. package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
  249. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +347 -0
  250. package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
  251. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +138 -0
  252. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
  253. package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
  254. package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
  255. package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
  256. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
  257. package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
  258. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
  259. package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
  260. package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +185 -0
  261. package/runtime/node_modules/@metro-labs/whatsapp/src/resolve.ts +52 -0
  262. package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +18 -0
  263. package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
  264. package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
  265. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
  266. package/runtime/node_modules/@metro-labs/xmtp/package.json +33 -0
  267. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +112 -0
  268. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
  269. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
  270. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
  271. package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
  272. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +375 -0
  273. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
  274. package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
  275. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
  276. package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
  277. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +335 -0
  278. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +80 -0
  279. package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
  280. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
  281. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +33 -0
  282. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
  283. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
  284. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
  285. package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
  286. package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +13 -0
  287. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
  288. package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
  289. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
  290. package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +84 -0
  291. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +53 -0
  292. package/runtime/runtime.json +3 -0
  293. package/runtime/server.ts +1 -0
  294. package/runtime/stations.json +37 -0
  295. package/runtime/trains/.keep +0 -0
  296. package/LICENSE +0 -21
  297. package/README.md +0 -139
  298. package/dist/broker/claims.js +0 -144
  299. package/dist/broker/history-stream.js +0 -149
  300. package/dist/cli/config.js +0 -179
  301. package/dist/cli/index.js +0 -175
  302. package/dist/cli/messenger-api.js +0 -214
  303. package/dist/cli/messenger-transcribe.js +0 -43
  304. package/dist/cli/messenger-uploads.js +0 -116
  305. package/dist/cli/monitor-api.js +0 -205
  306. package/dist/cli/tail.js +0 -95
  307. package/dist/cli/util.js +0 -72
  308. package/dist/cli/webhook.js +0 -181
  309. package/dist/codex-rc/client.js +0 -225
  310. package/dist/codex-rc/protocol.js +0 -38
  311. package/dist/dispatcher/server.js +0 -122
  312. package/dist/dispatcher.js +0 -97
  313. package/dist/history.js +0 -143
  314. package/dist/ipc.js +0 -90
  315. package/dist/lines.js +0 -54
  316. package/dist/local-identity.js +0 -80
  317. package/dist/log.js +0 -15
  318. package/dist/paths.js +0 -114
  319. package/dist/trains/protocol.js +0 -99
  320. package/dist/trains/supervisor.js +0 -210
  321. package/dist/tunnel.js +0 -102
  322. package/docs/broker.md +0 -166
  323. package/docs/monitor.md +0 -241
  324. package/docs/uri-scheme.md +0 -127
  325. package/examples/README.md +0 -32
  326. package/examples/telegram.ts +0 -121
  327. package/skills/metro/SKILL.md +0 -110
@@ -0,0 +1,353 @@
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 { errMsg, log } from '@metro-labs/core/log';
6
+ import { beginLogin, CodexAuthError, finishLogin, readCodexCliAuth } from './codex-auth.js';
7
+ import { beginDeviceLogin, pollDeviceLogin } from './codex-device.js';
8
+ import { codexModels, currentTokens, freshCodexState } from './codex.js';
9
+ import { beginLogin as beginGeminiLogin, exchangeCode as exchangeGeminiCode, GeminiAuthError, userEmail } from './gemini-auth.js';
10
+ import { onboard, parseGeminiProject } from './gemini-setup.js';
11
+ import { currentGeminiTokens, freshGeminiState, listGeminiModels, type GeminiDeps } from './gemini.js';
12
+ import { openrouterCredits, openrouterModels, openrouterZdrModels } from './openrouter.js';
13
+ import { anthropicModels, bedrockModels } from './provider-models.js';
14
+ import { syncAvailableModelsQuietly, type SetupDeps } from '../claude/setup.js';
15
+ import { lastServed } from './served.js';
16
+ import { geminiUsage, noteUsage, openrouterUsage, usageOf, usageSeen } from './usage.js';
17
+ import type { CodexTokens } from './codex-auth.js';
18
+ import { GatewayError } from './forward.js';
19
+ import {
20
+ applyModelUpdate,
21
+ ModelConfigError,
22
+ parseModelConfig,
23
+ publicModelConfig,
24
+ readModelConfig,
25
+ setCodexAuth,
26
+ setGeminiAuth,
27
+ writeModelConfig,
28
+ type ModelConfig,
29
+ } from './model-config.js';
30
+
31
+ const PATH = '/api/model';
32
+ const CODEX = '/api/model/codex/';
33
+ const GEMINI = '/api/model/gemini/';
34
+ const OPENROUTER = '/api/model/openrouter/';
35
+ const ANTHROPIC = '/api/model/anthropic/';
36
+ const BEDROCK = '/api/model/bedrock/';
37
+ const BUNDLE = '/api/model/bundle';
38
+ const RESTORE = '/api/model/restore';
39
+ const RESTORE_MAX = 64 * 1024;
40
+ const BODY_MAX = 16 * 1024;
41
+ const DEVICE_PREFIX = 'device/';
42
+ const DEVICE_ID_RE = /^[A-Za-z0-9_-]{16,64}$/;
43
+
44
+ export interface ModelApiDeps {
45
+ authorize: (subject: string) => void;
46
+ read?: () => ModelConfig;
47
+ write?: (cfg: ModelConfig) => void;
48
+ issuer?: string;
49
+ fetchImpl?: typeof fetch;
50
+ codexHome?: string;
51
+ codexBase?: string;
52
+ geminiAuthBase?: string;
53
+ geminiTokenBase?: string;
54
+ geminiUserBase?: string;
55
+ geminiBase?: string;
56
+ openrouterBase?: string;
57
+ anthropicBase?: string;
58
+ bedrockControlBase?: string;
59
+ setup?: SetupDeps;
60
+ }
61
+
62
+ interface Store {
63
+ read: () => ModelConfig;
64
+ write: (cfg: ModelConfig) => void;
65
+ }
66
+
67
+ type Handler = (req: IncomingMessage, deps: ModelApiDeps, store: Store) => Promise<unknown>;
68
+
69
+ interface Route {
70
+ method: 'GET' | 'POST';
71
+ run: Handler;
72
+ }
73
+
74
+ const settingsBody = (cfg: ModelConfig): Record<string, unknown> => ({
75
+ ...publicModelConfig(cfg),
76
+ lastServed: lastServed(),
77
+ usage: usageSeen(),
78
+ });
79
+
80
+ const CREDITS_TTL_MS = 5 * 60_000;
81
+
82
+ async function refreshCredits(cfg: ModelConfig, deps: ModelApiDeps, now = Date.now()): Promise<void> {
83
+ const key = cfg.openrouter.apiKey;
84
+ if (key === '') return;
85
+ const seen = usageOf('openrouter');
86
+ if (seen !== undefined && now - Date.parse(seen.at) < CREDITS_TTL_MS) return;
87
+ try {
88
+ const credits = await openrouterCredits(key, deps.openrouterBase, deps.fetchImpl);
89
+ noteUsage('openrouter', openrouterUsage(credits.total, credits.spent, new Date(now)));
90
+ } catch (err) {
91
+ log.warn({ err: errMsg(err) }, 'model-api: could not read the OpenRouter credits');
92
+ }
93
+ }
94
+
95
+ const geminiApiState = freshGeminiState();
96
+
97
+ const geminiDeps = (deps: ModelApiDeps, store: Store): GeminiDeps => ({
98
+ base: deps.geminiBase,
99
+ tokenBase: deps.geminiTokenBase,
100
+ fetchImpl: deps.fetchImpl,
101
+ save: (t) => {
102
+ store.write(setGeminiAuth(store.read(), t));
103
+ },
104
+ });
105
+
106
+ async function geminiModels(deps: ModelApiDeps, store: Store): Promise<Awaited<ReturnType<typeof listGeminiModels>>> {
107
+ const cfg = store.read();
108
+ if (cfg.gemini.auth === null) throw new ApiError('Gemini is not connected: sign in with Google first', 400);
109
+ const tokens = await currentGeminiTokens(cfg, geminiDeps(deps, store), geminiApiState);
110
+ return listGeminiModels(tokens, geminiDeps(deps, store));
111
+ }
112
+
113
+ async function refreshGeminiQuota(deps: ModelApiDeps, store: Store, now = Date.now()): Promise<void> {
114
+ if (store.read().gemini.auth === null) return;
115
+ const seen = usageOf('gemini');
116
+ if (seen !== undefined && now - Date.parse(seen.at) < CREDITS_TTL_MS) return;
117
+ try {
118
+ const usage = geminiUsage(await geminiModels(deps, store), new Date(now));
119
+ if (usage !== null) noteUsage('gemini', usage);
120
+ } catch (err) {
121
+ log.warn({ err: errMsg(err) }, 'model-api: could not read the Gemini quota');
122
+ }
123
+ }
124
+
125
+ async function settingsWithUsage(cfg: ModelConfig, deps: ModelApiDeps, store: Store): Promise<Record<string, unknown>> {
126
+ await refreshCredits(cfg, deps);
127
+ await refreshGeminiQuota(deps, store);
128
+ return settingsBody(cfg);
129
+ }
130
+
131
+ function asApiError(err: unknown): never {
132
+ if (err instanceof ModelConfigError || err instanceof CodexAuthError || err instanceof GeminiAuthError) throw new ApiError(err.message, 400);
133
+ if (err instanceof GatewayError) throw new ApiError(err.message, err.status >= 400 && err.status < 500 ? 400 : 502);
134
+ throw err;
135
+ }
136
+
137
+ async function update(req: IncomingMessage, store: Store, deps: ModelApiDeps): Promise<unknown> {
138
+ const patch = await readJsonBody(req, BODY_MAX);
139
+ let next: ModelConfig;
140
+ try {
141
+ next = applyModelUpdate(store.read(), patch);
142
+ } catch (err) {
143
+ asApiError(err);
144
+ }
145
+ store.write(next);
146
+ syncAvailableModelsQuietly(deps.setup ?? {}, next);
147
+ log.info({ provider: next.provider }, 'model-api: route updated');
148
+ return settingsBody(next);
149
+ }
150
+
151
+ function saveCodex(store: Store, cfg: ModelConfig, note: string): unknown {
152
+ store.write(cfg);
153
+ log.info({ signedIn: cfg.codex.auth !== null, plan: cfg.codex.auth?.plan ?? null }, note);
154
+ return settingsBody(cfg);
155
+ }
156
+
157
+ const modelApiState = freshCodexState();
158
+
159
+ async function pollDevice(id: string, deps: ModelApiDeps, store: Store): Promise<unknown> {
160
+ const result = await pollDeviceLogin(id, deps.fetchImpl).catch(asApiError);
161
+ if (result.status !== 'done') return result;
162
+ return { status: 'done', settings: saveCodex(store, setCodexAuth(store.read(), result.tokens), 'model-api: Codex connected by device code') };
163
+ }
164
+
165
+ const CODEX_ROUTES: Record<string, Route> = {
166
+ device: {
167
+ method: 'POST',
168
+ run: async (_req, deps) => {
169
+ const login = await beginDeviceLogin(deps.issuer, deps.fetchImpl).catch(asApiError);
170
+ return { id: login.id, user_code: login.userCode, verify_url: login.verifyUrl, interval: login.interval };
171
+ },
172
+ },
173
+ login: {
174
+ method: 'POST',
175
+ run: (_req, deps) => Promise.resolve({ url: beginLogin(deps.issuer).url }),
176
+ },
177
+ callback: {
178
+ method: 'POST',
179
+ run: async (req, deps, store) => {
180
+ const body = await readJsonBody(req, BODY_MAX);
181
+ const raw = isRecord(body) && typeof body.url === 'string' ? body.url : '';
182
+ const tokens = await finishLogin(raw, deps.issuer, deps.fetchImpl).catch(asApiError);
183
+ return saveCodex(store, setCodexAuth(store.read(), tokens), 'model-api: Codex connected');
184
+ },
185
+ },
186
+ logout: {
187
+ method: 'POST',
188
+ run: (_req, _deps, store) => Promise.resolve(saveCodex(store, setCodexAuth(store.read(), null), 'model-api: Codex disconnected')),
189
+ },
190
+ import: {
191
+ method: 'POST',
192
+ run: (_req, deps, store) => {
193
+ let tokens;
194
+ try {
195
+ tokens = readCodexCliAuth(deps.codexHome);
196
+ } catch (err) {
197
+ asApiError(err);
198
+ }
199
+ return Promise.resolve(saveCodex(store, setCodexAuth(store.read(), tokens), 'model-api: Codex CLI login imported'));
200
+ },
201
+ },
202
+ models: {
203
+ method: 'GET',
204
+ run: async (_req, deps, store) => {
205
+ const cfg = store.read();
206
+ if (cfg.codex.auth === null) throw new ApiError('Codex is not connected: sign in with ChatGPT first', 400);
207
+ const codexDeps = { issuer: deps.issuer, fetchImpl: deps.fetchImpl, base: deps.codexBase, save: (t: CodexTokens) => { store.write(setCodexAuth(store.read(), t)); } };
208
+ const auth = await currentTokens(cfg, codexDeps, modelApiState).catch(asApiError);
209
+ const models = await codexModels(auth, codexDeps).catch(asApiError);
210
+ return { models };
211
+ },
212
+ },
213
+ };
214
+
215
+ function projectOf(body: unknown): string | null {
216
+ try {
217
+ return parseGeminiProject(isRecord(body) ? body.project : null);
218
+ } catch (err) {
219
+ return asApiError(err);
220
+ }
221
+ }
222
+
223
+ async function connectGemini(req: IncomingMessage, deps: ModelApiDeps, store: Store): Promise<unknown> {
224
+ const body = await readJsonBody(req, BODY_MAX);
225
+ const code = isRecord(body) && typeof body.code === 'string' ? body.code : '';
226
+ const state = isRecord(body) && typeof body.state === 'string' ? body.state : '';
227
+ const project = projectOf(body);
228
+ const tokens = await exchangeGeminiCode(code, state, deps.geminiTokenBase, deps.fetchImpl).catch(asApiError);
229
+ const email = await userEmail(tokens, deps.geminiUserBase, deps.fetchImpl);
230
+ const onboarded = await onboard(tokens, project, deps.geminiBase, deps.fetchImpl).catch(asApiError);
231
+ const cfg = setGeminiAuth(store.read(), { ...tokens, email, project: onboarded.project, tier: onboarded.tier });
232
+ store.write(cfg);
233
+ log.info({ tier: onboarded.tier }, 'model-api: Gemini connected');
234
+ return settingsBody(cfg);
235
+ }
236
+
237
+ const GEMINI_ROUTES: Record<string, Route> = {
238
+ login: {
239
+ method: 'POST',
240
+ run: (_req, deps) => Promise.resolve(beginGeminiLogin(deps.geminiAuthBase)),
241
+ },
242
+ code: { method: 'POST', run: connectGemini },
243
+ logout: {
244
+ method: 'POST',
245
+ run: (_req, _deps, store) => {
246
+ const cfg = setGeminiAuth(store.read(), null);
247
+ store.write(cfg);
248
+ log.info('model-api: Gemini disconnected');
249
+ return Promise.resolve(settingsBody(cfg));
250
+ },
251
+ },
252
+ models: {
253
+ method: 'GET',
254
+ run: async (_req, deps, store) => ({ models: (await geminiModels(deps, store).catch(asApiError)).map((m) => m.id) }),
255
+ },
256
+ };
257
+
258
+ const OPENROUTER_ROUTES: Record<string, Route> = {
259
+ models: {
260
+ method: 'GET',
261
+ run: async (_req, deps) => ({ models: await openrouterModels(deps.openrouterBase, deps.fetchImpl).catch(asApiError) }),
262
+ },
263
+ zdr: {
264
+ method: 'GET',
265
+ run: async (_req, deps) => ({ models: await openrouterZdrModels(deps.openrouterBase, deps.fetchImpl).catch(asApiError) }),
266
+ },
267
+ };
268
+
269
+ const ANTHROPIC_ROUTES: Record<string, Route> = {
270
+ models: {
271
+ method: 'GET',
272
+ run: async (_req, deps, store) => ({ models: await anthropicModels(store.read().anthropic, deps.anthropicBase, deps.fetchImpl).catch(asApiError) }),
273
+ },
274
+ };
275
+
276
+ const BEDROCK_ROUTES: Record<string, Route> = {
277
+ models: {
278
+ method: 'GET',
279
+ run: async (_req, deps, store) => ({ models: await bedrockModels(store.read().bedrock, deps.bedrockControlBase, deps.fetchImpl).catch(asApiError) }),
280
+ },
281
+ };
282
+
283
+ const named = (table: Record<string, Route>, name: string, method: string | undefined): Route | number => {
284
+ const route = table[name];
285
+ if (route === undefined) return 404;
286
+ return route.method === method ? route : 405;
287
+ };
288
+
289
+ async function restore(req: IncomingMessage, store: Store, deps: ModelApiDeps): Promise<unknown> {
290
+ const body = await readJsonBody(req, RESTORE_MAX);
291
+ if (!isRecord(body)) throw new ApiError('body must be a JSON object', 400);
292
+ const next = parseModelConfig(body);
293
+ store.write(next);
294
+ syncAvailableModelsQuietly(deps.setup ?? {}, next);
295
+ log.info({ provider: next.provider }, 'model-api: model setup restored from a file');
296
+ return settingsBody(next);
297
+ }
298
+
299
+ function bundleRoute(path: string, method: string | undefined): Route | number {
300
+ if (path === BUNDLE) return method === 'GET' ? { method: 'GET', run: (_req, _deps, store) => Promise.resolve(store.read()) } : 405;
301
+ return method === 'POST' ? { method: 'POST', run: (req, deps, store) => restore(req, store, deps) } : 405;
302
+ }
303
+
304
+ function settingsRoute(method: string | undefined): Route | number {
305
+ if (method === 'GET') return { method: 'GET', run: (_req, deps, store) => settingsWithUsage(store.read(), deps, store) };
306
+ if (method === 'PUT') return { method: 'POST', run: (req, deps, store) => update(req, store, deps) };
307
+ return 405;
308
+ }
309
+
310
+ function codexRoute(rest: string, method: string | undefined): Route | number {
311
+ if (rest in CODEX_ROUTES) return named(CODEX_ROUTES, rest, method);
312
+ const id = rest.startsWith(DEVICE_PREFIX) ? rest.slice(DEVICE_PREFIX.length) : '';
313
+ if (!DEVICE_ID_RE.test(id)) return 404;
314
+ return method === 'GET' ? { method: 'GET', run: (_req, deps, store) => pollDevice(id, deps, store) } : 405;
315
+ }
316
+
317
+ const mine = (path: string): boolean =>
318
+ path === PATH || path === BUNDLE || path === RESTORE || path.startsWith(CODEX) || path.startsWith(GEMINI) || path.startsWith(OPENROUTER) || path.startsWith(ANTHROPIC) || path.startsWith(BEDROCK);
319
+
320
+ function routeFor(path: string, method: string | undefined): Route | number {
321
+ if (path === PATH) return settingsRoute(method);
322
+ if (path === BUNDLE || path === RESTORE) return bundleRoute(path, method);
323
+ if (path.startsWith(OPENROUTER)) return named(OPENROUTER_ROUTES, path.slice(OPENROUTER.length), method);
324
+ if (path.startsWith(ANTHROPIC)) return named(ANTHROPIC_ROUTES, path.slice(ANTHROPIC.length), method);
325
+ if (path.startsWith(BEDROCK)) return named(BEDROCK_ROUTES, path.slice(BEDROCK.length), method);
326
+ if (path.startsWith(GEMINI)) return named(GEMINI_ROUTES, path.slice(GEMINI.length), method);
327
+ return codexRoute(path.slice(CODEX.length), method);
328
+ }
329
+
330
+ export function handleModelRequest(req: IncomingMessage, res: ServerResponse, deps: ModelApiDeps): boolean {
331
+ const path = (req.url ?? '').split('?')[0] ?? '';
332
+ if (!mine(path)) return false;
333
+ if (req.method === 'OPTIONS') {
334
+ res.writeHead(204, cors(req)).end();
335
+ return true;
336
+ }
337
+ const route = routeFor(path, req.method);
338
+ if (typeof route === 'number') {
339
+ sendJson(req, res, route, { error: route === 404 ? 'not found' : 'method not allowed' });
340
+ return true;
341
+ }
342
+ const store: Store = { read: deps.read ?? readModelConfig, write: deps.write ?? writeModelConfig };
343
+ apiSession(req)
344
+ .then(async (session) => {
345
+ if (!session) throw new ApiError('unauthorized', 401);
346
+ deps.authorize(session.subject);
347
+ sendJson(req, res, 200, await route.run(req, deps, store));
348
+ })
349
+ .catch((err: unknown) => {
350
+ apiFailure(req, res, err, 'model-api');
351
+ });
352
+ return true;
353
+ }
@@ -0,0 +1,232 @@
1
+ import { join } from 'node:path';
2
+ import { readJson, writeSecure } from '@metro-labs/core/secure-fs';
3
+ import { agentsDir } from '../agents/files.js';
4
+ import { isRecord } from '@metro-labs/core/is-record';
5
+ import type { CodexTokens } from './codex-auth.js';
6
+ import { tokensFromDisk as geminiTokensFromDisk, type GeminiTokens } from './gemini-auth.js';
7
+
8
+ export const PROVIDERS = ['anthropic', 'bedrock', 'openrouter', 'codex', 'gemini'] as const;
9
+ export type Provider = (typeof PROVIDERS)[number];
10
+
11
+ export interface AnthropicSettings {
12
+ apiKey: string;
13
+ model: string;
14
+ }
15
+
16
+ export interface BedrockSettings {
17
+ region: string;
18
+ apiKey: string;
19
+ model: string;
20
+ }
21
+
22
+ export interface OpenRouterSettings {
23
+ apiKey: string;
24
+ model: string;
25
+ zdr: boolean;
26
+ }
27
+
28
+ export interface CodexSettings {
29
+ model: string;
30
+ auth: CodexTokens | null;
31
+ }
32
+
33
+ export interface GeminiSettings {
34
+ model: string;
35
+ auth: GeminiTokens | null;
36
+ }
37
+
38
+ export interface ModelConfig {
39
+ version: 1;
40
+ provider: Provider;
41
+ anthropic: AnthropicSettings;
42
+ bedrock: BedrockSettings;
43
+ openrouter: OpenRouterSettings;
44
+ codex: CodexSettings;
45
+ gemini: GeminiSettings;
46
+ }
47
+
48
+ export interface Route {
49
+ provider: Provider;
50
+ model: string;
51
+ }
52
+
53
+ export class ModelConfigError extends Error {}
54
+
55
+ export const MODEL_FILE = 'model.json';
56
+ const MAX_FIELD = 512;
57
+ const PREFIX_RE = /^(anthropic|bedrock|openrouter|codex|gemini):(.+)$/;
58
+ const SMALL_RE = /haiku/i;
59
+
60
+ const empty = (): ModelConfig => ({
61
+ version: 1,
62
+ provider: 'anthropic',
63
+ anthropic: { apiKey: '', model: '' },
64
+ bedrock: { region: '', apiKey: '', model: '' },
65
+ openrouter: { apiKey: '', model: '', zdr: false },
66
+ codex: { model: '', auth: null },
67
+ gemini: { model: '', auth: null },
68
+ });
69
+
70
+ const isProvider = (value: unknown): value is Provider =>
71
+ typeof value === 'string' && (PROVIDERS as readonly string[]).includes(value);
72
+
73
+ const text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '');
74
+ const maybe = (value: unknown): string | null => (typeof value === 'string' && value !== '' ? value : null);
75
+
76
+ function tokensFromDisk(raw: unknown): CodexTokens | null {
77
+ if (!isRecord(raw)) return null;
78
+ const accessToken = text(raw.accessToken);
79
+ const refreshToken = text(raw.refreshToken);
80
+ const accountId = text(raw.accountId);
81
+ if (accessToken === '' || refreshToken === '' || accountId === '') return null;
82
+ return {
83
+ accessToken,
84
+ refreshToken,
85
+ idToken: text(raw.idToken),
86
+ accountId,
87
+ email: maybe(raw.email),
88
+ plan: maybe(raw.plan),
89
+ savedAt: text(raw.savedAt) === '' ? new Date(0).toISOString() : text(raw.savedAt),
90
+ };
91
+ }
92
+
93
+ export function parseModelConfig(raw: unknown): ModelConfig {
94
+ const base = empty();
95
+ if (!isRecord(raw)) return base;
96
+ const anthropic = isRecord(raw.anthropic) ? raw.anthropic : {};
97
+ const bedrock = isRecord(raw.bedrock) ? raw.bedrock : {};
98
+ const openrouter = isRecord(raw.openrouter) ? raw.openrouter : {};
99
+ const codex = isRecord(raw.codex) ? raw.codex : {};
100
+ const gemini = isRecord(raw.gemini) ? raw.gemini : {};
101
+ return {
102
+ version: 1,
103
+ provider: isProvider(raw.provider) ? raw.provider : 'anthropic',
104
+ anthropic: { apiKey: text(anthropic.apiKey), model: text(anthropic.model) },
105
+ bedrock: { region: text(bedrock.region), apiKey: text(bedrock.apiKey), model: text(bedrock.model) },
106
+ openrouter: { apiKey: text(openrouter.apiKey), model: text(openrouter.model), zdr: openrouter.zdr === true },
107
+ codex: { model: text(codex.model), auth: tokensFromDisk(codex.auth) },
108
+ gemini: { model: text(gemini.model), auth: geminiTokensFromDisk(gemini.auth) },
109
+ };
110
+ }
111
+
112
+ export function readModelConfig(dir = agentsDir()): ModelConfig {
113
+ return parseModelConfig(readJson<unknown>(join(dir, MODEL_FILE), null, { warn: 'model-config: model.json is unreadable, so every request goes to Anthropic until it is fixed' }));
114
+ }
115
+
116
+ export function writeModelConfig(cfg: ModelConfig, dir = agentsDir()): void {
117
+ writeSecure(join(dir, MODEL_FILE), JSON.stringify(cfg, null, 2));
118
+ }
119
+
120
+ function flag(patch: Record<string, unknown>, key: string, current: boolean, what: string): boolean {
121
+ if (!(key in patch)) return current;
122
+ const value = patch[key];
123
+ if (typeof value !== 'boolean') throw new ModelConfigError(`${what} must be true or false`);
124
+ return value;
125
+ }
126
+
127
+ function field(patch: Record<string, unknown>, key: string, current: string, what: string): string {
128
+ if (!(key in patch)) return current;
129
+ const value = patch[key];
130
+ if (typeof value !== 'string') throw new ModelConfigError(`${what} must be a string`);
131
+ if (value.length > MAX_FIELD) throw new ModelConfigError(`${what} is too long`);
132
+ return value.trim();
133
+ }
134
+
135
+ export function applyModelUpdate(cfg: ModelConfig, patch: unknown): ModelConfig {
136
+ if (!isRecord(patch)) throw new ModelConfigError('body must be a JSON object');
137
+ const provider = 'provider' in patch ? patch.provider : cfg.provider;
138
+ if (!isProvider(provider)) throw new ModelConfigError(`provider must be one of ${PROVIDERS.join(', ')}`);
139
+ const anthropic = isRecord(patch.anthropic) ? patch.anthropic : {};
140
+ const bedrock = isRecord(patch.bedrock) ? patch.bedrock : {};
141
+ const openrouter = isRecord(patch.openrouter) ? patch.openrouter : {};
142
+ const codex = isRecord(patch.codex) ? patch.codex : {};
143
+ const gemini = isRecord(patch.gemini) ? patch.gemini : {};
144
+ return {
145
+ version: 1,
146
+ provider,
147
+ anthropic: {
148
+ apiKey: field(anthropic, 'apiKey', cfg.anthropic.apiKey, 'Anthropic API key'),
149
+ model: field(anthropic, 'model', cfg.anthropic.model, 'Anthropic model'),
150
+ },
151
+ bedrock: {
152
+ region: field(bedrock, 'region', cfg.bedrock.region, 'Bedrock region'),
153
+ apiKey: field(bedrock, 'apiKey', cfg.bedrock.apiKey, 'Bedrock API key'),
154
+ model: field(bedrock, 'model', cfg.bedrock.model, 'Bedrock model'),
155
+ },
156
+ openrouter: {
157
+ apiKey: field(openrouter, 'apiKey', cfg.openrouter.apiKey, 'OpenRouter API key'),
158
+ model: field(openrouter, 'model', cfg.openrouter.model, 'OpenRouter model'),
159
+ zdr: flag(openrouter, 'zdr', cfg.openrouter.zdr, 'OpenRouter zero data retention'),
160
+ },
161
+ codex: { model: field(codex, 'model', cfg.codex.model, 'Codex model'), auth: cfg.codex.auth },
162
+ gemini: { model: field(gemini, 'model', cfg.gemini.model, 'Gemini model'), auth: cfg.gemini.auth },
163
+ };
164
+ }
165
+
166
+ export const setCodexAuth = (cfg: ModelConfig, auth: CodexTokens | null): ModelConfig => ({ ...cfg, codex: { ...cfg.codex, auth } });
167
+
168
+ export const setGeminiAuth = (cfg: ModelConfig, auth: GeminiTokens | null): ModelConfig => ({ ...cfg, gemini: { ...cfg.gemini, auth } });
169
+
170
+ const CHECKS: Record<Provider, [(cfg: ModelConfig) => boolean, string][]> = {
171
+ anthropic: [],
172
+ bedrock: [
173
+ [(cfg) => cfg.bedrock.apiKey === '', 'Bedrock needs an API key: add it on the Model page.'],
174
+ [(cfg) => cfg.bedrock.region === '', 'Bedrock needs a region: add it on the Model page.'],
175
+ ],
176
+ openrouter: [
177
+ [(cfg) => cfg.openrouter.apiKey === '', 'OpenRouter needs an API key: add it on the Model page.'],
178
+ [(cfg) => cfg.openrouter.model === '', 'OpenRouter needs a model id: choose one on the Model page.'],
179
+ ],
180
+ codex: [
181
+ [(cfg) => cfg.codex.auth === null, 'Codex is not connected: sign in with ChatGPT on the Model page.'],
182
+ [(cfg) => cfg.codex.model === '', 'Codex needs a model id: choose one on the Model page.'],
183
+ ],
184
+ gemini: [
185
+ [(cfg) => cfg.gemini.auth === null, 'Gemini is not connected: sign in with Google on the Model page.'],
186
+ [(cfg) => cfg.gemini.model === '', 'Gemini needs a model id: choose one on the Model page.'],
187
+ ],
188
+ };
189
+
190
+ export function notReady(cfg: ModelConfig, provider: Provider = cfg.provider): string | null {
191
+ return CHECKS[provider].find(([missing]) => missing(cfg))?.[1] ?? null;
192
+ }
193
+
194
+ export function publicModelConfig(cfg: ModelConfig): Record<string, unknown> {
195
+ const auth = cfg.codex.auth;
196
+ const gemini = cfg.gemini.auth;
197
+ return {
198
+ provider: cfg.provider,
199
+ ready: notReady(cfg) === null,
200
+ reason: notReady(cfg),
201
+ anthropic: { model: cfg.anthropic.model, hasKey: cfg.anthropic.apiKey !== '' },
202
+ bedrock: { region: cfg.bedrock.region, model: cfg.bedrock.model, hasKey: cfg.bedrock.apiKey !== '' },
203
+ openrouter: { model: cfg.openrouter.model, hasKey: cfg.openrouter.apiKey !== '', zdr: cfg.openrouter.zdr },
204
+ codex: { model: cfg.codex.model, signedIn: auth !== null, account: auth?.email ?? null, plan: auth?.plan ?? null },
205
+ gemini: { model: cfg.gemini.model, signedIn: gemini !== null, account: gemini?.email ?? null, plan: gemini?.tier ?? null },
206
+ };
207
+ }
208
+
209
+ export const isSmallModel = (requested: string): boolean => SMALL_RE.test(requested);
210
+
211
+ const DEFAULTS: Record<Provider, (requested: string, cfg: ModelConfig) => string> = {
212
+ openrouter: (requested, cfg) => (requested.includes('/') ? requested : cfg.openrouter.model),
213
+ bedrock: (requested, cfg) => (cfg.bedrock.model === '' ? requested : cfg.bedrock.model),
214
+ codex: (requested, cfg) => (requested.startsWith('gpt-') ? requested : cfg.codex.model),
215
+ gemini: (requested, cfg) => (requested.startsWith('gemini-') ? requested : cfg.gemini.model),
216
+ anthropic: (requested, cfg) => (cfg.anthropic.model === '' || isSmallModel(requested) ? requested : cfg.anthropic.model),
217
+ };
218
+
219
+ function defaultModelFor(provider: Provider, requested: string, cfg: ModelConfig): string {
220
+ return DEFAULTS[provider](requested, cfg);
221
+ }
222
+
223
+ export function resolveRoute(requested: string, cfg: ModelConfig): Route {
224
+ const explicit = PREFIX_RE.exec(requested);
225
+ if (explicit !== null && isProvider(explicit[1]))
226
+ return { provider: explicit[1], model: (explicit[2] ?? '').trim() };
227
+ return { provider: cfg.provider, model: defaultModelFor(cfg.provider, requested, cfg) };
228
+ }
229
+
230
+ export function routeLabel(route: Route): string {
231
+ return route.provider === 'anthropic' ? route.model : `${route.provider}:${route.model}`;
232
+ }
@@ -0,0 +1,90 @@
1
+ import { isRecord } from '@metro-labs/core/is-record';
2
+ import { GatewayError } from './forward.js';
3
+
4
+ export const OPENROUTER_BASE = 'https://openrouter.ai/api';
5
+ const MODELS_MAX = 2000;
6
+
7
+ export interface OpenRouterModel {
8
+ id: string;
9
+ name: string;
10
+ prompt: number | null;
11
+ completion: number | null;
12
+ created: number | null;
13
+ }
14
+
15
+ const str = (value: unknown): string => (typeof value === 'string' ? value : '');
16
+
17
+ function price(raw: unknown): number | null {
18
+ const value = typeof raw === 'string' ? Number(raw) : typeof raw === 'number' ? raw : Number.NaN;
19
+ return Number.isFinite(value) && value >= 0 ? value : null;
20
+ }
21
+
22
+ function released(raw: unknown): number | null {
23
+ return typeof raw === 'number' && Number.isFinite(raw) && raw > 0 ? raw : null;
24
+ }
25
+
26
+ function modelOf(entry: unknown): OpenRouterModel | null {
27
+ if (!isRecord(entry)) return null;
28
+ const id = str(entry.id);
29
+ if (id === '') return null;
30
+ const pricing = isRecord(entry.pricing) ? entry.pricing : {};
31
+ return {
32
+ id,
33
+ name: str(entry.name) || id,
34
+ prompt: price(pricing.prompt),
35
+ completion: price(pricing.completion),
36
+ created: released(entry.created),
37
+ };
38
+ }
39
+
40
+ export function newestFirst(a: OpenRouterModel, b: OpenRouterModel): number {
41
+ const when = (b.created ?? 0) - (a.created ?? 0);
42
+ return when !== 0 ? when : a.id.localeCompare(b.id);
43
+ }
44
+
45
+ export async function openrouterModels(base = OPENROUTER_BASE, fetchImpl: typeof fetch = fetch): Promise<OpenRouterModel[]> {
46
+ const res = await fetchImpl(`${base}/v1/models`, { headers: { accept: 'application/json' }, redirect: 'manual' });
47
+ if (!res.ok) throw new GatewayError(res.status, 'api_error', `OpenRouter would not list its models (${String(res.status)})`);
48
+ const body: unknown = await res.json();
49
+ const data = typeof body === 'object' && body !== null ? (body as { data?: unknown }).data : undefined;
50
+ if (!Array.isArray(data)) throw new GatewayError(502, 'api_error', 'OpenRouter answered with no model list');
51
+ return data
52
+ .map(modelOf)
53
+ .filter((model): model is OpenRouterModel => model !== null)
54
+ .sort(newestFirst)
55
+ .slice(0, MODELS_MAX);
56
+ }
57
+
58
+ export interface OpenRouterCredits {
59
+ total: number;
60
+ spent: number;
61
+ }
62
+
63
+ export async function openrouterCredits(
64
+ apiKey: string,
65
+ base = OPENROUTER_BASE,
66
+ fetchImpl: typeof fetch = fetch,
67
+ ): Promise<OpenRouterCredits> {
68
+ const res = await fetchImpl(`${base}/v1/credits`, {
69
+ headers: { accept: 'application/json', authorization: `Bearer ${apiKey}` },
70
+ redirect: 'manual',
71
+ });
72
+ if (!res.ok) throw new GatewayError(res.status, 'api_error', `OpenRouter would not report the credits (${String(res.status)})`);
73
+ const body: unknown = await res.json();
74
+ const data = isRecord(body) && isRecord(body.data) ? body.data : {};
75
+ const total = Number(data.total_credits);
76
+ const spent = Number(data.total_usage);
77
+ if (!Number.isFinite(total) || !Number.isFinite(spent)) throw new GatewayError(502, 'api_error', 'OpenRouter answered with no credit figures');
78
+ return { total, spent };
79
+ }
80
+
81
+ export async function openrouterZdrModels(base = OPENROUTER_BASE, fetchImpl: typeof fetch = fetch): Promise<string[]> {
82
+ const res = await fetchImpl(`${base}/v1/endpoints/zdr`, { headers: { accept: 'application/json' }, redirect: 'manual' });
83
+ if (!res.ok) throw new GatewayError(res.status, 'api_error', `OpenRouter would not list its zero data retention endpoints (${String(res.status)})`);
84
+ const body: unknown = await res.json();
85
+ const data = typeof body === 'object' && body !== null ? (body as { data?: unknown }).data : undefined;
86
+ if (!Array.isArray(data)) throw new GatewayError(502, 'api_error', 'OpenRouter answered with no endpoint list');
87
+ const ids = new Set<string>();
88
+ for (const entry of data) if (isRecord(entry) && str(entry.model_id) !== '') ids.add(str(entry.model_id));
89
+ return [...ids].sort();
90
+ }