@stage-labs/metro 0.1.0-beta.6 → 0.1.0-beta.60

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 (258) hide show
  1. package/dist/api.js +14 -0
  2. package/dist/bedrock-eventstream.js +120 -0
  3. package/dist/bedrock-proxy.js +260 -0
  4. package/dist/bedrock-upstream.js +72 -0
  5. package/dist/bedrock.js +106 -0
  6. package/dist/claude.js +15 -0
  7. package/dist/cli.js +94 -0
  8. package/dist/control.js +74 -0
  9. package/dist/local.js +93 -0
  10. package/dist/plugin.js +80 -0
  11. package/dist/runtime-install.js +84 -0
  12. package/dist/runtime.js +63 -0
  13. package/dist/serve.js +147 -0
  14. package/dist/tail.js +119 -0
  15. package/dist/update.js +45 -0
  16. package/dist/version.js +97 -0
  17. package/package.json +14 -25
  18. package/runtime/node_modules/@metro-labs/discord-bot/package.json +28 -0
  19. package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +118 -0
  20. package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +388 -0
  21. package/runtime/node_modules/@metro-labs/discord-bot/src/api-base.ts +1 -0
  22. package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +29 -0
  23. package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +210 -0
  24. package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +50 -0
  25. package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +115 -0
  26. package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +127 -0
  27. package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +215 -0
  28. package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +43 -0
  29. package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +19 -0
  30. package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +82 -0
  31. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-speak.ts +195 -0
  32. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-transcribe.ts +292 -0
  33. package/runtime/node_modules/@metro-labs/discord-bot/src/voice.ts +197 -0
  34. package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +5 -0
  35. package/runtime/node_modules/@metro-labs/mcp/package.json +63 -0
  36. package/runtime/node_modules/@metro-labs/mcp/src/channels/dedupe.ts +24 -0
  37. package/runtime/node_modules/@metro-labs/mcp/src/channels/inbound.ts +367 -0
  38. package/runtime/node_modules/@metro-labs/mcp/src/channels/media-note.ts +85 -0
  39. package/runtime/node_modules/@metro-labs/mcp/src/channels/pending.ts +75 -0
  40. package/runtime/node_modules/@metro-labs/mcp/src/channels/relay.ts +129 -0
  41. package/runtime/node_modules/@metro-labs/mcp/src/channels/webhook-note.ts +51 -0
  42. package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +311 -0
  43. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +302 -0
  44. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-connector-api.ts +138 -0
  45. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-error.ts +8 -0
  46. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +95 -0
  47. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +99 -0
  48. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-owner.ts +32 -0
  49. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +128 -0
  50. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +280 -0
  51. package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +199 -0
  52. package/runtime/node_modules/@metro-labs/mcp/src/daemon/bundle-api.ts +111 -0
  53. package/runtime/node_modules/@metro-labs/mcp/src/daemon/claude-api.ts +97 -0
  54. package/runtime/node_modules/@metro-labs/mcp/src/daemon/claude-files.ts +329 -0
  55. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connect-hint.ts +39 -0
  56. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +311 -0
  57. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-json.ts +21 -0
  58. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth-routes.ts +157 -0
  59. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +127 -0
  60. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +98 -0
  61. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +70 -0
  62. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +330 -0
  63. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cors.ts +29 -0
  64. package/runtime/node_modules/@metro-labs/mcp/src/daemon/crash-guard.ts +49 -0
  65. package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +117 -0
  66. package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +368 -0
  67. package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity-key.ts +33 -0
  68. package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity-registry.ts +13 -0
  69. package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity-routes.ts +54 -0
  70. package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +7 -0
  71. package/runtime/node_modules/@metro-labs/mcp/src/daemon/is-record.ts +2 -0
  72. package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-cli-api.ts +70 -0
  73. package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +196 -0
  74. package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-owner.ts +14 -0
  75. package/runtime/node_modules/@metro-labs/mcp/src/daemon/log.ts +42 -0
  76. package/runtime/node_modules/@metro-labs/mcp/src/daemon/mode-api.ts +32 -0
  77. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +159 -0
  78. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +117 -0
  79. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +65 -0
  80. package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +109 -0
  81. package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +224 -0
  82. package/runtime/node_modules/@metro-labs/mcp/src/daemon/relay.ts +302 -0
  83. package/runtime/node_modules/@metro-labs/mcp/src/daemon/return-to.ts +21 -0
  84. package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-deps.ts +82 -0
  85. package/runtime/node_modules/@metro-labs/mcp/src/daemon/secure-fs.ts +57 -0
  86. package/runtime/node_modules/@metro-labs/mcp/src/daemon/server-types.ts +21 -0
  87. package/runtime/node_modules/@metro-labs/mcp/src/daemon/servers-api.ts +80 -0
  88. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +29 -0
  89. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +58 -0
  90. package/runtime/node_modules/@metro-labs/mcp/src/daemon/signed-identity.ts +36 -0
  91. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor-io.ts +97 -0
  92. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor.ts +320 -0
  93. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-call.ts +22 -0
  94. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-error.ts +53 -0
  95. package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +352 -0
  96. package/runtime/node_modules/@metro-labs/mcp/src/daemon/update-api.ts +116 -0
  97. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +294 -0
  98. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-store.ts +181 -0
  99. package/runtime/node_modules/@metro-labs/mcp/src/daemon/vault-api.ts +78 -0
  100. package/runtime/node_modules/@metro-labs/mcp/src/daemon/vault-types.ts +12 -0
  101. package/runtime/node_modules/@metro-labs/mcp/src/daemon/version.ts +2 -0
  102. package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +19 -0
  103. package/runtime/node_modules/@metro-labs/mcp/src/db/address.ts +6 -0
  104. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +43 -0
  105. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-map.ts +84 -0
  106. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-scope.ts +64 -0
  107. package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +32 -0
  108. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +144 -0
  109. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +39 -0
  110. package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +77 -0
  111. package/runtime/node_modules/@metro-labs/mcp/src/db/file-admin.ts +365 -0
  112. package/runtime/node_modules/@metro-labs/mcp/src/db/file-source.ts +161 -0
  113. package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +21 -0
  114. package/runtime/node_modules/@metro-labs/mcp/src/db/key-map.ts +45 -0
  115. package/runtime/node_modules/@metro-labs/mcp/src/db/local-connectors.ts +322 -0
  116. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +253 -0
  117. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +33 -0
  118. package/runtime/node_modules/@metro-labs/mcp/src/db/servers.ts +83 -0
  119. package/runtime/node_modules/@metro-labs/mcp/src/db/stations.ts +12 -0
  120. package/runtime/node_modules/@metro-labs/mcp/src/db/vault.ts +109 -0
  121. package/runtime/node_modules/@metro-labs/mcp/src/mcp/accounts.ts +141 -0
  122. package/runtime/node_modules/@metro-labs/mcp/src/mcp/call-tools.ts +203 -0
  123. package/runtime/node_modules/@metro-labs/mcp/src/mcp/channel-owner.ts +29 -0
  124. package/runtime/node_modules/@metro-labs/mcp/src/mcp/ctx.ts +62 -0
  125. package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +91 -0
  126. package/runtime/node_modules/@metro-labs/mcp/src/mcp/frame-scope.ts +21 -0
  127. package/runtime/node_modules/@metro-labs/mcp/src/mcp/group-tools.ts +129 -0
  128. package/runtime/node_modules/@metro-labs/mcp/src/mcp/index.ts +191 -0
  129. package/runtime/node_modules/@metro-labs/mcp/src/mcp/member-tools.ts +72 -0
  130. package/runtime/node_modules/@metro-labs/mcp/src/mcp/permission-relay.ts +66 -0
  131. package/runtime/node_modules/@metro-labs/mcp/src/mcp/raw-get-stream.ts +207 -0
  132. package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +47 -0
  133. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-registry.ts +172 -0
  134. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +43 -0
  135. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session.ts +211 -0
  136. package/runtime/node_modules/@metro-labs/mcp/src/mcp/str.ts +5 -0
  137. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-dispatch.ts +212 -0
  138. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +397 -0
  139. package/runtime/node_modules/@metro-labs/mcp/src/mcp/upload-tool.ts +66 -0
  140. package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +237 -0
  141. package/runtime/node_modules/@metro-labs/mcp/src/server.ts +2 -0
  142. package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +60 -0
  143. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-inline.ts +100 -0
  144. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +161 -0
  145. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +200 -0
  146. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-xmtp.ts +111 -0
  147. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +184 -0
  148. package/runtime/node_modules/@metro-labs/mcp/src/stations/attachments.ts +196 -0
  149. package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +82 -0
  150. package/runtime/node_modules/@metro-labs/mcp/src/stations/messaging-normalize.ts +88 -0
  151. package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +35 -0
  152. package/runtime/node_modules/@metro-labs/mcp/src/stations/station-runtime.ts +64 -0
  153. package/runtime/node_modules/@metro-labs/mcp/src/stations/types.ts +107 -0
  154. package/runtime/node_modules/@metro-labs/telegram/package.json +26 -0
  155. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +80 -0
  156. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +254 -0
  157. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +87 -0
  158. package/runtime/node_modules/@metro-labs/telegram/src/client.ts +77 -0
  159. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +141 -0
  160. package/runtime/node_modules/@metro-labs/telegram/src/group.ts +261 -0
  161. package/runtime/node_modules/@metro-labs/telegram/src/history.ts +52 -0
  162. package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +78 -0
  163. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +46 -0
  164. package/runtime/node_modules/@metro-labs/telegram/src/login.ts +166 -0
  165. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +80 -0
  166. package/runtime/node_modules/@metro-labs/telegram/src/members.ts +75 -0
  167. package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +22 -0
  168. package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +167 -0
  169. package/runtime/node_modules/@metro-labs/telegram/src/station.ts +24 -0
  170. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +7 -0
  171. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -0
  172. package/runtime/node_modules/@metro-labs/telegram-bot/package.json +25 -0
  173. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +119 -0
  174. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +295 -0
  175. package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
  176. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +172 -0
  177. package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +103 -0
  178. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
  179. package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
  180. package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +17 -0
  181. package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +48 -0
  182. package/runtime/node_modules/@metro-labs/telegram-bot/src/verify.ts +38 -0
  183. package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +3 -0
  184. package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
  185. package/runtime/node_modules/@metro-labs/webhook/src/station.ts +37 -0
  186. package/runtime/node_modules/@metro-labs/whatsapp/package.json +27 -0
  187. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +66 -0
  188. package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
  189. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +178 -0
  190. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
  191. package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
  192. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +318 -0
  193. package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
  194. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +132 -0
  195. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
  196. package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
  197. package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
  198. package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
  199. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
  200. package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
  201. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
  202. package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
  203. package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +141 -0
  204. package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +17 -0
  205. package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
  206. package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
  207. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
  208. package/runtime/node_modules/@metro-labs/xmtp/package.json +33 -0
  209. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +112 -0
  210. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
  211. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
  212. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
  213. package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
  214. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +375 -0
  215. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
  216. package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
  217. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
  218. package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
  219. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +333 -0
  220. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +78 -0
  221. package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
  222. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
  223. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +33 -0
  224. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
  225. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
  226. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
  227. package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
  228. package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +13 -0
  229. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
  230. package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
  231. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
  232. package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +84 -0
  233. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +39 -0
  234. package/runtime/runtime.json +3 -0
  235. package/runtime/stations.json +35 -0
  236. package/runtime/trains/.keep +0 -0
  237. package/LICENSE +0 -21
  238. package/README.md +0 -192
  239. package/dist/cache.js +0 -74
  240. package/dist/cli/actions.js +0 -156
  241. package/dist/cli/config.js +0 -184
  242. package/dist/cli/index.js +0 -162
  243. package/dist/cli/skill.js +0 -62
  244. package/dist/cli/util.js +0 -72
  245. package/dist/codex-rc.js +0 -236
  246. package/dist/dispatcher.js +0 -112
  247. package/dist/history.js +0 -84
  248. package/dist/ipc.js +0 -71
  249. package/dist/log.js +0 -15
  250. package/dist/paths.js +0 -68
  251. package/dist/stations/discord.js +0 -198
  252. package/dist/stations/index.js +0 -73
  253. package/dist/stations/telegram-md.js +0 -34
  254. package/dist/stations/telegram-upload.js +0 -113
  255. package/dist/stations/telegram.js +0 -235
  256. package/docs/agents.md +0 -168
  257. package/docs/uri-scheme.md +0 -71
  258. package/skills/metro/SKILL.md +0 -220
@@ -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 '../daemon/api-error.js';
11
+ import { ensureStationDeps } from '../daemon/runtime-deps.js';
12
+ import { publicBaseOrDefault } from '../daemon/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,196 @@
1
+ export const attachDir = (): string =>
2
+ process.env.METRO_XMTP_ATTACH_DIR ??
3
+ `${process.env.HOME}/.cache/metro/messenger-uploads`;
4
+
5
+ const CACHE_NAME_RE = /^msg_[A-Za-z0-9]{1,16}_\d+\.[A-Za-z0-9]{1,5}$/;
6
+
7
+ export const resolveCachedAttachment = (name: string): string | null =>
8
+ CACHE_NAME_RE.test(name) ? `${attachDir()}/${name}` : null;
9
+
10
+ const MIME_EXT: Record<string, string> = {
11
+ 'image/jpeg': 'jpg',
12
+ 'image/jpg': 'jpg',
13
+ 'image/png': 'png',
14
+ 'image/gif': 'gif',
15
+ 'image/webp': 'webp',
16
+ 'image/heic': 'heic',
17
+ 'audio/mp4': 'm4a',
18
+ 'audio/mpeg': 'mp3',
19
+ 'audio/ogg': 'ogg',
20
+ 'audio/wav': 'wav',
21
+ 'video/mp4': 'mp4',
22
+ 'video/quicktime': 'mov',
23
+ 'video/webm': 'webm',
24
+ 'application/pdf': 'pdf',
25
+ };
26
+
27
+ const extFromName = (filename: string | undefined): string | undefined => {
28
+ const ext = filename
29
+ ?.split('?')[0]
30
+ ?.split('#')[0]
31
+ ?.split('.')
32
+ .pop()
33
+ ?.toLowerCase();
34
+ return ext !== undefined && ext.length >= 1 && ext.length <= 5
35
+ ? ext
36
+ : undefined;
37
+ };
38
+
39
+ export const extFromMime = (mime: string | undefined): string | undefined => {
40
+ if (mime === undefined) return undefined;
41
+ const mapped = MIME_EXT[mime];
42
+ if (mapped) return mapped;
43
+ if (mime.startsWith('image/')) return mime.slice(6).replace('jpeg', 'jpg');
44
+ return undefined;
45
+ };
46
+
47
+ const extFor = (
48
+ filename: string | undefined,
49
+ mime: string | undefined,
50
+ ): string => extFromName(filename) ?? extFromMime(mime) ?? 'bin';
51
+
52
+ const cacheFileName = (
53
+ messageId: string,
54
+ index: number,
55
+ filename: string | undefined,
56
+ mime: string | undefined,
57
+ ): string => {
58
+ const safeId =
59
+ messageId.replace(/[^a-zA-Z0-9]/g, '').slice(0, 16) || 'unknown';
60
+ return `msg_${safeId}_${index}.${extFor(filename, mime)}`;
61
+ };
62
+
63
+ export interface SavedAttachment {
64
+ path: string;
65
+ mime?: string;
66
+ name?: string;
67
+ bytes: number;
68
+ }
69
+
70
+ export const MAX_ATTACHMENT_BYTES = Number(
71
+ process.env.METRO_ATTACH_MAX_BYTES ?? 100 * 1024 * 1024,
72
+ );
73
+
74
+ export function assertAttachmentSize(bytes: number): void {
75
+ if (bytes > MAX_ATTACHMENT_BYTES)
76
+ throw new Error(
77
+ `attachment size ${bytes} exceeds limit of ${MAX_ATTACHMENT_BYTES} bytes`,
78
+ );
79
+ }
80
+
81
+ export function assertContentLength(header: string | null | undefined): void {
82
+ if (header == null) return;
83
+ const n = Number(header);
84
+ if (Number.isFinite(n)) assertAttachmentSize(n);
85
+ }
86
+
87
+ const cachePathFor = async (
88
+ messageId: string,
89
+ index: number,
90
+ meta: { mime?: string; name?: string },
91
+ ): Promise<string> => {
92
+ const { mkdir } = await import('node:fs/promises');
93
+ const nodePath = await import('node:path');
94
+ const dir = attachDir();
95
+ await mkdir(dir, { recursive: true });
96
+ return nodePath.join(
97
+ dir,
98
+ cacheFileName(messageId, index, meta.name, meta.mime),
99
+ );
100
+ };
101
+
102
+ export const saveBufferToCache = async (
103
+ data: Uint8Array,
104
+ messageId: string,
105
+ index: number,
106
+ meta: { mime?: string; name?: string },
107
+ ): Promise<SavedAttachment> => {
108
+ assertAttachmentSize(data.length);
109
+ const { writeFile } = await import('node:fs/promises');
110
+ const path = await cachePathFor(messageId, index, meta);
111
+ await writeFile(path, data);
112
+ return { path, mime: meta.mime, name: meta.name, bytes: data.length };
113
+ };
114
+
115
+ export const saveStreamToCache = async (
116
+ chunks: AsyncIterable<Uint8Array>,
117
+ messageId: string,
118
+ index: number,
119
+ meta: { mime?: string; name?: string },
120
+ ): Promise<SavedAttachment> => {
121
+ const { open, rename, rm } = await import('node:fs/promises');
122
+ const path = await cachePathFor(messageId, index, meta);
123
+ const part = `${path}.part`;
124
+ const handle = await open(part, 'w');
125
+ let bytes = 0;
126
+ try {
127
+ for await (const chunk of chunks) {
128
+ bytes += chunk.length;
129
+ assertAttachmentSize(bytes);
130
+ await handle.write(chunk);
131
+ }
132
+ } catch (err) {
133
+ await handle.close();
134
+ await rm(part, { force: true });
135
+ throw err;
136
+ }
137
+ await handle.close();
138
+ await rename(part, path);
139
+ return { path, mime: meta.mime, name: meta.name, bytes };
140
+ };
141
+
142
+ export const guessMime = (path: string): string => {
143
+ const ext = (path.split('.').pop() ?? '').toLowerCase();
144
+ const map: Record<string, string> = {
145
+ png: 'image/png',
146
+ jpg: 'image/jpeg',
147
+ jpeg: 'image/jpeg',
148
+ gif: 'image/gif',
149
+ webp: 'image/webp',
150
+ heic: 'image/heic',
151
+ mp4: 'video/mp4',
152
+ mov: 'video/quicktime',
153
+ webm: 'video/webm',
154
+ m4v: 'video/x-m4v',
155
+ m4a: 'audio/mp4',
156
+ mp3: 'audio/mpeg',
157
+ ogg: 'audio/ogg',
158
+ wav: 'audio/wav',
159
+ pdf: 'application/pdf',
160
+ };
161
+ return map[ext] ?? 'application/octet-stream';
162
+ };
163
+
164
+ export const isImageMime = (mime: string): boolean =>
165
+ mime.toLowerCase().startsWith('image/');
166
+ export const isImageExt = (path: string): boolean =>
167
+ /\.(png|jpe?g|gif|webp|heic|bmp|svg)$/i.test(path);
168
+
169
+ export const appendFile = async (
170
+ form: FormData,
171
+ field: string,
172
+ path: string,
173
+ name: string,
174
+ ): Promise<void> => {
175
+ const data = await Bun.file(path).arrayBuffer();
176
+ form.append(field, new Blob([data]), name);
177
+ };
178
+
179
+ export const kindOf = (mime: string, path: string): string => {
180
+ if (isImageMime(mime) || isImageExt(path)) return 'image';
181
+ if (mime.startsWith('video/')) return 'video';
182
+ if (mime.startsWith('audio/')) return 'audio';
183
+ return 'file';
184
+ };
185
+
186
+ export const toCanonical = (a: {
187
+ path: string;
188
+ mime: string;
189
+ name: string;
190
+ }): Record<string, unknown> => ({
191
+ kind: kindOf(a.mime, a.path),
192
+ path: a.path,
193
+ url: a.path,
194
+ name: a.name,
195
+ mime: a.mime,
196
+ });
@@ -0,0 +1,82 @@
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 isSignedInt = (s: string): boolean => /^-?\d+$/.test(s);
29
+
30
+ function splitTelegramAccount(path: string[]): { accountId: string; rest: string[] } {
31
+ const first = path[0];
32
+ if (path.length >= 2 && first !== undefined && !isSignedInt(first))
33
+ return { accountId: first, rest: path.slice(1) };
34
+ return { accountId: 'default', rest: path };
35
+ }
36
+
37
+ function parseTelegramLine(
38
+ line: Line | string,
39
+ ): { accountId: string; chatId: number; topicId?: number } | null {
40
+ const p = Line.parse(line);
41
+ if (p?.station !== 'telegram-bot') return null;
42
+ const { accountId, rest } = splitTelegramAccount(p.path);
43
+ const [chatId, topicId] = rest;
44
+ if (rest.length < 1 || rest.length > 2 || chatId === undefined || !isSignedInt(chatId))
45
+ return null;
46
+ if (topicId !== undefined && !isSnowflake(topicId)) return null;
47
+ return {
48
+ accountId,
49
+ chatId: Number(chatId),
50
+ ...(topicId !== undefined ? { topicId: Number(topicId) } : {}),
51
+ };
52
+ }
53
+
54
+ export const Line = {
55
+ claude: (orgId: string, sessionId: string): Line =>
56
+ build('claude', orgId, sessionId),
57
+ webhook: (endpointId: string): Line => build('webhook', endpointId),
58
+ user: (station: string, id: string | number): Line =>
59
+ build(station, 'user', id),
60
+
61
+ parse(line: Line | string): { station: string; path: string[] } | null {
62
+ if (!line.startsWith(PREFIX)) return null;
63
+ const rest = line.slice(PREFIX.length);
64
+ const slash = rest.indexOf('/');
65
+ if (slash <= 0) return null;
66
+ const path = rest
67
+ .slice(slash + 1)
68
+ .split('/')
69
+ .filter(Boolean);
70
+ return path.length ? { station: rest.slice(0, slash), path } : null;
71
+ },
72
+ station: (line: Line | string): string | null =>
73
+ Line.parse(line)?.station ?? null,
74
+ isLocal: (line: Line | string): boolean => Line.station(line) === 'claude',
75
+
76
+ parseXmtp: (line: Line | string) => parseAccountScoped(line, 'xmtp'),
77
+
78
+ parseDiscord: (line: Line | string) =>
79
+ parseAccountScoped(line, 'discord-bot', isSnowflake),
80
+
81
+ parseTelegram: (line: Line | string) => parseTelegramLine(line),
82
+ };
@@ -0,0 +1,88 @@
1
+ type Args = Record<string, unknown>;
2
+ export interface Normalized {
3
+ action: string;
4
+ args: Args;
5
+ }
6
+
7
+ interface Attachment {
8
+ kind?: string;
9
+ path?: string;
10
+ url?: string;
11
+ data?: string;
12
+ mime?: string;
13
+ name?: string;
14
+ }
15
+
16
+ interface DiscordFile {
17
+ file: string;
18
+ kind: string;
19
+ name: string;
20
+ }
21
+
22
+ const discordFile = (a: Attachment): DiscordFile | null => {
23
+ const file = a.path ?? a.url;
24
+ if (typeof file !== 'string') return null;
25
+ return { file, kind: a.kind ?? 'file', name: a.name ?? '' };
26
+ };
27
+
28
+ function discordAttachments(att: Attachment[]): Args {
29
+ const usable = att
30
+ .map(discordFile)
31
+ .filter((f): f is DiscordFile => f !== null);
32
+ if (!usable.length) return {};
33
+ return {
34
+ files: usable.map((f) => f.file),
35
+ attachmentKinds: usable.map((f) => f.kind),
36
+ attachmentNames: usable.map((f) => f.name),
37
+ };
38
+ }
39
+
40
+ export function normalizeDiscord(action: string, env: Args): Normalized {
41
+ const att = (env.attachments as Attachment[] | undefined) ?? [];
42
+ if (action === 'send') {
43
+ return { action: 'send', args: { ...env, ...discordAttachments(att) } };
44
+ }
45
+ if (action === 'reply') {
46
+ const replyTo = env.replyTo ?? env.messageId;
47
+ return {
48
+ action: 'send',
49
+ args: { ...env, replyTo, ...discordAttachments(att) },
50
+ };
51
+ }
52
+ if (action === 'unreact')
53
+ return { action: 'react', args: { ...env, emoji: '' } };
54
+ if (action === 'read') {
55
+ return {
56
+ action: 'fetch',
57
+ args: { line: env.line, limit: env.limit, before: env.before },
58
+ };
59
+ }
60
+ return { action, args: env };
61
+ }
62
+
63
+ export function normalizeTelegram(action: string, env: Args): Normalized {
64
+ if (action === 'reply') {
65
+ return {
66
+ action: 'send',
67
+ args: {
68
+ line: env.line,
69
+ text: env.text,
70
+ replyTo: env.replyTo,
71
+ attachments: env.attachments,
72
+ },
73
+ };
74
+ }
75
+ if (action === 'unreact')
76
+ return { action: 'react', args: { ...env, emoji: '' } };
77
+ return { action, args: env };
78
+ }
79
+
80
+ export function normalizeXmtp(action: string, env: Args): Normalized {
81
+ if (action === 'unreact') {
82
+ return { action: 'react', args: { ...env, action: 'removed' } };
83
+ }
84
+ if (action === 'read') {
85
+ return { action: 'query', args: { line: env.line, limit: env.limit } };
86
+ }
87
+ return { action, args: env };
88
+ }
@@ -0,0 +1,35 @@
1
+ import type { Station } from './types.js';
2
+ import { Line } from './lines.js';
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,64 @@
1
+ import { serializeTrainError, type TrainErrorInfo } from '../daemon/train-error.js';
2
+ import type { Normalized } from './messaging-normalize.js';
3
+
4
+ export const emit = (e: unknown): void =>
5
+ void process.stdout.write(JSON.stringify(e) + '\n');
6
+
7
+ export const respond = (
8
+ id: string,
9
+ body: { result?: unknown; error?: string; errorInfo?: TrainErrorInfo },
10
+ ): void =>
11
+ void process.stdout.write(
12
+ JSON.stringify({ op: 'response', id, ...body }) + '\n',
13
+ );
14
+
15
+ export const mintId = (): string =>
16
+ `msg_${Math.random().toString(36).slice(2, 10)}`;
17
+
18
+ type Args = Record<string, unknown>;
19
+
20
+ export interface CallMsg {
21
+ op: 'call';
22
+ id: string;
23
+ action: string;
24
+ args: Args;
25
+ }
26
+
27
+ export type StationHandler = (id: string, args: Args) => void | Promise<void>;
28
+
29
+ export interface StationConfig {
30
+ handlers: Record<string, StationHandler>;
31
+ normalize: (action: string, args: Args) => Normalized;
32
+ preDispatch?: (id: string, action: string) => boolean;
33
+ }
34
+
35
+ const lineTag = (args: Args): string =>
36
+ typeof args.line === 'string' ? args.line : '?';
37
+
38
+ export function makeStation({ handlers, normalize, preDispatch }: StationConfig) {
39
+ const known = Object.keys(handlers).join(', ');
40
+ return async function handleCall(msg: CallMsg): Promise<void> {
41
+ const { id } = msg;
42
+ let action = msg.action;
43
+ let args: Args = msg.args;
44
+ try {
45
+ ({ action, args } = normalize(msg.action, msg.args));
46
+ emit({ op: 'log', text: `call ${action} recv (line=${lineTag(args)})` });
47
+ if (preDispatch?.(id, action)) return;
48
+ const handler = handlers[action];
49
+ if (!handler) {
50
+ respond(id, { error: `unknown action '${action}' (have: ${known})` });
51
+ return;
52
+ }
53
+ await handler(id, args);
54
+ emit({ op: 'log', text: `call ${action} done (line=${lineTag(args)})` });
55
+ } catch (err) {
56
+ const info = serializeTrainError(err);
57
+ emit({
58
+ op: 'log',
59
+ text: `call ${action} FAILED (line=${lineTag(args)}): ${info.error}`,
60
+ });
61
+ respond(id, info);
62
+ }
63
+ };
64
+ }