@stage-labs/metro 0.1.0-beta.8 → 0.1.0-beta.80

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 (282) hide show
  1. package/dist/api.js +14 -0
  2. package/dist/claude-settings.js +40 -0
  3. package/dist/claude.js +107 -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/node-name.js +71 -0
  9. package/dist/plugin.js +80 -0
  10. package/dist/provider-flags.js +9 -0
  11. package/dist/runtime-install.js +87 -0
  12. package/dist/runtime.js +74 -0
  13. package/dist/serve.js +146 -0
  14. package/dist/service.js +242 -0
  15. package/dist/tail.js +119 -0
  16. package/dist/update.js +45 -0
  17. package/dist/version.js +97 -0
  18. package/package.json +14 -25
  19. package/runtime/node_modules/@metro-labs/core/package.json +43 -0
  20. package/runtime/node_modules/@metro-labs/core/src/address.ts +6 -0
  21. package/runtime/node_modules/@metro-labs/core/src/endpoints.ts +7 -0
  22. package/runtime/node_modules/@metro-labs/core/src/events.ts +120 -0
  23. package/runtime/node_modules/@metro-labs/core/src/identity.ts +7 -0
  24. package/runtime/node_modules/@metro-labs/core/src/ids.ts +21 -0
  25. package/runtime/node_modules/@metro-labs/core/src/is-record.ts +2 -0
  26. package/runtime/node_modules/@metro-labs/core/src/lines.ts +82 -0
  27. package/runtime/node_modules/@metro-labs/core/src/log.ts +42 -0
  28. package/runtime/node_modules/@metro-labs/core/src/protocol.ts +226 -0
  29. package/runtime/node_modules/@metro-labs/core/src/secure-fs.ts +57 -0
  30. package/runtime/node_modules/@metro-labs/core/src/station-names.ts +12 -0
  31. package/runtime/node_modules/@metro-labs/core/src/stations/account-store.ts +60 -0
  32. package/runtime/node_modules/@metro-labs/core/src/stations/attachments.ts +196 -0
  33. package/runtime/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +88 -0
  34. package/runtime/node_modules/@metro-labs/core/src/stations/station-runtime.ts +64 -0
  35. package/runtime/node_modules/@metro-labs/core/src/stations/types.ts +107 -0
  36. package/runtime/node_modules/@metro-labs/core/src/str.ts +5 -0
  37. package/runtime/node_modules/@metro-labs/core/src/train-error.ts +53 -0
  38. package/runtime/node_modules/@metro-labs/core/src/version.ts +2 -0
  39. package/runtime/node_modules/@metro-labs/daemon/package.json +47 -0
  40. package/runtime/node_modules/@metro-labs/daemon/src/agents/account-attach.ts +19 -0
  41. package/runtime/node_modules/@metro-labs/daemon/src/agents/accounts-api.ts +311 -0
  42. package/runtime/node_modules/@metro-labs/daemon/src/agents/admin.ts +43 -0
  43. package/runtime/node_modules/@metro-labs/daemon/src/agents/api.ts +302 -0
  44. package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +111 -0
  45. package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +329 -0
  46. package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +155 -0
  47. package/runtime/node_modules/@metro-labs/daemon/src/agents/keys.ts +45 -0
  48. package/runtime/node_modules/@metro-labs/daemon/src/agents/map.ts +84 -0
  49. package/runtime/node_modules/@metro-labs/daemon/src/agents/scope.ts +64 -0
  50. package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +176 -0
  51. package/runtime/node_modules/@metro-labs/daemon/src/boot/connect-hint.ts +39 -0
  52. package/runtime/node_modules/@metro-labs/daemon/src/boot/crash-guard.ts +49 -0
  53. package/runtime/node_modules/@metro-labs/daemon/src/boot/local-owner.ts +14 -0
  54. package/runtime/node_modules/@metro-labs/daemon/src/boot/paths.ts +106 -0
  55. package/runtime/node_modules/@metro-labs/daemon/src/channels/addressed.ts +25 -0
  56. package/runtime/node_modules/@metro-labs/daemon/src/channels/dedupe.ts +24 -0
  57. package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +377 -0
  58. package/runtime/node_modules/@metro-labs/daemon/src/channels/media-note.ts +85 -0
  59. package/runtime/node_modules/@metro-labs/daemon/src/channels/pending.ts +75 -0
  60. package/runtime/node_modules/@metro-labs/daemon/src/channels/relay.ts +129 -0
  61. package/runtime/node_modules/@metro-labs/daemon/src/channels/webhook-note.ts +51 -0
  62. package/runtime/node_modules/@metro-labs/daemon/src/claude/api.ts +148 -0
  63. package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +329 -0
  64. package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +167 -0
  65. package/runtime/node_modules/@metro-labs/daemon/src/claude/settings.ts +117 -0
  66. package/runtime/node_modules/@metro-labs/daemon/src/connectors/api.ts +307 -0
  67. package/runtime/node_modules/@metro-labs/daemon/src/connectors/cli-api.ts +63 -0
  68. package/runtime/node_modules/@metro-labs/daemon/src/connectors/config.ts +144 -0
  69. package/runtime/node_modules/@metro-labs/daemon/src/connectors/json.ts +21 -0
  70. package/runtime/node_modules/@metro-labs/daemon/src/connectors/model.ts +69 -0
  71. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-client.ts +159 -0
  72. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-discovery.ts +117 -0
  73. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-pending.ts +65 -0
  74. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-routes.ts +157 -0
  75. package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth.ts +127 -0
  76. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-target.ts +39 -0
  77. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +295 -0
  78. package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +277 -0
  79. package/runtime/node_modules/@metro-labs/daemon/src/connectors/tools.ts +98 -0
  80. package/runtime/node_modules/@metro-labs/daemon/src/connectors/url.ts +70 -0
  81. package/runtime/node_modules/@metro-labs/daemon/src/connectors/verify.ts +330 -0
  82. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-grant.ts +99 -0
  83. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-owner.ts +32 -0
  84. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-serve.ts +121 -0
  85. package/runtime/node_modules/@metro-labs/daemon/src/files/upload-api.ts +294 -0
  86. package/runtime/node_modules/@metro-labs/daemon/src/files/upload-store.ts +181 -0
  87. package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +252 -0
  88. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +231 -0
  89. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-device.ts +91 -0
  90. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-stream.ts +288 -0
  91. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-translate.ts +203 -0
  92. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +220 -0
  93. package/runtime/node_modules/@metro-labs/daemon/src/gateway/eventstream.ts +123 -0
  94. package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +182 -0
  95. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +197 -0
  96. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +195 -0
  97. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +198 -0
  98. package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +40 -0
  99. package/runtime/node_modules/@metro-labs/daemon/src/gateway/served.ts +17 -0
  100. package/runtime/node_modules/@metro-labs/daemon/src/mcp/accounts.ts +137 -0
  101. package/runtime/node_modules/@metro-labs/daemon/src/mcp/call-tools.ts +216 -0
  102. package/runtime/node_modules/@metro-labs/daemon/src/mcp/channel-owner.ts +29 -0
  103. package/runtime/node_modules/@metro-labs/daemon/src/mcp/ctx.ts +62 -0
  104. package/runtime/node_modules/@metro-labs/daemon/src/mcp/event-store.ts +91 -0
  105. package/runtime/node_modules/@metro-labs/daemon/src/mcp/frame-scope.ts +21 -0
  106. package/runtime/node_modules/@metro-labs/daemon/src/mcp/group-tools.ts +129 -0
  107. package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +191 -0
  108. package/runtime/node_modules/@metro-labs/daemon/src/mcp/member-tools.ts +72 -0
  109. package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-relay.ts +66 -0
  110. package/runtime/node_modules/@metro-labs/daemon/src/mcp/raw-get-stream.ts +207 -0
  111. package/runtime/node_modules/@metro-labs/daemon/src/mcp/request-identity.ts +47 -0
  112. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-registry.ts +172 -0
  113. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-route.ts +43 -0
  114. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +215 -0
  115. package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-dispatch.ts +215 -0
  116. package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-schemas.ts +400 -0
  117. package/runtime/node_modules/@metro-labs/daemon/src/mcp/upload-tool.ts +66 -0
  118. package/runtime/node_modules/@metro-labs/daemon/src/monitor/api.ts +237 -0
  119. package/runtime/node_modules/@metro-labs/daemon/src/net/tunnel.ts +389 -0
  120. package/runtime/node_modules/@metro-labs/daemon/src/routes/http.ts +390 -0
  121. package/runtime/node_modules/@metro-labs/daemon/src/routes/identity.ts +54 -0
  122. package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +187 -0
  123. package/runtime/node_modules/@metro-labs/daemon/src/routes/session-apis.ts +60 -0
  124. package/runtime/node_modules/@metro-labs/daemon/src/routes/session.ts +29 -0
  125. package/runtime/node_modules/@metro-labs/daemon/src/server/control.ts +64 -0
  126. package/runtime/node_modules/@metro-labs/daemon/src/server/machine.ts +61 -0
  127. package/runtime/node_modules/@metro-labs/daemon/src/server/update.ts +116 -0
  128. package/runtime/node_modules/@metro-labs/daemon/src/server.ts +2 -0
  129. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-inline.ts +100 -0
  130. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-interactive.ts +161 -0
  131. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-resolve.ts +202 -0
  132. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-session.ts +280 -0
  133. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-xmtp.ts +111 -0
  134. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach.ts +184 -0
  135. package/runtime/node_modules/@metro-labs/daemon/src/stations/materialize.ts +248 -0
  136. package/runtime/node_modules/@metro-labs/daemon/src/stations/registry.ts +35 -0
  137. package/runtime/node_modules/@metro-labs/daemon/src/stations/runtime-deps.ts +82 -0
  138. package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor-io.ts +97 -0
  139. package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor.ts +320 -0
  140. package/runtime/node_modules/@metro-labs/daemon/src/stations/train-call.ts +22 -0
  141. package/runtime/node_modules/@metro-labs/daemon/src/terminal/api.ts +71 -0
  142. package/runtime/node_modules/@metro-labs/daemon/src/terminal/socket.ts +123 -0
  143. package/runtime/node_modules/@metro-labs/daemon/src/terminal/tickets.ts +40 -0
  144. package/runtime/node_modules/@metro-labs/discord-bot/package.json +28 -0
  145. package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +118 -0
  146. package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +388 -0
  147. package/runtime/node_modules/@metro-labs/discord-bot/src/api-base.ts +1 -0
  148. package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +29 -0
  149. package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +220 -0
  150. package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +50 -0
  151. package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +115 -0
  152. package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +127 -0
  153. package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +215 -0
  154. package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +43 -0
  155. package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +19 -0
  156. package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +82 -0
  157. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-speak.ts +195 -0
  158. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-transcribe.ts +292 -0
  159. package/runtime/node_modules/@metro-labs/discord-bot/src/voice.ts +197 -0
  160. package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +5 -0
  161. package/runtime/node_modules/@metro-labs/http/package.json +33 -0
  162. package/runtime/node_modules/@metro-labs/http/src/api-error.ts +8 -0
  163. package/runtime/node_modules/@metro-labs/http/src/api-http.ts +95 -0
  164. package/runtime/node_modules/@metro-labs/http/src/cors.ts +29 -0
  165. package/runtime/node_modules/@metro-labs/http/src/identity-key.ts +33 -0
  166. package/runtime/node_modules/@metro-labs/http/src/identity-registry.ts +13 -0
  167. package/runtime/node_modules/@metro-labs/http/src/mode-api.ts +32 -0
  168. package/runtime/node_modules/@metro-labs/http/src/return-to.ts +21 -0
  169. package/runtime/node_modules/@metro-labs/http/src/signed-identity.ts +36 -0
  170. package/runtime/node_modules/@metro-labs/telegram/package.json +26 -0
  171. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +80 -0
  172. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +254 -0
  173. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +87 -0
  174. package/runtime/node_modules/@metro-labs/telegram/src/client.ts +77 -0
  175. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +156 -0
  176. package/runtime/node_modules/@metro-labs/telegram/src/group.ts +261 -0
  177. package/runtime/node_modules/@metro-labs/telegram/src/history.ts +52 -0
  178. package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +78 -0
  179. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +46 -0
  180. package/runtime/node_modules/@metro-labs/telegram/src/login.ts +166 -0
  181. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +80 -0
  182. package/runtime/node_modules/@metro-labs/telegram/src/members.ts +75 -0
  183. package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +22 -0
  184. package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +167 -0
  185. package/runtime/node_modules/@metro-labs/telegram/src/station.ts +24 -0
  186. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +7 -0
  187. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -0
  188. package/runtime/node_modules/@metro-labs/telegram-bot/package.json +25 -0
  189. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +120 -0
  190. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +295 -0
  191. package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
  192. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +201 -0
  193. package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +109 -0
  194. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
  195. package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
  196. package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +17 -0
  197. package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +58 -0
  198. package/runtime/node_modules/@metro-labs/telegram-bot/src/verify.ts +38 -0
  199. package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +3 -0
  200. package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
  201. package/runtime/node_modules/@metro-labs/webhook/src/station.ts +37 -0
  202. package/runtime/node_modules/@metro-labs/whatsapp/package.json +27 -0
  203. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +66 -0
  204. package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
  205. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +178 -0
  206. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
  207. package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
  208. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +327 -0
  209. package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
  210. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +138 -0
  211. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
  212. package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
  213. package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
  214. package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
  215. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
  216. package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
  217. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
  218. package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
  219. package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +185 -0
  220. package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +17 -0
  221. package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
  222. package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
  223. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
  224. package/runtime/node_modules/@metro-labs/xmtp/package.json +33 -0
  225. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +112 -0
  226. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
  227. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
  228. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
  229. package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
  230. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +375 -0
  231. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
  232. package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
  233. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
  234. package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
  235. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +335 -0
  236. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +80 -0
  237. package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
  238. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
  239. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +33 -0
  240. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
  241. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
  242. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
  243. package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
  244. package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +13 -0
  245. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
  246. package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
  247. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
  248. package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +84 -0
  249. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +53 -0
  250. package/runtime/runtime.json +3 -0
  251. package/runtime/server.ts +1 -0
  252. package/runtime/stations.json +36 -0
  253. package/runtime/trains/.keep +0 -0
  254. package/LICENSE +0 -21
  255. package/README.md +0 -249
  256. package/dist/cache.js +0 -75
  257. package/dist/cli/actions.js +0 -139
  258. package/dist/cli/config.js +0 -185
  259. package/dist/cli/index.js +0 -177
  260. package/dist/cli/skill.js +0 -62
  261. package/dist/cli/util.js +0 -72
  262. package/dist/cli/webhook.js +0 -83
  263. package/dist/codex-rc.js +0 -245
  264. package/dist/dispatcher.js +0 -122
  265. package/dist/history.js +0 -99
  266. package/dist/ipc.js +0 -72
  267. package/dist/log.js +0 -15
  268. package/dist/paths.js +0 -68
  269. package/dist/registry.js +0 -48
  270. package/dist/stations/claude.js +0 -45
  271. package/dist/stations/codex.js +0 -68
  272. package/dist/stations/discord.js +0 -200
  273. package/dist/stations/index.js +0 -133
  274. package/dist/stations/telegram-md.js +0 -34
  275. package/dist/stations/telegram-upload.js +0 -113
  276. package/dist/stations/telegram.js +0 -213
  277. package/dist/stations/webhook.js +0 -100
  278. package/dist/tunnel.js +0 -49
  279. package/dist/webhooks.js +0 -40
  280. package/docs/agents.md +0 -216
  281. package/docs/uri-scheme.md +0 -125
  282. package/skills/metro/SKILL.md +0 -249
@@ -0,0 +1,184 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import {
3
+ DiscordTokenError,
4
+ verifyDiscordBotToken,
5
+ } from '@metro-labs/discord-bot/verify';
6
+ import {
7
+ TelegramTokenError,
8
+ verifyTelegramBotToken,
9
+ } from '@metro-labs/telegram-bot/verify';
10
+ import { ApiError } from '@metro-labs/http/api-error';
11
+ import { ensureStationDeps } from './runtime-deps.js';
12
+ import { publicBaseOrDefault } from '../files/attach-serve.js';
13
+ import {
14
+ discardXmtpDb,
15
+ newXmtpDbPath,
16
+ verifyXmtpKeyOutOfProcess,
17
+ XmtpAttachError,
18
+ type VerifyXmtpKey,
19
+ } from './attach-xmtp.js';
20
+
21
+ export const ATTACHABLE_STATIONS = [
22
+ 'discord-bot',
23
+ 'telegram-bot',
24
+ 'xmtp',
25
+ 'webhook',
26
+ ] as const;
27
+
28
+ export type AttachStation = (typeof ATTACHABLE_STATIONS)[number];
29
+
30
+ export class StationAttachError extends ApiError {}
31
+
32
+ export interface AttachInput {
33
+ station: AttachStation;
34
+ token?: unknown;
35
+ }
36
+
37
+ export interface OneTimeSecret {
38
+ label: string;
39
+ value: string;
40
+ note: string;
41
+ }
42
+
43
+ export interface PreparedAccount {
44
+ config: Record<string, unknown>;
45
+ identity: Record<string, string>;
46
+ secret?: OneTimeSecret;
47
+ discard?: () => void;
48
+ }
49
+
50
+ const TOKEN_RE = /^[A-Za-z0-9._:-]{8,256}$/;
51
+
52
+ const SECP256K1_ORDER = BigInt(
53
+ '0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141',
54
+ );
55
+
56
+ export function isAttachStation(raw: unknown): raw is AttachStation {
57
+ return (
58
+ typeof raw === 'string' &&
59
+ (ATTACHABLE_STATIONS as readonly string[]).includes(raw)
60
+ );
61
+ }
62
+
63
+ function requireToken(raw: unknown, label: string): string {
64
+ const token = typeof raw === 'string' ? raw.trim() : '';
65
+ if (token === '')
66
+ throw new StationAttachError(`a ${label} bot token is required`, 400);
67
+ if (!TOKEN_RE.test(token))
68
+ throw new StationAttachError(
69
+ `that does not look like a ${label} bot token`,
70
+ 400,
71
+ );
72
+ return token;
73
+ }
74
+
75
+ function rejected(err: unknown, own: boolean, fallback: string): never {
76
+ throw new StationAttachError(
77
+ own && err instanceof Error ? err.message : fallback,
78
+ 400,
79
+ );
80
+ }
81
+
82
+ async function prepareDiscord(raw: unknown): Promise<PreparedAccount> {
83
+ const token = requireToken(raw, 'Discord');
84
+ try {
85
+ const bot = await verifyDiscordBotToken(token);
86
+ if (!bot.messageContent)
87
+ throw new StationAttachError(
88
+ 'this Discord application does not have the Message Content intent enabled. Turn it on under Bot / Privileged Gateway Intents in the developer portal, then attach it again',
89
+ 400,
90
+ );
91
+ return {
92
+ config: { token },
93
+ identity: { userId: bot.userId, username: bot.username },
94
+ };
95
+ } catch (err) {
96
+ if (err instanceof StationAttachError) throw err;
97
+ return rejected(
98
+ err,
99
+ err instanceof DiscordTokenError,
100
+ 'Discord rejected that bot token',
101
+ );
102
+ }
103
+ }
104
+
105
+ async function prepareTelegram(raw: unknown): Promise<PreparedAccount> {
106
+ const token = requireToken(raw, 'Telegram');
107
+ try {
108
+ const bot = await verifyTelegramBotToken(token);
109
+ return {
110
+ config: { token },
111
+ identity: { botId: String(bot.botId), username: bot.username },
112
+ };
113
+ } catch (err) {
114
+ return rejected(
115
+ err,
116
+ err instanceof TelegramTokenError,
117
+ 'Telegram rejected that bot token',
118
+ );
119
+ }
120
+ }
121
+
122
+ export function newXmtpPrivateKey(): string {
123
+ for (;;) {
124
+ const hex = randomBytes(32).toString('hex');
125
+ const value = BigInt(`0x${hex}`);
126
+ if (value > 0n && value < SECP256K1_ORDER) return `0x${hex}`;
127
+ }
128
+ }
129
+
130
+ async function prepareXmtp(verify: VerifyXmtpKey): Promise<PreparedAccount> {
131
+ const privateKey = newXmtpPrivateKey();
132
+ const dbPath = newXmtpDbPath();
133
+ const identity = await verify(privateKey, dbPath).catch((err: unknown) =>
134
+ rejected(
135
+ err,
136
+ err instanceof XmtpAttachError,
137
+ 'Metro could not open an XMTP inbox with the key it generated, so nothing was attached',
138
+ ),
139
+ );
140
+ return {
141
+ config: { privateKey, dbPath },
142
+ identity: { inboxId: identity.inboxId, address: identity.address },
143
+ secret: {
144
+ label: 'xmtp private key',
145
+ value: privateKey,
146
+ note: 'Metro generated this key, opened inbox ' +
147
+ `${identity.inboxId} with it, and stores it with the account. ` +
148
+ 'It is shown once, here, and is never returned by any API again.',
149
+ },
150
+ discard: () => {
151
+ discardXmtpDb(identity.dbPath);
152
+ },
153
+ };
154
+ }
155
+
156
+ export const hookUrl = (webhookId: string, secret: string): string =>
157
+ `${publicBaseOrDefault().replace(/\/+$/, '')}/api/webhooks/${webhookId}/${secret}`;
158
+
159
+ const WEBHOOK_ID_FLOOR = 10n ** 18n;
160
+
161
+ export function newWebhookId(): string {
162
+ const raw = BigInt(`0x${randomBytes(8).toString('hex')}`);
163
+ return String(WEBHOOK_ID_FLOOR + (raw % (9n * WEBHOOK_ID_FLOOR)));
164
+ }
165
+
166
+ function prepareWebhook(): PreparedAccount {
167
+ const secret = randomBytes(48).toString('base64url');
168
+ const webhookId = newWebhookId();
169
+ return {
170
+ config: { secret, webhookId, createdAt: new Date().toISOString() },
171
+ identity: { endpoint: hookUrl(webhookId, secret) },
172
+ };
173
+ }
174
+
175
+ export async function prepareAccount(
176
+ input: AttachInput,
177
+ verify: VerifyXmtpKey = verifyXmtpKeyOutOfProcess,
178
+ ): Promise<PreparedAccount> {
179
+ ensureStationDeps(input.station);
180
+ if (input.station === 'discord-bot') return prepareDiscord(input.token);
181
+ if (input.station === 'telegram-bot') return prepareTelegram(input.token);
182
+ if (input.station === 'webhook') return prepareWebhook();
183
+ return prepareXmtp(verify);
184
+ }
@@ -0,0 +1,248 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from 'node:fs';
8
+ import { homedir } from 'node:os';
9
+ import { join } from 'node:path';
10
+ import { log } from '@metro-labs/core/log';
11
+ import { writeSecure } from '@metro-labs/core/secure-fs';
12
+ import { trainsDir } from '../boot/paths.js';
13
+ import {
14
+ setAgentMap,
15
+ setAllowlistMap,
16
+ type AgentMap,
17
+ type AgentNameMap,
18
+ type AllowlistMap,
19
+ } from '../agents/map.js';
20
+ import { setKeyMap } from '../agents/keys.js';
21
+ import { STATIONS, type StationName } from '@metro-labs/core/station-names';
22
+
23
+ interface StationTarget {
24
+ file: string;
25
+ fileEnv: string;
26
+ trainImport: string | null;
27
+ }
28
+
29
+ export interface LoadedAccount {
30
+ station: StationName;
31
+ id: string;
32
+ allowlist: string[] | null;
33
+ config: Record<string, unknown>;
34
+ }
35
+
36
+ export interface LoadedConnector {
37
+ id: string;
38
+ name: string;
39
+ url: string;
40
+ transport: 'http';
41
+ config: Record<string, unknown>;
42
+ }
43
+
44
+ export interface LoadedAgent {
45
+ id: string;
46
+ name: string;
47
+ accounts: LoadedAccount[];
48
+ key: string | null;
49
+ connectors?: LoadedConnector[];
50
+ }
51
+
52
+ const METRO_DIR = join(homedir(), '.metro');
53
+
54
+ const STATION_TARGETS: Record<StationName, StationTarget> = {
55
+ xmtp: {
56
+ file: 'xmtp-accounts.json',
57
+ fileEnv: 'XMTP_ACCOUNTS_FILE',
58
+ trainImport: '@metro-labs/xmtp/train',
59
+ },
60
+ 'telegram-bot': {
61
+ file: 'telegram-bot-accounts.json',
62
+ fileEnv: 'TELEGRAM_BOT_ACCOUNTS_FILE',
63
+ trainImport: '@metro-labs/telegram-bot/train',
64
+ },
65
+ 'telegram': {
66
+ file: 'telegram-accounts.json',
67
+ fileEnv: 'TELEGRAM_ACCOUNTS_FILE',
68
+ trainImport: '@metro-labs/telegram/train',
69
+ },
70
+ 'discord-bot': {
71
+ file: 'discord-bot-accounts.json',
72
+ fileEnv: 'DISCORD_BOT_ACCOUNTS_FILE',
73
+ trainImport: '@metro-labs/discord-bot/train',
74
+ },
75
+ whatsapp: {
76
+ file: 'whatsapp-accounts.json',
77
+ fileEnv: 'WHATSAPP_ACCOUNTS_FILE',
78
+ trainImport: '@metro-labs/whatsapp/train',
79
+ },
80
+ webhook: {
81
+ file: 'webhook-accounts.json',
82
+ fileEnv: 'WEBHOOK_ACCOUNTS_FILE',
83
+ trainImport: null,
84
+ },
85
+ };
86
+
87
+ function accountFilePath(station: StationName): string {
88
+ const target = STATION_TARGETS[station];
89
+ return process.env[target.fileEnv] ?? join(METRO_DIR, target.file);
90
+ }
91
+
92
+ export type StationSource = () => Promise<LoadedAgent[]>;
93
+
94
+ export const MOVABLE_STATIONS = new Set<StationName>(
95
+ STATIONS.filter((s) => s !== 'webhook'),
96
+ );
97
+
98
+ function trainStubPath(station: StationName): string {
99
+ return join(trainsDir(), `${station}.ts`);
100
+ }
101
+
102
+ function currentText(path: string): string | null {
103
+ try {
104
+ return readFileSync(path, 'utf8');
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
109
+
110
+ export function writeIfChanged(path: string, content: string): boolean {
111
+ if (currentText(path) === content) return false;
112
+ writeFileSync(path, content);
113
+ return true;
114
+ }
115
+
116
+ interface WrittenStations {
117
+ active: Map<StationName, number>;
118
+ changed: StationName[];
119
+ }
120
+
121
+ function writeStations(list: LoadedAgent[]): WrittenStations {
122
+ mkdirSync(METRO_DIR, { recursive: true });
123
+ mkdirSync(trainsDir(), { recursive: true });
124
+
125
+ const byStation = new Map<StationName, LoadedAccount[]>();
126
+ const map: AgentMap = {};
127
+ const names: AgentNameMap = {};
128
+ const allow: AllowlistMap = {};
129
+ for (const agent of list) {
130
+ names[agent.id] = agent.name;
131
+ for (const a of agent.accounts) {
132
+ if (!(a.station in STATION_TARGETS)) {
133
+ log.error(
134
+ { station: a.station, id: a.id },
135
+ 'unknown station — this metro is older or newer than the row; skipping it',
136
+ );
137
+ continue;
138
+ }
139
+ map[`${a.station}/${a.id}`] = agent.id;
140
+ if (a.allowlist) allow[`${a.station}/${a.id}`] = a.allowlist;
141
+ const cur = byStation.get(a.station);
142
+ if (cur) cur.push(a);
143
+ else byStation.set(a.station, [a]);
144
+ }
145
+ }
146
+ setAgentMap(map, names);
147
+ setAllowlistMap(allow);
148
+
149
+ const active = new Map<StationName, number>();
150
+ const changed: StationName[] = [];
151
+ for (const [station, accts] of byStation) {
152
+ const records = accts.map((a) => ({ id: a.id, ...a.config }));
153
+ const path = accountFilePath(station);
154
+ const content = JSON.stringify(records, null, 2);
155
+ if (currentText(path) !== content) {
156
+ writeSecure(path, content);
157
+ changed.push(station);
158
+ }
159
+ const { trainImport } = STATION_TARGETS[station];
160
+ if (trainImport !== null)
161
+ writeIfChanged(trainStubPath(station), `import '${trainImport}';\n`);
162
+ active.set(station, accts.length);
163
+ }
164
+ return { active, changed };
165
+ }
166
+
167
+ const BLANKED = '[]\n';
168
+
169
+ function pruneStations(present: Map<StationName, number>): StationName[] {
170
+ const removed: StationName[] = [];
171
+ for (const station of Object.keys(STATION_TARGETS) as StationName[]) {
172
+ if (present.has(station)) continue;
173
+ const hasTrain = STATION_TARGETS[station].trainImport !== null;
174
+ const stub = trainStubPath(station);
175
+ const stubGone = !hasTrain || !existsSync(stub);
176
+ const file = currentText(accountFilePath(station));
177
+ const fileInert = file === null || file === BLANKED;
178
+ if (stubGone && fileInert) continue;
179
+ if (!fileInert) writeSecure(accountFilePath(station), BLANKED);
180
+ if (hasTrain) rmSync(stub, { force: true });
181
+ removed.push(station);
182
+ }
183
+ return removed;
184
+ }
185
+
186
+ const stationLabels = (m: Map<StationName, number>): string[] =>
187
+ [...m].map(([station, n]) => `${station}(${n})`);
188
+
189
+ function applyKeyMap(list: LoadedAgent[]): void {
190
+ lastKey = list.find((a) => a.key !== null)?.key ?? null;
191
+ setKeyMap(
192
+ list.flatMap((agent) =>
193
+ agent.key === null ? [] : [{ key: agent.key, agentId: agent.id }],
194
+ ),
195
+ );
196
+ }
197
+
198
+ let lastKey: string | null = null;
199
+
200
+ export function localAgentKey(): string | null {
201
+ return lastKey;
202
+ }
203
+
204
+ async function loadAndWrite(
205
+ source: StationSource,
206
+ ): Promise<WrittenStations & { agents: number }> {
207
+ const list = await source();
208
+ applyKeyMap(list);
209
+ return { ...writeStations(list), agents: list.length };
210
+ }
211
+
212
+ export interface MaterializeOptions {
213
+ allowEmpty?: boolean;
214
+ }
215
+
216
+ export async function materializeFrom(
217
+ source: StationSource,
218
+ opts: MaterializeOptions = {},
219
+ ): Promise<void> {
220
+ const { active, agents: found } = await loadAndWrite(source);
221
+
222
+ if (found === 0 && opts.allowEmpty !== true)
223
+ throw new Error('no agents found — nothing to materialize');
224
+ const removed = pruneStations(active);
225
+ log.info(
226
+ { stations: stationLabels(active), agents: found, removed },
227
+ 'materialized station accounts',
228
+ );
229
+ }
230
+
231
+ export interface ReloadedStations {
232
+ active: StationName[];
233
+ removed: StationName[];
234
+ changed: StationName[];
235
+ }
236
+
237
+ export async function reloadFrom(
238
+ source: StationSource,
239
+ ): Promise<ReloadedStations> {
240
+ const { active, changed } = await loadAndWrite(source);
241
+ const removed = pruneStations(active);
242
+ if (removed.length > 0 || changed.length > 0)
243
+ log.info(
244
+ { stations: stationLabels(active), removed, changed },
245
+ 'reloaded station accounts',
246
+ );
247
+ return { active: [...active.keys()], removed, changed };
248
+ }
@@ -0,0 +1,35 @@
1
+ import type { Station } from '@metro-labs/core/stations/types';
2
+ import { Line } from '@metro-labs/core/lines';
3
+ import { xmtpStation } from '@metro-labs/xmtp';
4
+ import { telegramBotStation } from '@metro-labs/telegram-bot';
5
+ import { telegramStation } from '@metro-labs/telegram';
6
+ import { discordBotStation } from '@metro-labs/discord-bot';
7
+ import { whatsappStation } from '@metro-labs/whatsapp';
8
+ import { webhookStation } from '@metro-labs/webhook';
9
+
10
+ export const STATIONS: readonly Station[] = [
11
+ xmtpStation,
12
+ telegramBotStation,
13
+ telegramStation,
14
+ discordBotStation,
15
+ whatsappStation,
16
+ webhookStation,
17
+ ];
18
+
19
+ export const stationByName = (name: string): Station | undefined =>
20
+ STATIONS.find((s) => s.name === name);
21
+
22
+ export const stationForLine = (line: string): Station | undefined => {
23
+ const s = Line.station(line);
24
+ return s ? stationByName(s) : undefined;
25
+ };
26
+
27
+ export const accountStationNames = (): string[] =>
28
+ STATIONS.filter((s) => s.hasAccounts).map((s) => s.name);
29
+
30
+ export const accountStationCapabilities = (): Record<string, string[]> => {
31
+ const out: Record<string, string[]> = {};
32
+ for (const s of STATIONS)
33
+ if (s.hasAccounts) out[s.name] = [...s.messageVerbs].sort();
34
+ return out;
35
+ };
@@ -0,0 +1,82 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { log } from '@metro-labs/core/log';
5
+ import { isRecord } from '@metro-labs/core/is-record';
6
+ import type { StationName } from '@metro-labs/core/station-names';
7
+ import { knownAccounts } from '../agents/map.js';
8
+
9
+ const INSTALL_TIMEOUT_MS = 15 * 60_000;
10
+
11
+ export interface RuntimeManifest {
12
+ core: Record<string, string>;
13
+ stations: Record<string, Record<string, string>>;
14
+ }
15
+
16
+ export interface RuntimeStore {
17
+ dir: string;
18
+ manifest: RuntimeManifest;
19
+ }
20
+
21
+ const ranges = (raw: unknown): Record<string, string> =>
22
+ isRecord(raw)
23
+ ? Object.fromEntries(Object.entries(raw).filter((e): e is [string, string] => typeof e[1] === 'string'))
24
+ : {};
25
+
26
+ export function readManifest(path: string): RuntimeManifest {
27
+ const raw: unknown = JSON.parse(readFileSync(path, 'utf8'));
28
+ if (!isRecord(raw)) throw new Error(`${path} is not a runtime manifest`);
29
+ const stations = isRecord(raw.stations) ? raw.stations : {};
30
+ return {
31
+ core: ranges(raw.core),
32
+ stations: Object.fromEntries(Object.entries(stations).map(([name, deps]) => [name, ranges(deps)])),
33
+ };
34
+ }
35
+
36
+ export function runtimeStore(): RuntimeStore | null {
37
+ const dir = process.env.METRO_RUNTIME_STORE?.trim() ?? '';
38
+ const manifest = process.env.METRO_RUNTIME_MANIFEST?.trim() ?? '';
39
+ if (dir === '' || manifest === '') return null;
40
+ return { dir, manifest: readManifest(manifest) };
41
+ }
42
+
43
+ export function dependenciesFor(manifest: RuntimeManifest, stations: Iterable<string>): Record<string, string> {
44
+ const out: Record<string, string> = { ...manifest.core };
45
+ for (const station of new Set(stations)) Object.assign(out, manifest.stations[station] ?? {});
46
+ return Object.fromEntries(Object.entries(out).sort(([a], [b]) => a.localeCompare(b)));
47
+ }
48
+
49
+ const packageText = (deps: Record<string, string>): string =>
50
+ `${JSON.stringify({ name: 'metro-runtime', private: true, dependencies: deps }, null, 2)}\n`;
51
+
52
+ function current(dir: string): string | null {
53
+ try {
54
+ return readFileSync(join(dir, 'package.json'), 'utf8');
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ export function installRuntime(store: RuntimeStore, stations: Iterable<string>): boolean {
61
+ const wanted = packageText(dependenciesFor(store.manifest, stations));
62
+ const installed = existsSync(join(store.dir, 'node_modules', '.metro-installed'));
63
+ if (installed && current(store.dir) === wanted) return false;
64
+ mkdirSync(store.dir, { recursive: true });
65
+ writeFileSync(join(store.dir, 'package.json'), wanted);
66
+ log.info({ dir: store.dir }, 'runtime: installing the channel SDKs this machine needs');
67
+ const run = spawnSync('bun', ['install', '--no-summary', '--no-progress'], {
68
+ cwd: store.dir,
69
+ stdio: ['ignore', 'inherit', 'inherit'],
70
+ timeout: INSTALL_TIMEOUT_MS,
71
+ });
72
+ if (run.error !== undefined || run.status !== 0)
73
+ throw new Error(`bun install failed in ${store.dir}: ${run.error?.message ?? `exit ${String(run.status)}`}`);
74
+ writeFileSync(join(store.dir, 'node_modules', '.metro-installed'), `${new Date().toISOString()}\n`);
75
+ return true;
76
+ }
77
+
78
+ export function ensureStationDeps(station: StationName): void {
79
+ const store = runtimeStore();
80
+ if (store === null) return;
81
+ installRuntime(store, [...knownAccounts().map((a) => a.station), station]);
82
+ }
@@ -0,0 +1,97 @@
1
+ import { watch, type FSWatcher } from 'node:fs';
2
+ import { join, parse as parsePath } from 'node:path';
3
+ import { errMsg, log } from '@metro-labs/core/log';
4
+ import type { Pending } from '@metro-labs/core/trains/protocol';
5
+
6
+ export const RESTART_BACKOFFS_MS = [1_000, 5_000, 30_000] as const;
7
+ export const MAX_CONSECUTIVE_FAILS = 5;
8
+ export const HOT_RELOAD_DEBOUNCE_MS = 300;
9
+ export const TRAIN_EXT = /\.(ts|js|mjs)$/;
10
+
11
+ export interface TrainState {
12
+ name: string;
13
+ path: string;
14
+ proc: ReturnType<typeof Bun.spawn> | null;
15
+ pending: Map<string, Pending>;
16
+ buf: string;
17
+ errBuf: string;
18
+ failCount: number;
19
+ restartTimer: ReturnType<typeof setTimeout> | null;
20
+ startedAt: string | null;
21
+ stopped: boolean;
22
+ }
23
+
24
+ export function startWatcher(
25
+ dir: string,
26
+ timers: Map<string, ReturnType<typeof setTimeout>>,
27
+ onChange: (name: string, path: string) => void,
28
+ ): FSWatcher | null {
29
+ try {
30
+ const w = watch(dir, (_event, filename) => {
31
+ if (!filename) return;
32
+ const base = filename;
33
+ if (!TRAIN_EXT.test(base) || base.startsWith('_') || base.startsWith('.'))
34
+ return;
35
+ const name = parsePath(base).name;
36
+ const existing = timers.get(name);
37
+ if (existing) clearTimeout(existing);
38
+ timers.set(
39
+ name,
40
+ setTimeout(() => {
41
+ timers.delete(name);
42
+ onChange(name, join(dir, base));
43
+ }, HOT_RELOAD_DEBOUNCE_MS),
44
+ );
45
+ });
46
+ w.on('error', (err) => {
47
+ log.warn({ err: errMsg(err) }, 'train hot-reload: watcher error');
48
+ });
49
+ log.info({ dir }, 'train hot-reload: watching');
50
+ return w;
51
+ } catch (err) {
52
+ log.warn(
53
+ { err: errMsg(err) },
54
+ 'train hot-reload: watch unavailable (on-demand restart only)',
55
+ );
56
+ return null;
57
+ }
58
+ }
59
+
60
+ export function killGracefully(
61
+ proc: ReturnType<typeof Bun.spawn> | null,
62
+ ): Promise<unknown> | null {
63
+ if (proc?.exitCode !== null) return null;
64
+ try {
65
+ proc.kill('SIGTERM');
66
+ } catch {
67
+ }
68
+ const grace = setTimeout(() => {
69
+ try {
70
+ proc.kill('SIGKILL');
71
+ } catch {
72
+ }
73
+ }, 2_000);
74
+ return proc.exited.finally(() => {
75
+ clearTimeout(grace);
76
+ });
77
+ }
78
+
79
+ export async function pumpStream(
80
+ stream: unknown,
81
+ name: string,
82
+ kind: 'stdout' | 'stderr',
83
+ onChunk: (s: string) => void,
84
+ ): Promise<void> {
85
+ if (!stream || typeof stream === 'number') return;
86
+ const reader = (stream as ReadableStream<Uint8Array>).getReader();
87
+ const dec = new TextDecoder();
88
+ try {
89
+ while (true) {
90
+ const { value, done } = await reader.read();
91
+ if (done) break;
92
+ onChunk(dec.decode(value, { stream: true }));
93
+ }
94
+ } catch (err) {
95
+ log.debug({ name, err: errMsg(err) }, `train: ${kind} pump ended`);
96
+ }
97
+ }