@stage-labs/metro 0.1.0-beta.13 → 0.1.0-beta.130

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 (328) hide show
  1. package/dist/api.js +14 -0
  2. package/dist/claude-settings.js +40 -0
  3. package/dist/claude.js +176 -0
  4. package/dist/cli.js +101 -0
  5. package/dist/control.js +74 -0
  6. package/dist/hold.js +211 -0
  7. package/dist/local.js +93 -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/plugin.js +120 -0
  12. package/dist/provider-flags.js +9 -0
  13. package/dist/route.js +38 -0
  14. package/dist/runtime-install.js +97 -0
  15. package/dist/runtime.js +74 -0
  16. package/dist/serve.js +150 -0
  17. package/dist/service.js +242 -0
  18. package/dist/tail.js +119 -0
  19. package/dist/update.js +45 -0
  20. package/dist/version.js +97 -0
  21. package/package.json +14 -27
  22. package/runtime/marketplace/.claude-plugin/marketplace.json +12 -0
  23. package/runtime/marketplace/plugin/.claude-plugin/plugin.json +18 -0
  24. package/runtime/marketplace/plugin/.mcp.json +1 -0
  25. package/runtime/marketplace/plugin/bin/guard.mjs +55 -0
  26. package/runtime/marketplace/plugin/bin/metro-plugin.mjs +122 -0
  27. package/runtime/marketplace/plugin/bin/session-start.mjs +31 -0
  28. package/runtime/marketplace/plugin/hooks/hooks.json +25 -0
  29. package/runtime/marketplace/plugin/orchestrator.md +16 -0
  30. package/runtime/marketplace/plugin/skills/refresh/SKILL.md +15 -0
  31. package/runtime/node_modules/@metro-labs/core/package.json +43 -0
  32. package/runtime/node_modules/@metro-labs/core/src/address.ts +6 -0
  33. package/runtime/node_modules/@metro-labs/core/src/endpoints.ts +7 -0
  34. package/runtime/node_modules/@metro-labs/core/src/events.ts +120 -0
  35. package/runtime/node_modules/@metro-labs/core/src/identity.ts +7 -0
  36. package/runtime/node_modules/@metro-labs/core/src/ids.ts +21 -0
  37. package/runtime/node_modules/@metro-labs/core/src/is-record.ts +2 -0
  38. package/runtime/node_modules/@metro-labs/core/src/lines.ts +87 -0
  39. package/runtime/node_modules/@metro-labs/core/src/log.ts +42 -0
  40. package/runtime/node_modules/@metro-labs/core/src/protocol.ts +226 -0
  41. package/runtime/node_modules/@metro-labs/core/src/secure-fs.ts +57 -0
  42. package/runtime/node_modules/@metro-labs/core/src/station-names.ts +13 -0
  43. package/runtime/node_modules/@metro-labs/core/src/stations/account-store.ts +60 -0
  44. package/runtime/node_modules/@metro-labs/core/src/stations/attachments.ts +196 -0
  45. package/runtime/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +88 -0
  46. package/runtime/node_modules/@metro-labs/core/src/stations/station-runtime.ts +64 -0
  47. package/runtime/node_modules/@metro-labs/core/src/stations/types.ts +108 -0
  48. package/runtime/node_modules/@metro-labs/core/src/str.ts +5 -0
  49. package/runtime/node_modules/@metro-labs/core/src/train-error.ts +53 -0
  50. package/runtime/node_modules/@metro-labs/core/src/version.ts +2 -0
  51. package/runtime/node_modules/@metro-labs/daemon/package.json +48 -0
  52. package/runtime/node_modules/@metro-labs/daemon/src/agents/account-attach.ts +19 -0
  53. package/runtime/node_modules/@metro-labs/daemon/src/agents/account-routes.ts +70 -0
  54. package/runtime/node_modules/@metro-labs/daemon/src/agents/accounts-api.ts +365 -0
  55. package/runtime/node_modules/@metro-labs/daemon/src/agents/admin.ts +43 -0
  56. package/runtime/node_modules/@metro-labs/daemon/src/agents/allowlist.ts +37 -0
  57. package/runtime/node_modules/@metro-labs/daemon/src/agents/api.ts +300 -0
  58. package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +122 -0
  59. package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +371 -0
  60. package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +155 -0
  61. package/runtime/node_modules/@metro-labs/daemon/src/agents/keys.ts +45 -0
  62. package/runtime/node_modules/@metro-labs/daemon/src/agents/map.ts +95 -0
  63. package/runtime/node_modules/@metro-labs/daemon/src/agents/scope.ts +64 -0
  64. package/runtime/node_modules/@metro-labs/daemon/src/agents/senders.ts +38 -0
  65. package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +207 -0
  66. package/runtime/node_modules/@metro-labs/daemon/src/boot/connect-hint.ts +39 -0
  67. package/runtime/node_modules/@metro-labs/daemon/src/boot/crash-guard.ts +49 -0
  68. package/runtime/node_modules/@metro-labs/daemon/src/boot/local-owner.ts +14 -0
  69. package/runtime/node_modules/@metro-labs/daemon/src/boot/paths.ts +106 -0
  70. package/runtime/node_modules/@metro-labs/daemon/src/channels/addressed.ts +25 -0
  71. package/runtime/node_modules/@metro-labs/daemon/src/channels/dedupe.ts +24 -0
  72. package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +377 -0
  73. package/runtime/node_modules/@metro-labs/daemon/src/channels/media-note.ts +85 -0
  74. package/runtime/node_modules/@metro-labs/daemon/src/channels/pending.ts +75 -0
  75. package/runtime/node_modules/@metro-labs/daemon/src/channels/relay.ts +129 -0
  76. package/runtime/node_modules/@metro-labs/daemon/src/channels/webhook-note.ts +51 -0
  77. package/runtime/node_modules/@metro-labs/daemon/src/claude/api.ts +309 -0
  78. package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +358 -0
  79. package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +169 -0
  80. package/runtime/node_modules/@metro-labs/daemon/src/claude/onboarding.ts +56 -0
  81. package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-install.ts +140 -0
  82. package/runtime/node_modules/@metro-labs/daemon/src/claude/session-files.ts +90 -0
  83. package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +253 -0
  84. package/runtime/node_modules/@metro-labs/daemon/src/claude/settings.ts +117 -0
  85. package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +239 -0
  86. package/runtime/node_modules/@metro-labs/daemon/src/claude/skills.ts +154 -0
  87. package/runtime/node_modules/@metro-labs/daemon/src/claude/version.ts +109 -0
  88. package/runtime/node_modules/@metro-labs/daemon/src/connectors/api.ts +310 -0
  89. package/runtime/node_modules/@metro-labs/daemon/src/connectors/cli-api.ts +63 -0
  90. package/runtime/node_modules/@metro-labs/daemon/src/connectors/config.ts +175 -0
  91. package/runtime/node_modules/@metro-labs/daemon/src/connectors/json.ts +21 -0
  92. package/runtime/node_modules/@metro-labs/daemon/src/connectors/model.ts +77 -0
  93. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-client.ts +166 -0
  94. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-discovery.ts +139 -0
  95. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-pending.ts +65 -0
  96. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-routes.ts +171 -0
  97. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth.ts +155 -0
  98. package/runtime/node_modules/@metro-labs/daemon/src/connectors/plugin-sync.ts +122 -0
  99. package/runtime/node_modules/@metro-labs/daemon/src/connectors/reach.ts +27 -0
  100. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-target.ts +39 -0
  101. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +296 -0
  102. package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +286 -0
  103. package/runtime/node_modules/@metro-labs/daemon/src/connectors/tools.ts +98 -0
  104. package/runtime/node_modules/@metro-labs/daemon/src/connectors/url.ts +70 -0
  105. package/runtime/node_modules/@metro-labs/daemon/src/connectors/verify.ts +332 -0
  106. package/runtime/node_modules/@metro-labs/daemon/src/connectors/watch.ts +54 -0
  107. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-grant.ts +99 -0
  108. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-owner.ts +32 -0
  109. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-serve.ts +121 -0
  110. package/runtime/node_modules/@metro-labs/daemon/src/files/upload-api.ts +294 -0
  111. package/runtime/node_modules/@metro-labs/daemon/src/files/upload-store.ts +181 -0
  112. package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +259 -0
  113. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +231 -0
  114. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-device.ts +91 -0
  115. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-stream.ts +288 -0
  116. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-translate.ts +203 -0
  117. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +231 -0
  118. package/runtime/node_modules/@metro-labs/daemon/src/gateway/eventstream.ts +123 -0
  119. package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +187 -0
  120. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +229 -0
  121. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +269 -0
  122. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +207 -0
  123. package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +90 -0
  124. package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +65 -0
  125. package/runtime/node_modules/@metro-labs/daemon/src/gateway/served.ts +17 -0
  126. package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage.ts +229 -0
  127. package/runtime/node_modules/@metro-labs/daemon/src/mcp/accounts.ts +151 -0
  128. package/runtime/node_modules/@metro-labs/daemon/src/mcp/call-tools.ts +216 -0
  129. package/runtime/node_modules/@metro-labs/daemon/src/mcp/channel-owner.ts +29 -0
  130. package/runtime/node_modules/@metro-labs/daemon/src/mcp/ctx.ts +62 -0
  131. package/runtime/node_modules/@metro-labs/daemon/src/mcp/event-store.ts +91 -0
  132. package/runtime/node_modules/@metro-labs/daemon/src/mcp/frame-scope.ts +21 -0
  133. package/runtime/node_modules/@metro-labs/daemon/src/mcp/group-tools.ts +129 -0
  134. package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +191 -0
  135. package/runtime/node_modules/@metro-labs/daemon/src/mcp/member-tools.ts +72 -0
  136. package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-relay.ts +66 -0
  137. package/runtime/node_modules/@metro-labs/daemon/src/mcp/raw-get-stream.ts +207 -0
  138. package/runtime/node_modules/@metro-labs/daemon/src/mcp/request-identity.ts +47 -0
  139. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-registry.ts +172 -0
  140. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-route.ts +43 -0
  141. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +215 -0
  142. package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-dispatch.ts +215 -0
  143. package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-schemas.ts +400 -0
  144. package/runtime/node_modules/@metro-labs/daemon/src/mcp/upload-tool.ts +66 -0
  145. package/runtime/node_modules/@metro-labs/daemon/src/monitor/api.ts +237 -0
  146. package/runtime/node_modules/@metro-labs/daemon/src/net/tunnel.ts +389 -0
  147. package/runtime/node_modules/@metro-labs/daemon/src/routes/body.ts +24 -0
  148. package/runtime/node_modules/@metro-labs/daemon/src/routes/http.ts +372 -0
  149. package/runtime/node_modules/@metro-labs/daemon/src/routes/identity.ts +54 -0
  150. package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +223 -0
  151. package/runtime/node_modules/@metro-labs/daemon/src/routes/session-apis.ts +60 -0
  152. package/runtime/node_modules/@metro-labs/daemon/src/routes/session.ts +29 -0
  153. package/runtime/node_modules/@metro-labs/daemon/src/routes/threema-callback.ts +103 -0
  154. package/runtime/node_modules/@metro-labs/daemon/src/server/control.ts +64 -0
  155. package/runtime/node_modules/@metro-labs/daemon/src/server/machine.ts +61 -0
  156. package/runtime/node_modules/@metro-labs/daemon/src/server/update.ts +116 -0
  157. package/runtime/node_modules/@metro-labs/daemon/src/server.ts +2 -0
  158. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-inline.ts +100 -0
  159. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-interactive.ts +161 -0
  160. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-resolve.ts +202 -0
  161. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-session.ts +280 -0
  162. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-xmtp.ts +111 -0
  163. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach.ts +248 -0
  164. package/runtime/node_modules/@metro-labs/daemon/src/stations/materialize.ts +261 -0
  165. package/runtime/node_modules/@metro-labs/daemon/src/stations/registry.ts +37 -0
  166. package/runtime/node_modules/@metro-labs/daemon/src/stations/runtime-deps.ts +82 -0
  167. package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor-io.ts +97 -0
  168. package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor.ts +320 -0
  169. package/runtime/node_modules/@metro-labs/daemon/src/stations/threema-callbacks.ts +32 -0
  170. package/runtime/node_modules/@metro-labs/daemon/src/stations/train-call.ts +22 -0
  171. package/runtime/node_modules/@metro-labs/daemon/src/terminal/api.ts +96 -0
  172. package/runtime/node_modules/@metro-labs/daemon/src/terminal/socket.ts +123 -0
  173. package/runtime/node_modules/@metro-labs/daemon/src/terminal/tickets.ts +40 -0
  174. package/runtime/node_modules/@metro-labs/discord-bot/package.json +28 -0
  175. package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +118 -0
  176. package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +388 -0
  177. package/runtime/node_modules/@metro-labs/discord-bot/src/api-base.ts +1 -0
  178. package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +29 -0
  179. package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +220 -0
  180. package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +50 -0
  181. package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +115 -0
  182. package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +127 -0
  183. package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +215 -0
  184. package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +43 -0
  185. package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +19 -0
  186. package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +82 -0
  187. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-speak.ts +195 -0
  188. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-transcribe.ts +292 -0
  189. package/runtime/node_modules/@metro-labs/discord-bot/src/voice.ts +197 -0
  190. package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +5 -0
  191. package/runtime/node_modules/@metro-labs/http/package.json +33 -0
  192. package/runtime/node_modules/@metro-labs/http/src/api-error.ts +8 -0
  193. package/runtime/node_modules/@metro-labs/http/src/api-http.ts +95 -0
  194. package/runtime/node_modules/@metro-labs/http/src/cors.ts +29 -0
  195. package/runtime/node_modules/@metro-labs/http/src/identity-key.ts +33 -0
  196. package/runtime/node_modules/@metro-labs/http/src/identity-registry.ts +13 -0
  197. package/runtime/node_modules/@metro-labs/http/src/mode-api.ts +32 -0
  198. package/runtime/node_modules/@metro-labs/http/src/return-to.ts +21 -0
  199. package/runtime/node_modules/@metro-labs/http/src/signed-identity.ts +36 -0
  200. package/runtime/node_modules/@metro-labs/telegram/package.json +26 -0
  201. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +80 -0
  202. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +254 -0
  203. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +87 -0
  204. package/runtime/node_modules/@metro-labs/telegram/src/client.ts +77 -0
  205. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +156 -0
  206. package/runtime/node_modules/@metro-labs/telegram/src/group.ts +261 -0
  207. package/runtime/node_modules/@metro-labs/telegram/src/history.ts +52 -0
  208. package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +78 -0
  209. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +46 -0
  210. package/runtime/node_modules/@metro-labs/telegram/src/login.ts +166 -0
  211. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +80 -0
  212. package/runtime/node_modules/@metro-labs/telegram/src/members.ts +75 -0
  213. package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +22 -0
  214. package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +167 -0
  215. package/runtime/node_modules/@metro-labs/telegram/src/station.ts +24 -0
  216. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +7 -0
  217. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -0
  218. package/runtime/node_modules/@metro-labs/telegram-bot/package.json +25 -0
  219. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +120 -0
  220. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +295 -0
  221. package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
  222. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +201 -0
  223. package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +109 -0
  224. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
  225. package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
  226. package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +17 -0
  227. package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +58 -0
  228. package/runtime/node_modules/@metro-labs/telegram-bot/src/verify.ts +38 -0
  229. package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +3 -0
  230. package/runtime/node_modules/@metro-labs/threema/package.json +26 -0
  231. package/runtime/node_modules/@metro-labs/threema/src/accounts.ts +100 -0
  232. package/runtime/node_modules/@metro-labs/threema/src/actions.ts +219 -0
  233. package/runtime/node_modules/@metro-labs/threema/src/api.ts +130 -0
  234. package/runtime/node_modules/@metro-labs/threema/src/crypto.ts +132 -0
  235. package/runtime/node_modules/@metro-labs/threema/src/format.ts +123 -0
  236. package/runtime/node_modules/@metro-labs/threema/src/ids.ts +21 -0
  237. package/runtime/node_modules/@metro-labs/threema/src/index.ts +29 -0
  238. package/runtime/node_modules/@metro-labs/threema/src/station.ts +10 -0
  239. package/runtime/node_modules/@metro-labs/threema/src/verify.ts +117 -0
  240. package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
  241. package/runtime/node_modules/@metro-labs/webhook/src/station.ts +37 -0
  242. package/runtime/node_modules/@metro-labs/whatsapp/package.json +27 -0
  243. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +66 -0
  244. package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
  245. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +190 -0
  246. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
  247. package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
  248. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +347 -0
  249. package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
  250. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +138 -0
  251. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
  252. package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
  253. package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
  254. package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
  255. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
  256. package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
  257. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
  258. package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
  259. package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +185 -0
  260. package/runtime/node_modules/@metro-labs/whatsapp/src/resolve.ts +52 -0
  261. package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +18 -0
  262. package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
  263. package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
  264. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
  265. package/runtime/node_modules/@metro-labs/xmtp/package.json +33 -0
  266. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +112 -0
  267. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
  268. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
  269. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
  270. package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
  271. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +375 -0
  272. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
  273. package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
  274. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
  275. package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
  276. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +335 -0
  277. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +80 -0
  278. package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
  279. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
  280. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +33 -0
  281. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
  282. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
  283. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
  284. package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
  285. package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +13 -0
  286. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
  287. package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
  288. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
  289. package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +84 -0
  290. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +53 -0
  291. package/runtime/runtime.json +3 -0
  292. package/runtime/server.ts +1 -0
  293. package/runtime/stations.json +39 -0
  294. package/runtime/trains/.keep +0 -0
  295. package/LICENSE +0 -21
  296. package/README.md +0 -252
  297. package/dist/broker.js +0 -202
  298. package/dist/cache.js +0 -69
  299. package/dist/cli/actions.js +0 -206
  300. package/dist/cli/config.js +0 -185
  301. package/dist/cli/index.js +0 -188
  302. package/dist/cli/skill.js +0 -62
  303. package/dist/cli/tail.js +0 -164
  304. package/dist/cli/util.js +0 -72
  305. package/dist/cli/webhook.js +0 -81
  306. package/dist/codex-rc.js +0 -245
  307. package/dist/dispatcher.js +0 -128
  308. package/dist/history.js +0 -121
  309. package/dist/ipc.js +0 -72
  310. package/dist/log.js +0 -15
  311. package/dist/monitor.js +0 -194
  312. package/dist/paths.js +0 -68
  313. package/dist/registry.js +0 -48
  314. package/dist/stations/claude.js +0 -45
  315. package/dist/stations/codex.js +0 -68
  316. package/dist/stations/discord.js +0 -216
  317. package/dist/stations/index.js +0 -129
  318. package/dist/stations/telegram-md.js +0 -34
  319. package/dist/stations/telegram-upload.js +0 -113
  320. package/dist/stations/telegram.js +0 -234
  321. package/dist/stations/webhook.js +0 -103
  322. package/dist/tunnel.js +0 -64
  323. package/dist/webhooks.js +0 -41
  324. package/docs/broker.md +0 -214
  325. package/docs/monitor.md +0 -163
  326. package/docs/uri-scheme.md +0 -124
  327. package/docs/users.md +0 -226
  328. package/skills/metro/SKILL.md +0 -256
package/dist/monitor.js DELETED
@@ -1,194 +0,0 @@
1
- /** Read-only HTTP monitor endpoints. `/api/state` (snapshot) + `/api/tail` (SSE). */
2
- /** Mounted on the webhook server. Bearer auth via METRO_MONITOR_TOKEN (503 when unset). */
3
- import { timingSafeEqual } from 'node:crypto';
4
- import { watch } from 'node:fs';
5
- import pkg from '../package.json' with { type: 'json' };
6
- import { errMsg, log } from './log.js';
7
- import { HISTORY_FILE, historySize, passesMode, readClaims, readEntriesFrom, } from './broker.js';
8
- import { readBotIds } from './cache.js';
9
- import { readHistory } from './history.js';
10
- import { asLine } from './stations/index.js';
11
- const HISTORY_LIMIT = 100;
12
- function authorized(req) {
13
- const token = process.env.METRO_MONITOR_TOKEN;
14
- if (!token)
15
- return { ok: false, status: 503, msg: 'monitor endpoints not configured (METRO_MONITOR_TOKEN unset)' };
16
- const header = req.headers['authorization'];
17
- const value = Array.isArray(header) ? header[0] : header;
18
- if (!value || !value.startsWith('Bearer '))
19
- return { ok: false, status: 401, msg: 'unauthorized' };
20
- const given = Buffer.from(value.slice('Bearer '.length));
21
- const want = Buffer.from(token);
22
- if (given.length !== want.length || !timingSafeEqual(given, want)) {
23
- return { ok: false, status: 401, msg: 'unauthorized' };
24
- }
25
- return { ok: true };
26
- }
27
- /** Hosts that serve `/api/*`. webhook.metro.box stays scoped to /wh/*. */
28
- const MONITOR_HOSTS = new Set(['monitor.metro.box', 'localhost', '127.0.0.1']);
29
- function monitorHostAllowed(req) {
30
- const raw = req.headers[':authority'] ?? req.headers.host;
31
- if (!raw)
32
- return true;
33
- return MONITOR_HOSTS.has(raw.split(':')[0].toLowerCase());
34
- }
35
- export function handleMonitorRequest(req, res) {
36
- const url = req.url ?? '';
37
- if (!url.startsWith('/api/'))
38
- return false;
39
- if (!monitorHostAllowed(req))
40
- return false; // let outer router 404 it
41
- const [pathOnly, queryString = ''] = url.split('?', 2);
42
- if (req.method !== 'GET') {
43
- res.writeHead(405, { 'content-type': 'application/json' });
44
- res.end(JSON.stringify({ error: 'method not allowed' }));
45
- return true;
46
- }
47
- const auth = authorized(req);
48
- if (!auth.ok) {
49
- res.writeHead(auth.status, { 'content-type': 'application/json' });
50
- res.end(JSON.stringify({ error: auth.msg }));
51
- return true;
52
- }
53
- const query = new URLSearchParams(queryString);
54
- if (pathOnly === '/api/state') {
55
- handleState(res, query);
56
- return true;
57
- }
58
- if (pathOnly === '/api/tail') {
59
- handleTail(req, res, query).catch(err => {
60
- log.warn({ err: errMsg(err) }, 'monitor: tail handler error');
61
- try {
62
- if (!res.headersSent)
63
- res.writeHead(500).end();
64
- else
65
- res.end();
66
- }
67
- catch { /* ignore */ }
68
- });
69
- return true;
70
- }
71
- res.writeHead(404, { 'content-type': 'application/json' });
72
- res.end(JSON.stringify({ error: 'not found' }));
73
- return true;
74
- }
75
- function parseNonNegInt(raw) {
76
- if (raw === null)
77
- return null;
78
- const n = Number(raw);
79
- if (!Number.isFinite(n) || n < 0 || !Number.isInteger(n))
80
- return null;
81
- return n;
82
- }
83
- function handleState(res, query) {
84
- const before = parseNonNegInt(query.get('before'));
85
- const limitRaw = parseNonNegInt(query.get('limit'));
86
- /** Page mode: skip `before` newest entries, return next `limit` (default HISTORY_LIMIT, capped at 500). */
87
- if (before !== null) {
88
- const limit = Math.min(limitRaw ?? HISTORY_LIMIT, 500);
89
- const page = readHistory({ limit, skip: before });
90
- res.writeHead(200, { 'content-type': 'application/json' });
91
- res.end(JSON.stringify({ recent_history: page }));
92
- return;
93
- }
94
- /** `readHistory` is newest-first — what the activity feed expects. */
95
- const limit = Math.min(limitRaw ?? HISTORY_LIMIT, 500);
96
- const recent = readHistory({ limit });
97
- const claims = readClaims();
98
- const linesSet = new Set();
99
- for (const e of recent)
100
- linesSet.add(e.line);
101
- for (const line of Object.keys(claims))
102
- linesSet.add(line);
103
- res.writeHead(200, { 'content-type': 'application/json' });
104
- res.end(JSON.stringify({
105
- claims,
106
- lines: [...linesSet],
107
- recent_history: recent,
108
- bot_ids: readBotIds(),
109
- version: pkg.version,
110
- }));
111
- }
112
- /** Mirrors `cli/tail.ts:resolveMode` but operates on URLSearchParams. */
113
- function resolveQueryMode(query, self) {
114
- const strict = query.get('strict') === 'true' || query.get('mode') === 'strict';
115
- const unclaimed = query.get('unclaimed') === 'true' || query.get('mode') === 'unclaimed';
116
- const all = query.get('all') === 'true' || query.get('mode') === 'all';
117
- if ([strict, unclaimed, all].filter(Boolean).length > 1)
118
- return 'all';
119
- if (strict && self)
120
- return 'mine-only';
121
- if (unclaimed)
122
- return 'unclaimed';
123
- if (all || !self)
124
- return 'all';
125
- return 'mine-or-unclaimed';
126
- }
127
- async function handleTail(req, res, query) {
128
- const asParam = query.get('as');
129
- const self = asParam ? asLine(asParam) : null;
130
- const mode = resolveQueryMode(query, self);
131
- const chatFilter = query.get('chat');
132
- const stationFilter = query.get('station');
133
- const includeWebhooks = query.get('include_webhooks') === 'true';
134
- res.writeHead(200, {
135
- 'content-type': 'text/event-stream',
136
- 'cache-control': 'no-cache, no-transform',
137
- 'connection': 'keep-alive',
138
- /** Bearer auth already gates us; CORS can be permissive. */
139
- 'access-control-allow-origin': '*',
140
- /** Cloudflare/proxies buffer SSE without this hint. */
141
- 'x-accel-buffering': 'no',
142
- });
143
- /** `since=tail` (default) starts at EOF; `since=0` replays the full file. */
144
- const since = query.get('since');
145
- let offset = since === '0' ? 0 : historySize();
146
- if (since && since !== '0' && since !== 'tail') {
147
- const n = Number(since);
148
- if (Number.isFinite(n) && n >= 0)
149
- offset = n;
150
- }
151
- /** 4 KiB padding so Cloudflare's HTTP/2 buffer flushes (else holds 30+ s on free tier). */
152
- res.write(`: metro monitor tail (mode=${mode}${self ? `, as=${self}` : ''})\n`);
153
- res.write(`: ${'-'.repeat(4096)}\n\n`);
154
- const drain = () => {
155
- const claims = readClaims();
156
- for (const { entry, offset: next } of readEntriesFrom(offset)) {
157
- offset = next;
158
- if (chatFilter && entry.line !== chatFilter)
159
- continue;
160
- if (stationFilter && entry.station !== stationFilter)
161
- continue;
162
- if (!passesMode(entry, mode, self, claims, { includeWebhooks }))
163
- continue;
164
- res.write(`id: ${entry.id}\n`);
165
- res.write('event: history\n');
166
- res.write(`data: ${JSON.stringify(entry)}\n\n`);
167
- }
168
- };
169
- drain();
170
- /** fs.watch coalesces on macOS — poll every 1s as a backstop. */
171
- let watcher = null;
172
- try {
173
- watcher = watch(HISTORY_FILE, () => drain());
174
- }
175
- catch { /* file may not exist yet */ }
176
- const poll = setInterval(drain, 1_000);
177
- const keepalive = setInterval(() => res.write(': keepalive\n\n'), 25_000);
178
- const cleanup = () => {
179
- clearInterval(poll);
180
- clearInterval(keepalive);
181
- if (watcher) {
182
- try {
183
- watcher.close();
184
- }
185
- catch { /* ignore */ }
186
- }
187
- try {
188
- res.end();
189
- }
190
- catch { /* ignore */ }
191
- };
192
- req.on('close', cleanup);
193
- req.on('error', cleanup);
194
- }
package/dist/paths.js DELETED
@@ -1,68 +0,0 @@
1
- import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
2
- import { homedir } from 'node:os';
3
- import { dirname, join } from 'node:path';
4
- import { log } from './log.js';
5
- export const STATE_DIR = process.env.METRO_STATE_DIR ?? join(homedir(), '.cache', 'metro');
6
- mkdirSync(STATE_DIR, { recursive: true });
7
- const CONFIG_DIR = process.env.METRO_CONFIG_DIR ?? join(process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'metro');
8
- export const CONFIG_ENV_FILE = join(CONFIG_DIR, '.env');
9
- const LINE_RE = /^\s*([A-Za-z_]\w*)\s*=\s*(.*?)\s*$/;
10
- const QUOTED_RE = /^(['"])(.*)\1$/;
11
- export function readDotenv(path) {
12
- if (!existsSync(path))
13
- return {};
14
- const out = {};
15
- for (const line of readFileSync(path, 'utf8').split('\n')) {
16
- const m = line.match(LINE_RE);
17
- if (m)
18
- out[m[1]] = m[2].replace(QUOTED_RE, '$2');
19
- }
20
- return out;
21
- }
22
- export function writeDotenv(path, env) {
23
- mkdirSync(dirname(path), { recursive: true });
24
- writeFileSync(path, Object.entries(env).map(([k, v]) => `${k}=${v}`).join('\n') + '\n');
25
- chmodSync(path, 0o600);
26
- }
27
- /** Precedence: process.env > cwd/.env > $METRO_CONFIG_DIR/.env. First-set wins. */
28
- export function loadMetroEnv() {
29
- for (const path of [join(process.cwd(), '.env'), CONFIG_ENV_FILE]) {
30
- for (const [k, v] of Object.entries(readDotenv(path))) {
31
- if (process.env[k] === undefined)
32
- process.env[k] = v;
33
- }
34
- }
35
- }
36
- export function configuredPlatforms() {
37
- return { telegram: !!process.env.TELEGRAM_BOT_TOKEN, discord: !!process.env.DISCORD_BOT_TOKEN };
38
- }
39
- export function requireConfiguredPlatform(p, hasWebhooks) {
40
- if (p.telegram || p.discord || hasWebhooks)
41
- return;
42
- log.fatal('no inputs configured — run `metro setup telegram <token>`, `metro setup discord <token>`, or `metro webhook add <label>`');
43
- process.exit(2);
44
- }
45
- /** Singleton pidfile. Exits if another instance owns it; reclaims stale locks. */
46
- export function acquireLock(lockFile) {
47
- if (existsSync(lockFile)) {
48
- const pid = Number(readFileSync(lockFile, 'utf8').trim());
49
- try {
50
- if (Number.isInteger(pid) && pid > 0) {
51
- process.kill(pid, 0);
52
- log.info({ pid }, 'another `metro` is running; exiting');
53
- process.exit(0);
54
- }
55
- }
56
- catch { /* stale */ }
57
- try {
58
- unlinkSync(lockFile);
59
- }
60
- catch { /* ignore */ }
61
- }
62
- writeFileSync(lockFile, String(process.pid));
63
- process.on('exit', () => { try {
64
- if (readFileSync(lockFile, 'utf8').trim() === String(process.pid))
65
- unlinkSync(lockFile);
66
- }
67
- catch { /* ignore */ } });
68
- }
package/dist/registry.js DELETED
@@ -1,48 +0,0 @@
1
- /** Append-only registry of `(station, user-id, sessions[])` tuples metro has seen. */
2
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { STATE_DIR } from './paths.js';
5
- import { Line } from './stations/index.js';
6
- import { errMsg, log } from './log.js';
7
- const REGISTRY_FILE = join(STATE_DIR, 'user-registry.json');
8
- function readRegistry() {
9
- if (!existsSync(REGISTRY_FILE))
10
- return {};
11
- try {
12
- return JSON.parse(readFileSync(REGISTRY_FILE, 'utf8'));
13
- }
14
- catch (err) {
15
- log.warn({ err: errMsg(err) }, 'user-registry: malformed, resetting');
16
- return {};
17
- }
18
- }
19
- function record(station, userId, sessionId) {
20
- const reg = readRegistry();
21
- const rows = (reg[station] ??= []);
22
- let row = rows.find(r => r.userId === userId);
23
- if (!row) {
24
- row = { userId, sessions: [], lastSeen: '' };
25
- rows.push(row);
26
- }
27
- if (sessionId && !row.sessions.includes(sessionId))
28
- row.sessions.push(sessionId);
29
- row.lastSeen = new Date().toISOString();
30
- try {
31
- writeFileSync(REGISTRY_FILE, JSON.stringify(reg, null, 2));
32
- }
33
- catch (err) {
34
- log.warn({ err: errMsg(err) }, 'user-registry: write failed');
35
- }
36
- }
37
- /** Scan a line URI for `(station, userId, sessionId)` and record it. No-op on non-user or participant URIs. */
38
- export function noteUserFromLine(line) {
39
- const station = Line.station(line);
40
- if (station !== 'claude' && station !== 'codex')
41
- return;
42
- const p = station === 'claude' ? Line.parseClaude(line) : Line.parseCodex(line);
43
- if (p)
44
- record(station, p.userId, p.sessionId);
45
- }
46
- export function listUsers(station) {
47
- return readRegistry()[station] ?? [];
48
- }
@@ -1,45 +0,0 @@
1
- /** Resolve the Claude Code user identity (account id + session id). */
2
- import { execFileSync } from 'node:child_process';
3
- /** Short TTL so account switches via `claude auth login` propagate to the daemon within seconds. */
4
- const TTL_MS = 5_000;
5
- let cache = null;
6
- /** Stable per-Anthropic-account UUID. Same across devices for the same login. */
7
- export function claudeAccountId() {
8
- if (cache && Date.now() - cache.at < TTL_MS)
9
- return cache.id;
10
- let raw;
11
- try {
12
- raw = execFileSync('claude', ['auth', 'status', '--json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
13
- }
14
- catch (e) {
15
- throw new Error(`metro: failed to run 'claude auth status --json' — is Claude Code installed and on PATH? (${e.message})`);
16
- }
17
- let parsed;
18
- try {
19
- parsed = JSON.parse(raw);
20
- }
21
- catch {
22
- throw new Error(`metro: 'claude auth status --json' returned non-JSON: ${raw.slice(0, 200)}`);
23
- }
24
- if (!parsed.loggedIn || !parsed.orgId) {
25
- throw new Error('metro: Claude Code is not logged in — run \'claude auth login\'');
26
- }
27
- cache = { id: parsed.orgId, at: Date.now() };
28
- return parsed.orgId;
29
- }
30
- export function tryClaudeAccountId() {
31
- try {
32
- return claudeAccountId();
33
- }
34
- catch {
35
- return null;
36
- }
37
- }
38
- /** User-id for the line URI: `METRO_USER_ID` override, else the account id. */
39
- export function claudeUserId() {
40
- return process.env.METRO_USER_ID || claudeAccountId();
41
- }
42
- /** Session: `CLAUDE_CODE_SESSION_ID` (stable across `--resume`). Override: `METRO_USER_SESSION_ID`. */
43
- export function claudeSessionId() {
44
- return process.env.METRO_USER_SESSION_ID || process.env.CLAUDE_CODE_SESSION_ID || null;
45
- }
@@ -1,68 +0,0 @@
1
- /** Resolve the Codex user identity (account id + session id). */
2
- import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
3
- import { homedir } from 'node:os';
4
- import { dirname, join } from 'node:path';
5
- import { STATE_DIR } from '../paths.js';
6
- /** Short TTL so account switches via `codex login` propagate to the daemon within seconds. */
7
- const TTL_MS = 5_000;
8
- let cache = null;
9
- function authPath() {
10
- return join(process.env.CODEX_HOME || join(homedir(), '.codex'), 'auth.json');
11
- }
12
- export function codexAccountId() {
13
- if (cache && Date.now() - cache.at < TTL_MS)
14
- return cache.id;
15
- const path = authPath();
16
- let raw;
17
- try {
18
- raw = readFileSync(path, 'utf8');
19
- }
20
- catch (e) {
21
- throw new Error(`metro: failed to read ${path} — is Codex logged in? (${e.message})`);
22
- }
23
- let parsed;
24
- try {
25
- parsed = JSON.parse(raw);
26
- }
27
- catch {
28
- throw new Error(`metro: ${path} is not valid JSON`);
29
- }
30
- const id = parsed.tokens?.account_id;
31
- if (!id) {
32
- throw new Error(`metro: no Codex account_id in ${path} (auth_mode=${parsed.auth_mode ?? 'unknown'}) — sign in with 'codex login' (ChatGPT mode required)`);
33
- }
34
- cache = { id, at: Date.now() };
35
- return id;
36
- }
37
- export function tryCodexAccountId() {
38
- try {
39
- return codexAccountId();
40
- }
41
- catch {
42
- return null;
43
- }
44
- }
45
- /** User-id for the line URI: `METRO_USER_ID` override, else the account id. */
46
- export function codexUserId() {
47
- return process.env.METRO_USER_ID || codexAccountId();
48
- }
49
- const SESSION_FILE = join(STATE_DIR, 'stations', 'codex', 'session-id');
50
- /** Session: codex-rc thread id (daemon persists; CLIs read state file). Override: `METRO_USER_SESSION_ID`. */
51
- export function codexSessionId() {
52
- if (process.env.METRO_USER_SESSION_ID)
53
- return process.env.METRO_USER_SESSION_ID;
54
- try {
55
- return readFileSync(SESSION_FILE, 'utf8').trim() || null;
56
- }
57
- catch {
58
- return null;
59
- }
60
- }
61
- /** Daemon-side: persist the rc thread id so CLI processes can read it. Best-effort. */
62
- export function setCodexSessionId(threadId) {
63
- try {
64
- mkdirSync(dirname(SESSION_FILE), { recursive: true });
65
- writeFileSync(SESSION_FILE, threadId ?? '');
66
- }
67
- catch { /* CLI just won't have a session segment */ }
68
- }
@@ -1,216 +0,0 @@
1
- /** Discord station: receive via discord.js gateway; send/edit/react/download/fetch via REST. */
2
- import { writeFile } from 'node:fs/promises';
3
- import { join } from 'node:path';
4
- import { Client, Events, GatewayIntentBits, Partials } from 'discord.js';
5
- import { readFile } from 'node:fs/promises';
6
- import { basename } from 'node:path';
7
- import { errMsg, log } from '../log.js';
8
- import { mintId } from '../history.js';
9
- import { Line, } from './index.js';
10
- const API_BASE = 'https://discord.com/api/v10';
11
- const SUPPRESS_EMBEDS = 1 << 2;
12
- const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
13
- const token = () => {
14
- const t = process.env.DISCORD_BOT_TOKEN;
15
- if (!t)
16
- throw new Error('DISCORD_BOT_TOKEN is not set');
17
- return t;
18
- };
19
- async function rest(method, path, body, timeoutMs = 30_000, retriesLeft = 2) {
20
- const res = await fetch(`${API_BASE}${path}`, {
21
- method,
22
- headers: {
23
- 'Authorization': `Bot ${token()}`,
24
- 'User-Agent': 'metro (https://github.com/bonustrack/metro, dev)',
25
- ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
26
- },
27
- body: body !== undefined ? JSON.stringify(body) : undefined,
28
- signal: AbortSignal.timeout(timeoutMs),
29
- });
30
- if (res.status === 429 && retriesLeft > 0) {
31
- const retryAfter = Number(res.headers.get('retry-after')) || 1;
32
- log.debug({ path, retryAfter }, 'discord 429; backing off');
33
- await new Promise(r => setTimeout(r, Math.max(retryAfter * 1000, 250)));
34
- return rest(method, path, body, timeoutMs, retriesLeft - 1);
35
- }
36
- if (!res.ok) {
37
- const text = await res.text().catch(() => '');
38
- throw new Error(`discord ${method} ${path}: ${res.status} ${text}`);
39
- }
40
- return res.status === 204 ? undefined : (await res.json());
41
- }
42
- /** Multipart upload (payload_json + files[N]). Same retry semantics as `rest`. */
43
- async function restMultipart(method, path, payload, files) {
44
- const form = new FormData();
45
- form.append('payload_json', JSON.stringify(payload));
46
- for (const [i, f] of files.entries()) {
47
- form.append(`files[${i}]`, new Blob([new Uint8Array(f.data)]), basename(f.path));
48
- }
49
- const res = await fetch(`${API_BASE}${path}`, {
50
- method,
51
- headers: {
52
- 'Authorization': `Bot ${token()}`,
53
- 'User-Agent': 'metro (https://github.com/bonustrack/metro, dev)',
54
- },
55
- body: form, signal: AbortSignal.timeout(60_000),
56
- });
57
- if (!res.ok) {
58
- const t = await res.text().catch(() => '');
59
- throw new Error(`discord ${method} ${path}: ${res.status} ${t}`);
60
- }
61
- return (await res.json());
62
- }
63
- const collectFiles = async (opts) => {
64
- const paths = [...(opts?.images ?? []), ...(opts?.documents ?? []), ...(opts?.voice ? [opts.voice] : [])];
65
- return Promise.all(paths.map(async (p) => ({ path: p, data: await readFile(p) })));
66
- };
67
- /** Convert button rows to Discord component arrays (URL buttons only — style=5). */
68
- const discordButtons = (rows) => rows.map(row => ({
69
- type: 1, components: row.map(b => ({ type: 2, style: 5, label: b.text, url: b.url })),
70
- }));
71
- const channelOf = (line) => {
72
- const id = Line.parseDiscord(line);
73
- if (!id)
74
- throw new Error(`not a discord line: ${line}`);
75
- return id;
76
- };
77
- export class DiscordStation {
78
- name = 'discord';
79
- client = null;
80
- messageHandler = () => { };
81
- reactionHandler = () => { };
82
- onMessage(handler) {
83
- this.messageHandler = handler;
84
- }
85
- onReaction(handler) { this.reactionHandler = handler; }
86
- getClient() {
87
- return this.client ??= new Client({
88
- intents: [
89
- GatewayIntentBits.DirectMessages, GatewayIntentBits.Guilds,
90
- GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent,
91
- GatewayIntentBits.GuildMessageReactions, GatewayIntentBits.DirectMessageReactions,
92
- ],
93
- partials: [Partials.Channel, Partials.Message, Partials.Reaction],
94
- });
95
- }
96
- async start() {
97
- const c = this.getClient();
98
- c.on(Events.MessageCreate, m => { void this.handleMessage(m); });
99
- c.on(Events.MessageReactionAdd, (r, u) => { void this.handleReaction(r, u); });
100
- c.on(Events.Error, err => log.error({ err: errMsg(err) }, 'discord error'));
101
- await c.login(process.env.DISCORD_BOT_TOKEN);
102
- await new Promise(r => c.once(Events.ClientReady, () => r()));
103
- }
104
- async stop() {
105
- if (!this.client)
106
- return;
107
- await this.client.destroy();
108
- this.client = null;
109
- }
110
- async getMe() {
111
- return rest('GET', '/users/@me');
112
- }
113
- async send(line, text, opts) {
114
- const payload = { content: text, flags: SUPPRESS_EMBEDS };
115
- if (opts?.replyTo)
116
- payload.message_reference = { message_id: opts.replyTo };
117
- if (opts?.buttons?.length)
118
- payload.components = discordButtons(opts.buttons);
119
- const path = `/channels/${channelOf(line)}/messages`;
120
- const files = await collectFiles(opts);
121
- const sent = files.length
122
- ? await restMultipart('POST', path, payload, files)
123
- : await rest('POST', path, payload);
124
- return sent.id;
125
- }
126
- async edit(line, messageId, text, opts) {
127
- const payload = { content: text, flags: SUPPRESS_EMBEDS };
128
- payload.components = opts?.buttons?.length ? discordButtons(opts.buttons) : [];
129
- await rest('PATCH', `/channels/${channelOf(line)}/messages/${messageId}`, payload);
130
- }
131
- async react(line, messageId, emoji) {
132
- const ch = channelOf(line);
133
- if (!emoji) {
134
- await rest('DELETE', `/channels/${ch}/messages/${messageId}/reactions/@me`);
135
- return;
136
- }
137
- await rest('PUT', `/channels/${ch}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}/@me`);
138
- }
139
- async download(line, messageId, outDir) {
140
- const ch = channelOf(line);
141
- const msg = await rest('GET', `/channels/${ch}/messages/${messageId}`);
142
- const out = [];
143
- for (const [i, a] of (msg.attachments ?? []).entries()) {
144
- if (!a.content_type?.startsWith('image/'))
145
- continue;
146
- if (a.size > MAX_ATTACHMENT_BYTES) {
147
- log.warn({ size: a.size, name: a.filename }, 'discord: attachment too large; skipped');
148
- continue;
149
- }
150
- try {
151
- const res = await fetch(a.url, { signal: AbortSignal.timeout(30_000) });
152
- if (!res.ok)
153
- throw new Error(`status ${res.status}`);
154
- const buf = Buffer.from(await res.arrayBuffer());
155
- const path = join(outDir, `${messageId}-${i}-${a.filename}`);
156
- await writeFile(path, buf);
157
- out.push({ path, mediaType: a.content_type });
158
- }
159
- catch (err) {
160
- log.warn({ err: errMsg(err), url: a.url }, 'discord: attachment fetch failed');
161
- }
162
- }
163
- return out;
164
- }
165
- async fetch(line, limit) {
166
- const capped = Math.max(1, Math.min(100, limit | 0));
167
- const msgs = await rest('GET', `/channels/${channelOf(line)}/messages?limit=${capped}`);
168
- return [...msgs].reverse().map(m => ({
169
- messageId: m.id, author: m.author.username, text: m.content, timestamp: m.timestamp,
170
- }));
171
- }
172
- async handleReaction(r, u) {
173
- if (u.bot)
174
- return;
175
- const emoji = r.emoji.name;
176
- if (!emoji)
177
- return;
178
- const channelId = r.message.channelId;
179
- const messageId = r.message.id;
180
- const username = 'username' in u && u.username ? u.username : undefined;
181
- log.info({ from: username, channel: channelId, emoji, messageId }, 'discord: reaction');
182
- this.reactionHandler({
183
- id: mintId(), ts: new Date().toISOString(),
184
- station: 'discord', line: Line.discord(channelId),
185
- from: Line.user('discord', u.id), fromName: username,
186
- messageId, emoji, isPrivate: r.message.guildId === null,
187
- });
188
- }
189
- async handleMessage(m) {
190
- if (m.author.bot)
191
- return;
192
- const tags = [...m.attachments.values()].map(a => a.contentType?.startsWith('image/') ? '[image]'
193
- : a.contentType?.startsWith('audio/') ? `[audio: ${a.name}]` : `[file: ${a.name}]`);
194
- const text = [m.content.trim(), ...tags].filter(Boolean).join(' ');
195
- if (!text)
196
- return;
197
- log.info({ from: m.author.username, channel: m.channelId, text: text.slice(0, 80) }, 'discord: inbound');
198
- const lineName = m.channel && 'name' in m.channel
199
- ? m.channel.name ?? undefined : undefined;
200
- const payload = m.toJSON();
201
- if (m.reference?.messageId) {
202
- try {
203
- payload.referencedMessage = (await m.fetchReference()).toJSON();
204
- }
205
- catch (err) {
206
- log.debug({ err: errMsg(err) }, 'discord: fetchReference failed');
207
- }
208
- }
209
- this.messageHandler({
210
- id: mintId(), ts: new Date(m.createdTimestamp).toISOString(),
211
- station: 'discord', line: Line.discord(m.channelId), lineName,
212
- from: Line.user('discord', m.author.id), fromName: m.author.username,
213
- messageId: m.id, text, payload, isPrivate: m.guildId === null,
214
- });
215
- }
216
- }