@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,40 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+
6
+ const agentsDir = () => process.env.METRO_AGENTS_DIR || join(homedir(), '.metro', 'agents');
7
+
8
+ function readAgent(path) {
9
+ try {
10
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
11
+ return typeof parsed?.key === 'string' ? parsed : null;
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
16
+
17
+ function pickAgent() {
18
+ const dir = agentsDir();
19
+ const fixed = readAgent(join(dir, 'agent.json'));
20
+ if (fixed !== null) return fixed;
21
+ if (!existsSync(dir)) throw new Error(`no agent on this machine yet (${dir})`);
22
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
23
+ if (!entry.isDirectory()) continue;
24
+ const found = readAgent(join(dir, entry.name, 'agent.json'));
25
+ if (found !== null) return found;
26
+ }
27
+ throw new Error(`no agent on this machine yet (${dir})`);
28
+ }
29
+
30
+ if (process.argv[2] !== 'headers') {
31
+ process.stderr.write('usage: metro-plugin.mjs headers\n');
32
+ process.exit(1);
33
+ }
34
+
35
+ try {
36
+ process.stdout.write(`${JSON.stringify({ Authorization: `Bearer ${pickAgent().key}` })}\n`);
37
+ } catch (err) {
38
+ process.stderr.write(`metro: ${err.message}\n`);
39
+ process.stdout.write('{}\n');
40
+ }
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const given = (value) => (typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined);
8
+ const claudeDir = given(process.env.CLAUDE_CONFIG_DIR) ?? join(given(process.env.HOME) ?? homedir(), '.claude');
9
+ const SKILL = join(claudeDir, 'skills', 'metro-orchestrator', 'SKILL.md');
10
+ const FALLBACK = join(dirname(dirname(fileURLToPath(import.meta.url))), 'orchestrator.md');
11
+
12
+ const body = (text) => text.replace(/^---[\s\S]*?\n---\n/, '').trim();
13
+
14
+ function guidance() {
15
+ for (const path of [SKILL, FALLBACK]) {
16
+ if (!existsSync(path)) continue;
17
+ try {
18
+ const text = body(readFileSync(path, 'utf8'));
19
+ if (text !== '') return text;
20
+ } catch {
21
+ continue;
22
+ }
23
+ }
24
+ return '';
25
+ }
26
+
27
+ const text = guidance();
28
+ if (text !== '')
29
+ process.stdout.write(
30
+ `${JSON.stringify({ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: `This machine runs as a metro agent. Standing rules, kept in the metro-orchestrator skill:\n\n${text}` } })}\n`,
31
+ );
@@ -0,0 +1,25 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/bin/guard.mjs\"",
9
+ "statusMessage": "Checking the orchestrator boundary"
10
+ }
11
+ ]
12
+ }
13
+ ],
14
+ "SessionStart": [
15
+ {
16
+ "hooks": [
17
+ {
18
+ "type": "command",
19
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/bin/session-start.mjs\""
20
+ }
21
+ ]
22
+ }
23
+ ]
24
+ }
25
+ }
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: metro-orchestrator
3
+ description: How this box's agent works on metro. Acknowledge, delegate, relay, answer over chat, never block on the terminal. Loaded at every session start by the metro plugin; edit it here to change the rules on this box.
4
+ ---
5
+
6
+ You are the main thread of a Claude Code session that is connected to chat through metro, on a machine nobody watches. The main thread is an orchestrator: it may only delegate and talk. A guard in the metro plugin denies every other tool on this thread, so this is the shape of the work, not a preference.
7
+
8
+ **Inbound.** A message arrives as a `<channel source="metro" …>` block. Read `addressed` first: `direct`, `mention` or `reply` means it is for you; without it the message merely happened in a room you watch, so treat it as context and stay silent unless the person plainly wants you. For a message that is for you, the first tool call is `react` on it (👀, then 👍 or ✅ once done) so the person knows you saw it, and any reply goes on the same `line`, passed verbatim.
9
+
10
+ **Delegate everything that is work.** Reading files, running commands, searching, fetching pages, editing, and every connector tool: hand it to a subagent with the `Agent` tool, `subagent_type: "worker"`, always `run_in_background: true`, then keep answering chat while it runs and relay the result when the task notification arrives. A foreground subagent blinds you to chat and the guard refuses it. You may read image files yourself, so a screenshot someone sends is yours to look at.
11
+
12
+ **Never wait on the terminal.** `AskUserQuestion`, plan mode and anything that blocks for a person at the keyboard are denied everywhere, main thread and subagents alike. When something is ambiguous, do what does not depend on it, choose the most reasonable reading, say so in your reply, and if a real decision is needed ask over chat with `send` and carry on. Tool-approval prompts reach the chat too; the person answers `yes <id>` or `no <id>` there.
13
+
14
+ **Report like a relay.** A worker's report is the only evidence you have, so pass on what it verified and say plainly what it did not. Short messages on chat: lead with the outcome, one idea per sentence, no tool names, no file paths unless the person needs them. Send a file out with `create_upload` (the curl line it hands back runs in a worker) and then `send` with the upload id.
15
+
16
+ **Keep going.** Long tasks run as background agents and loops; check in over chat when something lands rather than going quiet. The session is restarted by the daemon if it exits, so an interrupted task is resumed from chat, not from memory.
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@metro-labs/core",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "What every metro process shares: the logger, the event bus and its types, ids, secure file writes, the train protocol, station types and attachment handling. No HTTP, no policy.",
6
+ "type": "module",
7
+ "exports": {
8
+ "./log": "./src/log.ts",
9
+ "./events": "./src/events.ts",
10
+ "./identity": "./src/identity.ts",
11
+ "./is-record": "./src/is-record.ts",
12
+ "./secure-fs": "./src/secure-fs.ts",
13
+ "./version": "./src/version.ts",
14
+ "./train-error": "./src/train-error.ts",
15
+ "./trains/protocol": "./src/protocol.ts",
16
+ "./ids": "./src/ids.ts",
17
+ "./address": "./src/address.ts",
18
+ "./station-names": "./src/station-names.ts",
19
+ "./str": "./src/str.ts",
20
+ "./lines": "./src/lines.ts",
21
+ "./endpoints": "./src/endpoints.ts",
22
+ "./stations/types": "./src/stations/types.ts",
23
+ "./stations/station-runtime": "./src/stations/station-runtime.ts",
24
+ "./stations/account-store": "./src/stations/account-store.ts",
25
+ "./stations/attachments": "./src/stations/attachments.ts",
26
+ "./stations/messaging-normalize": "./src/stations/messaging-normalize.ts"
27
+ },
28
+ "scripts": {
29
+ "build": "tsc",
30
+ "test": "tsc --noEmit && METRO_STATE_DIR=\"$(mktemp -d /tmp/metro-test.XXXXXX)\" bun test test/"
31
+ },
32
+ "dependencies": {
33
+ "pino": "^9.5.0",
34
+ "pino-pretty": "^13.1.3"
35
+ },
36
+ "devDependencies": {
37
+ "@stage-labs/config": "0.1.0-beta.2",
38
+ "eslint": "^10.3.0",
39
+ "knip": "^6.15.0",
40
+ "madge": "^8.0.0",
41
+ "typescript": "^5"
42
+ }
43
+ }
@@ -0,0 +1,6 @@
1
+ const ADDRESS_RE = /^0x[0-9a-f]{40}$/;
2
+
3
+ export function normalizeAddress(raw: string): string | null {
4
+ const address = raw.trim().toLowerCase();
5
+ return ADDRESS_RE.test(address) ? address : null;
6
+ }
@@ -0,0 +1,7 @@
1
+ export interface Endpoint {
2
+ id: string;
3
+ webhookId?: string;
4
+ label: string;
5
+ secret?: string;
6
+ createdAt: string;
7
+ }
@@ -0,0 +1,120 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { Line } from './lines.js';
3
+
4
+ export { userSelf, daemonSelf } from './identity.js';
5
+
6
+ export type StructuredEvent =
7
+ | { type: 'msg' }
8
+ | { type: 'react'; emoji?: string; targetId?: string }
9
+ | { type: 'edit'; targetId?: string }
10
+ | { type: 'delete'; targetId?: string }
11
+ | { type: 'reply'; replyTo?: string }
12
+ | { type: 'system'; source?: string; eventName?: string };
13
+
14
+ export interface MetroEvent {
15
+ id: string;
16
+ ts: string;
17
+ station: string;
18
+ line: Line;
19
+ lineName?: string;
20
+ from: Line;
21
+ fromName?: string;
22
+ fromDisplayName?: string;
23
+ to: Line;
24
+ text?: string;
25
+ messageId?: string;
26
+ replyTo?: string;
27
+ isPrivate?: boolean;
28
+ mentionsSelf?: boolean;
29
+ replyToSelf?: boolean;
30
+ payload?: unknown;
31
+ display?: string;
32
+ event?: StructuredEvent;
33
+ agent?: string;
34
+ seq?: number;
35
+ }
36
+
37
+ function isExternalWebhook(e: MetroEvent): boolean {
38
+ return e.station === 'webhook' && !Line.isLocal(e.from);
39
+ }
40
+
41
+ function webhookEventName(e: MetroEvent): string | undefined {
42
+ const headers = (e.payload as { headers?: Record<string, string> } | undefined)
43
+ ?.headers;
44
+ return headers?.['x-github-event'] ?? headers?.['x-intercom-topic'];
45
+ }
46
+
47
+ export function classifyEvent(e: MetroEvent): StructuredEvent {
48
+ if (isExternalWebhook(e)) {
49
+ return {
50
+ type: 'system',
51
+ source: 'webhook',
52
+ eventName: webhookEventName(e),
53
+ };
54
+ }
55
+ const emoji =
56
+ (e.payload as { emoji?: string } | undefined)?.emoji ??
57
+ e.text?.match(/^\[react (.+)\]$/)?.[1];
58
+ if (emoji) return { type: 'react', emoji, targetId: e.replyTo };
59
+ if (e.replyTo) return { type: 'reply', replyTo: e.replyTo };
60
+ return { type: 'msg' };
61
+ }
62
+
63
+ export function formatDisplay(e: MetroEvent): string {
64
+ const headerFor = (icon: string, parts: (string | undefined)[]): string =>
65
+ `**${icon} ${parts.filter(Boolean).join(' · ')}**`;
66
+ const body = e.text ?? '';
67
+ if (isExternalWebhook(e)) {
68
+ const ev = webhookEventName(e);
69
+ return `${headerFor('🪝', ['webhook', e.lineName, ev])}\n> ${body}`;
70
+ }
71
+ if (Line.isLocal(e.from)) {
72
+ return `${headerFor('📤', [e.station, '→', e.fromName ?? e.to])}\n> ${body}`;
73
+ }
74
+ return `${headerFor('📩', [e.station, e.fromName ?? e.from, e.lineName])}\n> ${body}`;
75
+ }
76
+
77
+ export const mintId = (): string =>
78
+ `msg_${randomBytes(6).toString('base64url')}`;
79
+
80
+ export type BusListener = (event: MetroEvent, busSeq: number) => void;
81
+
82
+ export interface BufferedEvent {
83
+ busSeq: number;
84
+ event: MetroEvent;
85
+ }
86
+
87
+ const BUS_BUFFER_MAX = 500;
88
+
89
+ const listeners = new Set<BusListener>();
90
+ const buffer: BufferedEvent[] = [];
91
+ let busSeq = 0;
92
+
93
+ export function publishEvent(event: MetroEvent): void {
94
+ busSeq += 1;
95
+ const seq = busSeq;
96
+ buffer.push({ busSeq: seq, event });
97
+ if (buffer.length > BUS_BUFFER_MAX) buffer.shift();
98
+ for (const fn of [...listeners]) {
99
+ try {
100
+ fn(event, seq);
101
+ } catch (err) {
102
+ console.error('[metro-bus] listener threw for busSeq', seq, err);
103
+ }
104
+ }
105
+ }
106
+
107
+ export function currentBusSeq(): number {
108
+ return busSeq;
109
+ }
110
+
111
+ export function bufferedSince(afterBusSeq: number): BufferedEvent[] {
112
+ return buffer.filter((b) => b.busSeq > afterBusSeq);
113
+ }
114
+
115
+ export function subscribeEvents(fn: BusListener): () => void {
116
+ listeners.add(fn);
117
+ return () => {
118
+ listeners.delete(fn);
119
+ };
120
+ }
@@ -0,0 +1,7 @@
1
+ import { Line } from './lines.js';
2
+
3
+ export const userSelf = (): Line =>
4
+ (process.env.METRO_FROM ?? 'metro://user') as Line;
5
+
6
+ export const daemonSelf = (): Line =>
7
+ (process.env.METRO_FROM ?? process.env.METRO_SELF_URI ?? 'metro://user') as Line;
@@ -0,0 +1,21 @@
1
+ import { randomBytes } from 'node:crypto';
2
+
3
+ const ID_BYTES = 8;
4
+ const REROLLS = 16;
5
+
6
+ export const ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{10}$/;
7
+
8
+ export function newId(): string {
9
+ for (let attempt = 0; attempt < REROLLS; attempt += 1) {
10
+ const id = randomBytes(ID_BYTES).toString('base64url');
11
+ if (ID_RE.test(id)) return id;
12
+ }
13
+ throw new Error('could not generate an id');
14
+ }
15
+
16
+ export function parseId(raw: unknown): string | null {
17
+ if (typeof raw !== 'string') return null;
18
+ return ID_RE.test(raw) ? raw : null;
19
+ }
20
+
21
+ export const AGENT_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{1,31}$/;
@@ -0,0 +1,2 @@
1
+ export const isRecord = (v: unknown): v is Record<string, unknown> =>
2
+ typeof v === 'object' && v !== null && !Array.isArray(v);
@@ -0,0 +1,87 @@
1
+ export type Line = string & { readonly __line: unique symbol };
2
+ export const asLine = (s: string): Line => s as Line;
3
+
4
+ const PREFIX = 'metro://';
5
+ const build = (station: string, ...seg: (string | number)[]): Line =>
6
+ asLine(`${PREFIX}${station}/${seg.map(String).join('/')}`);
7
+
8
+ function parseAccountScoped(
9
+ line: Line | string,
10
+ station: string,
11
+ validate?: (resource: string) => boolean,
12
+ ): { accountId: string; resource: string } | null {
13
+ const p = Line.parse(line);
14
+ if (p?.station !== station) return null;
15
+ let accountId: string, resource: string;
16
+ if (p.path.length === 2 && p.path[0] !== undefined && p.path[1] !== undefined) {
17
+ accountId = p.path[0];
18
+ resource = p.path[1];
19
+ } else if (p.path.length === 1 && p.path[0] !== undefined) {
20
+ accountId = 'default';
21
+ resource = p.path[0];
22
+ } else return null;
23
+ if (validate && !validate(resource)) return null;
24
+ return { accountId, resource };
25
+ }
26
+
27
+ const isSnowflake = (s: string): boolean => /^\d+$/.test(s);
28
+ const isThreemaId = (s: string): boolean =>
29
+ /^(?:[A-Z0-9]{8}|\*[A-Z0-9]{7})$/i.test(s);
30
+ const isSignedInt = (s: string): boolean => /^-?\d+$/.test(s);
31
+
32
+ function splitTelegramAccount(path: string[]): { accountId: string; rest: string[] } {
33
+ const first = path[0];
34
+ if (path.length >= 2 && first !== undefined && !isSignedInt(first))
35
+ return { accountId: first, rest: path.slice(1) };
36
+ return { accountId: 'default', rest: path };
37
+ }
38
+
39
+ function parseTelegramLine(
40
+ line: Line | string,
41
+ ): { accountId: string; chatId: number; topicId?: number } | null {
42
+ const p = Line.parse(line);
43
+ if (p?.station !== 'telegram-bot') return null;
44
+ const { accountId, rest } = splitTelegramAccount(p.path);
45
+ const [chatId, topicId] = rest;
46
+ if (rest.length < 1 || rest.length > 2 || chatId === undefined || !isSignedInt(chatId))
47
+ return null;
48
+ if (topicId !== undefined && !isSnowflake(topicId)) return null;
49
+ return {
50
+ accountId,
51
+ chatId: Number(chatId),
52
+ ...(topicId !== undefined ? { topicId: Number(topicId) } : {}),
53
+ };
54
+ }
55
+
56
+ export const Line = {
57
+ claude: (orgId: string, sessionId: string): Line =>
58
+ build('claude', orgId, sessionId),
59
+ webhook: (endpointId: string): Line => build('webhook', endpointId),
60
+ user: (station: string, id: string | number): Line =>
61
+ build(station, 'user', id),
62
+
63
+ parse(line: Line | string): { station: string; path: string[] } | null {
64
+ if (!line.startsWith(PREFIX)) return null;
65
+ const rest = line.slice(PREFIX.length);
66
+ const slash = rest.indexOf('/');
67
+ if (slash <= 0) return null;
68
+ const path = rest
69
+ .slice(slash + 1)
70
+ .split('/')
71
+ .filter(Boolean);
72
+ return path.length ? { station: rest.slice(0, slash), path } : null;
73
+ },
74
+ station: (line: Line | string): string | null =>
75
+ Line.parse(line)?.station ?? null,
76
+ isLocal: (line: Line | string): boolean => Line.station(line) === 'claude',
77
+
78
+ parseXmtp: (line: Line | string) => parseAccountScoped(line, 'xmtp'),
79
+
80
+ parseDiscord: (line: Line | string) =>
81
+ parseAccountScoped(line, 'discord-bot', isSnowflake),
82
+
83
+ parseTelegram: (line: Line | string) => parseTelegramLine(line),
84
+
85
+ parseThreema: (line: Line | string) =>
86
+ parseAccountScoped(line, 'threema', isThreemaId),
87
+ };
@@ -0,0 +1,42 @@
1
+ import { writeSync } from 'node:fs';
2
+ import pino from 'pino';
3
+ import pinoPretty from 'pino-pretty';
4
+
5
+ const stream = process.stderr.isTTY
6
+ ? pinoPretty({
7
+ colorize: true,
8
+ translateTime: 'HH:MM:ss',
9
+ ignore: 'pid,hostname,name',
10
+ destination: 2,
11
+ })
12
+ : pino.destination(2);
13
+
14
+ export const log = pino(
15
+ { base: { name: 'metro' }, level: process.env.METRO_LOG_LEVEL ?? 'info' },
16
+ stream,
17
+ );
18
+
19
+ export const logFatalSync = (
20
+ fields: Record<string, unknown>,
21
+ msg: string,
22
+ ): void => {
23
+ const line = JSON.stringify({
24
+ level: 60,
25
+ time: Date.now(),
26
+ name: 'metro',
27
+ ...fields,
28
+ msg,
29
+ });
30
+ try {
31
+ writeSync(2, `${line}\n`);
32
+ } catch {
33
+ return;
34
+ }
35
+ };
36
+
37
+ export const errMsg = (err: unknown): string => {
38
+ if (err instanceof Error) return err.message;
39
+ if (err && typeof err === 'object' && 'message' in err)
40
+ return String(err.message);
41
+ return String(err);
42
+ };
@@ -0,0 +1,226 @@
1
+ import { readdirSync, statSync } from 'node:fs';
2
+ import { join, parse as parsePath } from 'node:path';
3
+ import { errMsg, log } from './log.js';
4
+ import { coerceErrorInfo, type TrainErrorInfo } from './train-error.js';
5
+ import type { StructuredEvent } from './events.js';
6
+ import type { CallMsg } from './stations/station-runtime.js';
7
+
8
+ export {
9
+ TrainError,
10
+ serializeTrainError,
11
+ type TrainErrorInfo,
12
+ } from './train-error.js';
13
+
14
+ const CALL_TIMEOUT_MS = 60_000;
15
+
16
+ export interface Pending {
17
+ resolve: (r: TrainCallResponse) => void;
18
+ reject: (err: Error) => void;
19
+ timer: ReturnType<typeof setTimeout>;
20
+ }
21
+ export interface CallTarget {
22
+ name: string;
23
+ pending: Map<string, Pending>;
24
+ proc: ({ stdin?: unknown } & Record<string, unknown>) | null;
25
+ }
26
+
27
+ const STDOUT_LINE_MAX = 4 * 1024 * 1024;
28
+
29
+ export type TrainEvent = {
30
+ station?: string;
31
+ line?: string;
32
+ line_name?: string;
33
+ from?: string;
34
+ from_name?: string;
35
+ from_display_name?: string;
36
+ to?: string;
37
+ message_id?: string;
38
+ reply_to?: string;
39
+ is_private?: boolean;
40
+ mentions_self?: boolean;
41
+ reply_to_self?: boolean;
42
+ text?: string;
43
+ emoji?: string;
44
+ payload?: unknown;
45
+ ts?: string;
46
+ id?: string;
47
+ event?: StructuredEvent;
48
+ } & Record<string, unknown>;
49
+
50
+ export interface TrainCallResponse {
51
+ result?: unknown;
52
+ error?: string;
53
+ errorInfo?: TrainErrorInfo;
54
+ }
55
+
56
+ export type TrainMessage =
57
+ | {
58
+ op: 'response';
59
+ id: string;
60
+ result?: unknown;
61
+ error?: string;
62
+ errorInfo?: TrainErrorInfo;
63
+ }
64
+ | { op: 'log'; text?: string }
65
+ | { op: 'event'; event: TrainEvent }
66
+ | { op: 'ignore' };
67
+
68
+ export function parseTrainLine(
69
+ name: string,
70
+ line: string,
71
+ ): TrainMessage | null {
72
+ let msg: {
73
+ op?: string;
74
+ id?: string;
75
+ result?: unknown;
76
+ error?: string;
77
+ errorInfo?: unknown;
78
+ text?: string;
79
+ } & Record<string, unknown>;
80
+ try {
81
+ msg = JSON.parse(line) as typeof msg;
82
+ } catch (err) {
83
+ log.warn(
84
+ { name, err: errMsg(err), line: line.slice(0, 200) },
85
+ 'train: bad JSON',
86
+ );
87
+ return null;
88
+ }
89
+ if (msg.op === 'response') {
90
+ if (typeof msg.id !== 'string') return { op: 'ignore' };
91
+ return {
92
+ op: 'response',
93
+ id: msg.id,
94
+ result: msg.result,
95
+ error: msg.error,
96
+ errorInfo: coerceErrorInfo(msg.errorInfo),
97
+ };
98
+ }
99
+ if (msg.op === 'log') return { op: 'log', text: msg.text };
100
+ if (typeof msg.line !== 'string') {
101
+ log.warn(
102
+ { name, line: line.slice(0, 200) },
103
+ 'train: event missing `line` (string) — dropped',
104
+ );
105
+ return { op: 'ignore' };
106
+ }
107
+ return { op: 'event', event: msg };
108
+ }
109
+
110
+ export function drainLines(
111
+ name: string,
112
+ buf: string,
113
+ onLine: (line: string) => void,
114
+ ): string {
115
+ if (buf.length > STDOUT_LINE_MAX && !buf.includes('\n')) {
116
+ log.warn(
117
+ { name, bytes: buf.length },
118
+ 'train: dropping oversized stdout line',
119
+ );
120
+ return '';
121
+ }
122
+ let nl;
123
+ while ((nl = buf.indexOf('\n')) !== -1) {
124
+ const line = buf.slice(0, nl).trim();
125
+ buf = buf.slice(nl + 1);
126
+ if (!line) continue;
127
+ onLine(line);
128
+ }
129
+ return buf;
130
+ }
131
+
132
+ export function readCalls(
133
+ name: string,
134
+ handleCall: (msg: CallMsg) => Promise<void>,
135
+ ): void {
136
+ let buf = '';
137
+ process.stdin.setEncoding('utf8');
138
+ process.stdin.on('data', (chunk: Buffer | string) => {
139
+ buf += typeof chunk === 'string' ? chunk : chunk.toString('utf8');
140
+ buf = drainLines(name, buf, (line) => {
141
+ try {
142
+ const msg = JSON.parse(line) as Partial<CallMsg>;
143
+ if (msg.op === 'call')
144
+ handleCall(msg as CallMsg).catch((e: unknown) => {
145
+ process.stderr.write(`call failed: ${errMsg(e)}\n`);
146
+ });
147
+ } catch (err: unknown) {
148
+ process.stderr.write(`bad stdin line: ${errMsg(err)}\n`);
149
+ }
150
+ });
151
+ });
152
+ }
153
+
154
+ function encodeCall(id: string, action: string, args: unknown): string {
155
+ return JSON.stringify({ op: 'call', id, action, args }) + '\n';
156
+ }
157
+
158
+ function isTrainFile(name: string): boolean {
159
+ return (
160
+ /\.(ts|js|mjs)$/.test(name) &&
161
+ !name.startsWith('_') &&
162
+ !name.startsWith('.')
163
+ );
164
+ }
165
+
166
+ export function listTrainFiles(dir: string): { name: string; path: string }[] {
167
+ return readdirSync(dir)
168
+ .filter(isTrainFile)
169
+ .map((f) => ({ name: parsePath(f).name, path: join(dir, f) }))
170
+ .filter((t) => {
171
+ try {
172
+ return statSync(t.path).isFile();
173
+ } catch {
174
+ return false;
175
+ }
176
+ });
177
+ }
178
+
179
+ export function mintCallId(seq: number): string {
180
+ return `req_${seq}_${Math.random().toString(36).slice(2, 8)}`;
181
+ }
182
+
183
+ export function failAllPending(
184
+ pending: Map<string, Pending>,
185
+ reason: string,
186
+ ): void {
187
+ for (const p of pending.values()) {
188
+ clearTimeout(p.timer);
189
+ p.reject(new Error(reason));
190
+ }
191
+ pending.clear();
192
+ }
193
+
194
+ export function sendCall(
195
+ t: CallTarget,
196
+ id: string,
197
+ action: string,
198
+ args: unknown,
199
+ ): Promise<TrainCallResponse> {
200
+ return new Promise<TrainCallResponse>((resolve, reject) => {
201
+ const timer = setTimeout(() => {
202
+ t.pending.delete(id);
203
+ reject(
204
+ new Error(
205
+ `train '${t.name}' call '${action}' timed out after ${CALL_TIMEOUT_MS}ms`,
206
+ ),
207
+ );
208
+ }, CALL_TIMEOUT_MS);
209
+ t.pending.set(id, { resolve, reject, timer });
210
+ try {
211
+ const stdin = (
212
+ t.proc as {
213
+ stdin?: { write: (s: string) => void; flush: () => void };
214
+ } | null
215
+ )?.stdin;
216
+ if (!stdin || typeof stdin === 'number')
217
+ throw new Error('stdin not piped');
218
+ stdin.write(encodeCall(id, action, args));
219
+ stdin.flush();
220
+ } catch (err) {
221
+ clearTimeout(timer);
222
+ t.pending.delete(id);
223
+ reject(new Error(`train '${t.name}' stdin write failed: ${errMsg(err)}`));
224
+ }
225
+ });
226
+ }