@stage-labs/metro 0.1.0-beta.3 → 0.1.0-beta.30

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 (236) hide show
  1. package/dist/api.js +131 -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 +104 -0
  6. package/dist/claude.js +15 -0
  7. package/dist/cli.js +157 -253
  8. package/dist/control.js +158 -0
  9. package/dist/plugin.js +82 -0
  10. package/dist/prompt.js +61 -0
  11. package/dist/runtime.js +94 -0
  12. package/dist/store.js +70 -0
  13. package/dist/tail.js +120 -0
  14. package/dist/update.js +33 -0
  15. package/dist/version.js +97 -0
  16. package/package.json +34 -21
  17. package/runtime/node_modules/@metro-labs/discord-bot/package.json +28 -0
  18. package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +119 -0
  19. package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +388 -0
  20. package/runtime/node_modules/@metro-labs/discord-bot/src/api-base.ts +1 -0
  21. package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +29 -0
  22. package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +210 -0
  23. package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +50 -0
  24. package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +115 -0
  25. package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +127 -0
  26. package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +215 -0
  27. package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +43 -0
  28. package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +19 -0
  29. package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +82 -0
  30. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-speak.ts +195 -0
  31. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-transcribe.ts +292 -0
  32. package/runtime/node_modules/@metro-labs/discord-bot/src/voice.ts +197 -0
  33. package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +5 -0
  34. package/runtime/node_modules/@metro-labs/mcp/package.json +66 -0
  35. package/runtime/node_modules/@metro-labs/mcp/src/channels/dedupe.ts +24 -0
  36. package/runtime/node_modules/@metro-labs/mcp/src/channels/inbound.ts +367 -0
  37. package/runtime/node_modules/@metro-labs/mcp/src/channels/media-note.ts +85 -0
  38. package/runtime/node_modules/@metro-labs/mcp/src/channels/pending.ts +75 -0
  39. package/runtime/node_modules/@metro-labs/mcp/src/channels/relay.ts +129 -0
  40. package/runtime/node_modules/@metro-labs/mcp/src/channels/webhook-note.ts +51 -0
  41. package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +311 -0
  42. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +337 -0
  43. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-error.ts +8 -0
  44. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +129 -0
  45. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +99 -0
  46. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-owner.ts +32 -0
  47. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +129 -0
  48. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +278 -0
  49. package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +321 -0
  50. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +123 -0
  51. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair.ts +20 -0
  52. package/runtime/node_modules/@metro-labs/mcp/src/daemon/collection-api.ts +202 -0
  53. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +339 -0
  54. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-json.ts +21 -0
  55. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth-routes.ts +157 -0
  56. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +127 -0
  57. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +100 -0
  58. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +57 -0
  59. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +333 -0
  60. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cors.ts +29 -0
  61. package/runtime/node_modules/@metro-labs/mcp/src/daemon/crash-guard.ts +49 -0
  62. package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +117 -0
  63. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-auth.ts +164 -0
  64. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-oauth.ts +208 -0
  65. package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +359 -0
  66. package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +79 -0
  67. package/runtime/node_modules/@metro-labs/mcp/src/daemon/log.ts +42 -0
  68. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +162 -0
  69. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +120 -0
  70. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +65 -0
  71. package/runtime/node_modules/@metro-labs/mcp/src/daemon/pair-codes.ts +58 -0
  72. package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +134 -0
  73. package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +214 -0
  74. package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +224 -0
  75. package/runtime/node_modules/@metro-labs/mcp/src/daemon/relay.ts +302 -0
  76. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +130 -0
  77. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-pair.ts +18 -0
  78. package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-source.ts +105 -0
  79. package/runtime/node_modules/@metro-labs/mcp/src/daemon/secure-fs.ts +57 -0
  80. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +27 -0
  81. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +43 -0
  82. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +218 -0
  83. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor-io.ts +97 -0
  84. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor.ts +323 -0
  85. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-call.ts +22 -0
  86. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-error.ts +53 -0
  87. package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +169 -0
  88. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +295 -0
  89. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-store.ts +181 -0
  90. package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +91 -0
  91. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +215 -0
  92. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-map.ts +71 -0
  93. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-scope.ts +64 -0
  94. package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +37 -0
  95. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-collections.ts +218 -0
  96. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +147 -0
  97. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +126 -0
  98. package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +308 -0
  99. package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +19 -0
  100. package/runtime/node_modules/@metro-labs/mcp/src/db/key-map.ts +45 -0
  101. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +382 -0
  102. package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +300 -0
  103. package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +31 -0
  104. package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +105 -0
  105. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +131 -0
  106. package/runtime/node_modules/@metro-labs/mcp/src/db/token-carry.ts +45 -0
  107. package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +57 -0
  108. package/runtime/node_modules/@metro-labs/mcp/src/db/whatsapp-login.ts +18 -0
  109. package/runtime/node_modules/@metro-labs/mcp/src/mcp/accounts.ts +145 -0
  110. package/runtime/node_modules/@metro-labs/mcp/src/mcp/call-tools.ts +203 -0
  111. package/runtime/node_modules/@metro-labs/mcp/src/mcp/channel-owner.ts +29 -0
  112. package/runtime/node_modules/@metro-labs/mcp/src/mcp/ctx.ts +62 -0
  113. package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +91 -0
  114. package/runtime/node_modules/@metro-labs/mcp/src/mcp/frame-scope.ts +21 -0
  115. package/runtime/node_modules/@metro-labs/mcp/src/mcp/group-tools.ts +129 -0
  116. package/runtime/node_modules/@metro-labs/mcp/src/mcp/index.ts +192 -0
  117. package/runtime/node_modules/@metro-labs/mcp/src/mcp/member-tools.ts +72 -0
  118. package/runtime/node_modules/@metro-labs/mcp/src/mcp/permission-relay.ts +66 -0
  119. package/runtime/node_modules/@metro-labs/mcp/src/mcp/raw-get-stream.ts +207 -0
  120. package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +73 -0
  121. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-registry.ts +172 -0
  122. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +43 -0
  123. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session.ts +211 -0
  124. package/runtime/node_modules/@metro-labs/mcp/src/mcp/str.ts +5 -0
  125. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-dispatch.ts +212 -0
  126. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +397 -0
  127. package/runtime/node_modules/@metro-labs/mcp/src/mcp/upload-tool.ts +66 -0
  128. package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +240 -0
  129. package/runtime/node_modules/@metro-labs/mcp/src/server.ts +2 -0
  130. package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +80 -0
  131. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-inline.ts +100 -0
  132. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +161 -0
  133. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +200 -0
  134. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-xmtp.ts +111 -0
  135. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +182 -0
  136. package/runtime/node_modules/@metro-labs/mcp/src/stations/attachments.ts +196 -0
  137. package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +82 -0
  138. package/runtime/node_modules/@metro-labs/mcp/src/stations/messaging-normalize.ts +88 -0
  139. package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +35 -0
  140. package/runtime/node_modules/@metro-labs/mcp/src/stations/station-runtime.ts +64 -0
  141. package/runtime/node_modules/@metro-labs/mcp/src/stations/types.ts +107 -0
  142. package/runtime/node_modules/@metro-labs/telegram/package.json +28 -0
  143. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +81 -0
  144. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +254 -0
  145. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +87 -0
  146. package/runtime/node_modules/@metro-labs/telegram/src/client.ts +77 -0
  147. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +141 -0
  148. package/runtime/node_modules/@metro-labs/telegram/src/group.ts +261 -0
  149. package/runtime/node_modules/@metro-labs/telegram/src/history.ts +52 -0
  150. package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +78 -0
  151. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +46 -0
  152. package/runtime/node_modules/@metro-labs/telegram/src/login.ts +166 -0
  153. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +80 -0
  154. package/runtime/node_modules/@metro-labs/telegram/src/members.ts +75 -0
  155. package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +22 -0
  156. package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +167 -0
  157. package/runtime/node_modules/@metro-labs/telegram/src/station.ts +24 -0
  158. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +7 -0
  159. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -0
  160. package/runtime/node_modules/@metro-labs/telegram-bot/package.json +25 -0
  161. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +120 -0
  162. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +295 -0
  163. package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
  164. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +172 -0
  165. package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +103 -0
  166. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
  167. package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
  168. package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +17 -0
  169. package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +48 -0
  170. package/runtime/node_modules/@metro-labs/telegram-bot/src/verify.ts +38 -0
  171. package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +3 -0
  172. package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
  173. package/runtime/node_modules/@metro-labs/webhook/src/station.ts +40 -0
  174. package/runtime/node_modules/@metro-labs/whatsapp/package.json +29 -0
  175. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +67 -0
  176. package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
  177. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +178 -0
  178. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
  179. package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
  180. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +318 -0
  181. package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
  182. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +132 -0
  183. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
  184. package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
  185. package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
  186. package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
  187. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
  188. package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
  189. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
  190. package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
  191. package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +141 -0
  192. package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +17 -0
  193. package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
  194. package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
  195. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
  196. package/runtime/node_modules/@metro-labs/xmtp/package.json +33 -0
  197. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +113 -0
  198. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
  199. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
  200. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
  201. package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
  202. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +375 -0
  203. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
  204. package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
  205. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
  206. package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
  207. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +333 -0
  208. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +78 -0
  209. package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
  210. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
  211. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +33 -0
  212. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
  213. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
  214. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
  215. package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
  216. package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +13 -0
  217. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
  218. package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
  219. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
  220. package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +84 -0
  221. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +39 -0
  222. package/runtime/runtime.json +3 -0
  223. package/runtime/trains/.keep +0 -0
  224. package/LICENSE +0 -21
  225. package/README.md +0 -114
  226. package/dist/agents/claude.js +0 -229
  227. package/dist/agents/codex.js +0 -282
  228. package/dist/agents/types.js +0 -3
  229. package/dist/channels/discord.js +0 -142
  230. package/dist/channels/telegram.js +0 -209
  231. package/dist/lib/dotenv.js +0 -31
  232. package/dist/lib/scope-cache.js +0 -85
  233. package/dist/lib/streaming.js +0 -207
  234. package/dist/log.js +0 -12
  235. package/dist/orchestrator.js +0 -399
  236. package/dist/paths.js +0 -33
@@ -0,0 +1,197 @@
1
+ import { errMsg } from '@metro-labs/mcp/log';
2
+ import {
3
+ joinVoiceChannel,
4
+ getVoiceConnection,
5
+ VoiceConnectionStatus,
6
+ entersState,
7
+ type VoiceConnection,
8
+ } from '@discordjs/voice';
9
+ import type { Client, VoiceBasedChannel } from 'discord.js';
10
+ import { accountFor, accounts } from './accounts.js';
11
+ import { respond } from './wire.js';
12
+ import {
13
+ startTranscription,
14
+ stopTranscription,
15
+ setTranscription,
16
+ } from './voice-transcribe.js';
17
+
18
+ const DEFAULT_USERNAME = 'bonustrack_';
19
+
20
+ function clientFor(account?: string): { accountId: string; client: Client } {
21
+ const accountId = accountFor({ account });
22
+ const acct = accounts.get(accountId);
23
+ if (!acct) throw new Error(`unknown account '${accountId}'`);
24
+ return { accountId, client: acct.client };
25
+ }
26
+
27
+ function voiceStateMatches(
28
+ vs: { id: string; member?: { user: { username?: string } } | null },
29
+ opts: { userId?: string; username?: string },
30
+ ): boolean {
31
+ if (opts.userId && vs.id === opts.userId) return true;
32
+ return Boolean(
33
+ opts.username &&
34
+ vs.member?.user.username?.toLowerCase() === opts.username.toLowerCase(),
35
+ );
36
+ }
37
+
38
+ function findUserVoiceChannel(
39
+ client: Client,
40
+ opts: { userId?: string; username?: string },
41
+ ): VoiceBasedChannel | null {
42
+ for (const guild of client.guilds.cache.values()) {
43
+ for (const vs of guild.voiceStates.cache.values()) {
44
+ if (!vs.channelId) continue;
45
+ if (voiceStateMatches(vs, opts)) return vs.channel ?? null;
46
+ }
47
+ }
48
+ return null;
49
+ }
50
+
51
+ async function resolveTarget(
52
+ client: Client,
53
+ args: {
54
+ guildId?: string;
55
+ channelId?: string;
56
+ userId?: string;
57
+ username?: string;
58
+ },
59
+ ): Promise<VoiceBasedChannel> {
60
+ if (args.channelId) {
61
+ const ch = await client.channels.fetch(args.channelId);
62
+ if (!ch || !('guild' in ch) || !ch.isVoiceBased()) {
63
+ throw new Error(`channel ${args.channelId} is not a voice channel`);
64
+ }
65
+ return ch;
66
+ }
67
+ const ch = findUserVoiceChannel(client, {
68
+ userId: args.userId,
69
+ username: args.userId ? undefined : (args.username ?? DEFAULT_USERNAME),
70
+ });
71
+ if (!ch) {
72
+ const who = args.userId ?? args.username ?? DEFAULT_USERNAME;
73
+ throw new Error(
74
+ `could not find a voice channel for '${who}' — ` +
75
+ 'is the user connected to voice in a guild the bot shares, and is the ' +
76
+ 'GuildVoiceStates intent enabled? (full daemon restart needed after intent change)',
77
+ );
78
+ }
79
+ return ch;
80
+ }
81
+
82
+ export async function joinVoice(
83
+ id: string,
84
+ rawArgs: Record<string, unknown>,
85
+ ): Promise<void> {
86
+ const args = rawArgs as {
87
+ account?: string;
88
+ guildId?: string;
89
+ channelId?: string;
90
+ userId?: string;
91
+ username?: string;
92
+ };
93
+ const { accountId, client } = clientFor(args.account);
94
+ if (!client.isReady()) {
95
+ respond(id, { error: `gateway not ready for '${accountId}'` });
96
+ return;
97
+ }
98
+
99
+ const channel = await resolveTarget(client, args);
100
+ const guild = channel.guild;
101
+
102
+ const connection: VoiceConnection = joinVoiceChannel({
103
+ channelId: channel.id,
104
+ guildId: guild.id,
105
+ adapterCreator: guild.voiceAdapterCreator,
106
+ selfDeaf: false,
107
+ selfMute: true,
108
+ });
109
+
110
+ try {
111
+ await entersState(connection, VoiceConnectionStatus.Ready, 15_000);
112
+ } catch (err) {
113
+ connection.destroy();
114
+ throw new Error(
115
+ `voice connection failed to become Ready: ${errMsg(err)}`,
116
+ );
117
+ }
118
+
119
+ try {
120
+ startTranscription(guild.id, channel.id, accountId, client, connection);
121
+ } catch (err) {
122
+ process.stderr.write(
123
+ `voice transcription arm failed: ${errMsg(err)}\n`,
124
+ );
125
+ }
126
+
127
+ respond(id, {
128
+ result: {
129
+ ok: true,
130
+ account: accountId,
131
+ guildId: guild.id,
132
+ guildName: guild.name,
133
+ channelId: channel.id,
134
+ channelName: channel.name,
135
+ status: connection.state.status,
136
+ transcribing: true,
137
+ },
138
+ });
139
+ }
140
+
141
+ export function voiceDebug(id: string, rawArgs: Record<string, unknown>): void {
142
+ const args = rawArgs as { account?: string };
143
+ const { accountId, client } = clientFor(args.account);
144
+ const guilds = [...client.guilds.cache.values()].map((g) => {
145
+ const voiceChannels = [...g.channels.cache.values()]
146
+ .filter((c) => c.isVoiceBased())
147
+ .map((c) => ({ id: c.id, name: c.name }));
148
+ const occupants = [...g.voiceStates.cache.values()]
149
+ .filter((vs) => vs.channelId)
150
+ .map((vs) => ({
151
+ channelId: vs.channelId,
152
+ channelName: vs.channel?.name ?? null,
153
+ userId: vs.id,
154
+ username: vs.member?.user.username ?? null,
155
+ }));
156
+ return { id: g.id, name: g.name, voiceChannels, occupants };
157
+ });
158
+ respond(id, {
159
+ result: { ok: true, account: accountId, ready: client.isReady(), guilds },
160
+ });
161
+ }
162
+
163
+ export function leaveVoice(id: string, rawArgs: Record<string, unknown>): void {
164
+ const args = rawArgs as { account?: string; guildId?: string };
165
+ const { accountId, client } = clientFor(args.account);
166
+
167
+ const guildIds = args.guildId
168
+ ? [args.guildId]
169
+ : [...client.guilds.cache.keys()];
170
+ const left: string[] = [];
171
+ for (const gid of guildIds) {
172
+ const conn = getVoiceConnection(gid);
173
+ if (conn) {
174
+ stopTranscription(gid);
175
+ conn.destroy();
176
+ left.push(gid);
177
+ }
178
+ }
179
+ respond(id, { result: { ok: true, account: accountId, leftGuilds: left } });
180
+ }
181
+
182
+ export function voiceTranscribe(
183
+ id: string,
184
+ rawArgs: Record<string, unknown>,
185
+ ): void {
186
+ const args = rawArgs as { account?: string; guildId?: string; on?: boolean };
187
+ const { accountId, client } = clientFor(args.account);
188
+ const on = args.on !== false;
189
+ const guildIds = args.guildId
190
+ ? [args.guildId]
191
+ : [...client.guilds.cache.keys()];
192
+ const toggled: string[] = [];
193
+ for (const gid of guildIds) if (setTranscription(gid, on)) toggled.push(gid);
194
+ respond(id, {
195
+ result: { ok: true, account: accountId, on, toggledGuilds: toggled },
196
+ });
197
+ }
@@ -0,0 +1,5 @@
1
+ export { emit, respond, mintId } from '@metro-labs/mcp/stations/station-runtime';
2
+
3
+ export const SELF_URI =
4
+ process.env.METRO_SELF_URI ??
5
+ 'metro://claude/user/8a1857f3-4039-4da6-a4e1-611b432d2082';
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@metro-labs/mcp",
3
+ "version": "0.1.0-beta.15",
4
+ "private": true,
5
+ "description": "Event-interception wire. Supervises train subprocesses in ~/.metro/trains/, multiplexes their JSON event stream onto stdout, and routes outbound action calls back via stdin. Per-platform code is written by the agent (or user) as train scripts — metro core is pure transport.",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/bonustrack/metro.git"
10
+ },
11
+ "homepage": "https://github.com/bonustrack/metro#readme",
12
+ "bugs": "https://github.com/bonustrack/metro/issues",
13
+ "type": "module",
14
+ "engines": {
15
+ "node": ">=22"
16
+ },
17
+ "bin": {
18
+ "metro-daemon": "./dist/server.js"
19
+ },
20
+ "exports": {
21
+ ".": "./src/mcp/index.ts",
22
+ "./server": "./dist/server.js",
23
+ "./log": "./src/daemon/log.ts",
24
+ "./train-error": "./src/daemon/train-error.ts",
25
+ "./secure-fs": "./src/daemon/secure-fs.ts",
26
+ "./lines": "./src/stations/lines.ts",
27
+ "./events": "./src/daemon/events.ts",
28
+ "./endpoints": "./src/daemon/tunnel.ts",
29
+ "./trains/protocol": "./src/daemon/protocol.ts",
30
+ "./stations/types": "./src/stations/types.ts",
31
+ "./stations/station-runtime": "./src/stations/station-runtime.ts",
32
+ "./stations/account-store": "./src/stations/account-store.ts",
33
+ "./stations/attachments": "./src/stations/attachments.ts",
34
+ "./stations/messaging-normalize": "./src/stations/messaging-normalize.ts",
35
+ "./db/whatsapp-login": "./src/db/whatsapp-login.ts"
36
+ },
37
+ "scripts": {
38
+ "start": "bun src/server.ts",
39
+ "build": "tsc",
40
+ "db:generate": "drizzle-kit generate",
41
+ "db:migrate": "drizzle-kit migrate",
42
+ "test": "tsc --noEmit && DYLD_FALLBACK_LIBRARY_PATH=/usr/lib METRO_STATE_DIR=\"$(mktemp -d /tmp/metro-test.XXXXXX)\" bun test test/"
43
+ },
44
+ "dependencies": {
45
+ "@metro-labs/discord-bot": "workspace:*",
46
+ "@metro-labs/telegram": "workspace:*",
47
+ "@metro-labs/telegram-bot": "workspace:*",
48
+ "@metro-labs/webhook": "workspace:*",
49
+ "@metro-labs/whatsapp": "workspace:*",
50
+ "@metro-labs/xmtp": "workspace:*",
51
+ "@modelcontextprotocol/sdk": "^1.12.0",
52
+ "drizzle-kit": "^0.31.0",
53
+ "drizzle-orm": "^0.44.0",
54
+ "pino": "^9.5.0",
55
+ "pino-pretty": "^13.1.3",
56
+ "postgres": "^3.4.5",
57
+ "zod": "^3.23.8"
58
+ },
59
+ "devDependencies": {
60
+ "@stage-labs/config": "0.1.0-beta.2",
61
+ "eslint": "^10.3.0",
62
+ "knip": "^6.15.0",
63
+ "madge": "^8.0.0",
64
+ "typescript": "^5"
65
+ }
66
+ }
@@ -0,0 +1,24 @@
1
+ import { accountFromLine, agentIdForAccount } from '../db/agent-map.js';
2
+
3
+ const accountStrippedLine = (line: string): string => {
4
+ const parts = line.split('/');
5
+ if (parts.length < 5) return line;
6
+ return [parts[0], parts[1], parts[2], ...parts.slice(4)].join('/');
7
+ };
8
+
9
+ export const dedupeOwner = (line: string): string => {
10
+ const acct = accountFromLine(line);
11
+ if (!acct) return 'unowned';
12
+ const agentId = agentIdForAccount(acct.station, acct.accountId);
13
+ return agentId === undefined
14
+ ? `account:${acct.accountId}`
15
+ : `agent:${agentId}`;
16
+ };
17
+
18
+ export const dedupeKey = (
19
+ station: string,
20
+ line: string,
21
+ kind: string,
22
+ messageId: string,
23
+ ): string =>
24
+ `${dedupeOwner(line)} ${station} ${accountStrippedLine(line)} ${kind} ${messageId}`;
@@ -0,0 +1,367 @@
1
+ import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { str } from '../mcp/str.js';
3
+ import { dedupeKey } from './dedupe.js';
4
+ import {
5
+ buildMediaFailureNote,
6
+ buildMediaNote,
7
+ type MediaNote,
8
+ type SavedMedia,
9
+ } from './media-note.js';
10
+ import { buildWebhookNote } from './webhook-note.js';
11
+ import {
12
+ capSet,
13
+ displayNameMeta,
14
+ senderMeta,
15
+ tsMeta,
16
+ takeMediaCtx,
17
+ type MediaCtx,
18
+ type PendingAtt,
19
+ type PendingMsg,
20
+ } from './pending.js';
21
+
22
+ interface InboundDeps {
23
+ mcp: Server;
24
+ log: (...a: unknown[]) => void;
25
+ getStations: () => Set<string>;
26
+ senderAllowed: (from: string, line: string) => boolean;
27
+ }
28
+
29
+ const ATTACH_TIMEOUT_MS = 15_000;
30
+ const DEDUPE_TTL_MS = 30_000;
31
+ const DEDUPE_MAX = 2_000;
32
+ const ALLOWED_LINES_MAX = 2_000;
33
+ const PENDING_PERMISSIONS_MAX = 500;
34
+
35
+ const PERMISSION_REPLY_RE = /^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i;
36
+
37
+ const shortId = (id: string): string =>
38
+ id.length > 10 ? `${id.slice(0, 6)}…` : id;
39
+
40
+ export class InboundRelay {
41
+ private readonly deps: InboundDeps;
42
+ private readonly pendingAttachments = new Map<string, PendingMsg>();
43
+ private readonly seenEvents = new Map<string, number>();
44
+ private readonly allowedLines = new Set<string>();
45
+ private readonly pendingPermissions = new Set<string>();
46
+ private lastLine: string | undefined;
47
+
48
+ constructor(deps: InboundDeps) {
49
+ this.deps = deps;
50
+ }
51
+
52
+ get knownLine(): string | undefined {
53
+ return this.lastLine;
54
+ }
55
+
56
+ registerPermission(requestId: string): void {
57
+ this.pendingPermissions.add(requestId);
58
+ capSet(this.pendingPermissions, PENDING_PERMISSIONS_MAX);
59
+ }
60
+
61
+ private notify(method: string, params: Record<string, unknown>): Promise<void> {
62
+ return this.deps.mcp.notification({ method, params });
63
+ }
64
+
65
+ private isDuplicate(
66
+ station: string,
67
+ line: string,
68
+ kind: string,
69
+ messageId: string,
70
+ ): boolean {
71
+ if (!messageId) return false;
72
+ const key = dedupeKey(station, line, kind, messageId);
73
+ const now = Date.now();
74
+ if (this.seenEvents.size >= DEDUPE_MAX) {
75
+ for (const [k, t] of this.seenEvents) {
76
+ if (now - t > DEDUPE_TTL_MS) this.seenEvents.delete(k);
77
+ }
78
+ }
79
+ const prev = this.seenEvents.get(key);
80
+ if (prev !== undefined && now - prev < DEDUPE_TTL_MS) return true;
81
+ this.seenEvents.set(key, now);
82
+ return false;
83
+ }
84
+
85
+ private surfaceNote(
86
+ ctx: MediaCtx,
87
+ p: SavedMedia,
88
+ note: MediaNote,
89
+ extra: Record<string, unknown>,
90
+ ): Promise<void> {
91
+ return this.notify('notifications/claude/channel', {
92
+ content: note.content,
93
+ meta: {
94
+ line: ctx.line,
95
+ from: ctx.from,
96
+ station: ctx.station,
97
+ ...senderMeta(ctx),
98
+ kind: note.kind,
99
+ mime: p.mime ?? '',
100
+ name: note.name,
101
+ ...extra,
102
+ },
103
+ });
104
+ }
105
+
106
+ private async surfaceMedia(ctx: MediaCtx, p: SavedMedia): Promise<void> {
107
+ const note = await buildMediaNote(p, ctx.text ?? '');
108
+ if (!note) return;
109
+ await this.surfaceNote(ctx, p, note, {
110
+ ...(p.url ? { url: p.url } : {}),
111
+ local_path: note.path,
112
+ });
113
+ }
114
+
115
+ private surfaceMediaFailure(ctx: MediaCtx, p: SavedMedia): Promise<void> {
116
+ return this.surfaceNote(ctx, p, buildMediaFailureNote(p, ctx.text ?? ''), {
117
+ attachment_error: p.reason ?? '',
118
+ });
119
+ }
120
+
121
+ private async flushPendingFallback(id: string): Promise<void> {
122
+ const e = this.pendingAttachments.get(id);
123
+ if (!e) return;
124
+ this.pendingAttachments.delete(id);
125
+ const missing = e.attachments.filter((_, i) => !e.saved.has(i));
126
+ if (!missing.length) return;
127
+ const names = missing
128
+ .map((a) => a.name ?? a.kind ?? 'attachment')
129
+ .join(', ');
130
+ await this.notify('notifications/claude/channel', {
131
+ content:
132
+ (e.text ? `${e.text}\n` : '') +
133
+ `[attachment(s) could not be fetched in time: ${names}]`,
134
+ meta: {
135
+ line: e.line,
136
+ from: e.from,
137
+ station: e.station,
138
+ ...tsMeta(e.ts),
139
+ message_id: e.messageId,
140
+ line_name: e.lineName,
141
+ from_name: e.fromName,
142
+ ...displayNameMeta(e.fromDisplayName),
143
+ },
144
+ });
145
+ }
146
+
147
+ private mediaCtxFor(
148
+ ev: Record<string, unknown>,
149
+ payload: SavedMedia,
150
+ ): MediaCtx | null {
151
+ const forId = str(payload.attachmentFor);
152
+ const buf = forId ? this.pendingAttachments.get(forId) : undefined;
153
+ if (buf) {
154
+ buf.saved.add(typeof payload.index === 'number' ? payload.index : 0);
155
+ const ctx = takeMediaCtx(buf);
156
+ if (buf.saved.size >= buf.attachments.length) {
157
+ clearTimeout(buf.timer);
158
+ this.pendingAttachments.delete(forId);
159
+ }
160
+ return ctx;
161
+ }
162
+ const line = str(ev.line);
163
+ if (!line || !this.allowedLines.has(line)) return null;
164
+ return {
165
+ line,
166
+ from: 'metro://attachment',
167
+ station: str(ev.station) || 'xmtp',
168
+ ts: str(ev.ts),
169
+ };
170
+ }
171
+
172
+ private async routeAttachment(ev: Record<string, unknown>): Promise<boolean> {
173
+ const p = ev.payload as SavedMedia | undefined;
174
+ if (!p) return false;
175
+ const type = p.contentType;
176
+ if (type !== 'attachmentSaved' && type !== 'attachmentFailed') return false;
177
+ const ctx = this.mediaCtxFor(ev, p);
178
+ if (!ctx) return true;
179
+ if (type === 'attachmentFailed') await this.surfaceMediaFailure(ctx, p);
180
+ else await this.surfaceMedia(ctx, p);
181
+ return true;
182
+ }
183
+
184
+ private bufferAttachments(
185
+ ev: Record<string, unknown>,
186
+ base: { line: string; from: string; station: string; text: string },
187
+ atts: PendingAtt[],
188
+ ): void {
189
+ const id = str(ev.id);
190
+ if (!id) return;
191
+ const existing = this.pendingAttachments.get(id);
192
+ if (existing) clearTimeout(existing.timer);
193
+ this.pendingAttachments.set(id, {
194
+ ...base,
195
+ ts: str(ev.ts),
196
+ messageId: str(ev.messageId),
197
+ lineName: str(ev.lineName),
198
+ fromName: str(ev.fromName),
199
+ fromDisplayName: str(ev.fromDisplayName),
200
+ attachments: atts.map((a) => ({ kind: a.kind, name: a.name })),
201
+ saved: new Set<number>(),
202
+ timer: setTimeout(() => {
203
+ this.flushPendingFallback(id).catch((err: unknown) => {
204
+ this.deps.log(
205
+ 'inbound: attachment fallback not delivered',
206
+ 'id',
207
+ id,
208
+ 'line',
209
+ base.line,
210
+ err,
211
+ );
212
+ });
213
+ }, ATTACH_TIMEOUT_MS),
214
+ });
215
+ }
216
+
217
+ private async handleReact(
218
+ ev: Record<string, unknown>,
219
+ base: { line: string; from: string; station: string; text: string },
220
+ ): Promise<void> {
221
+ const re = ev.event as { emoji?: unknown; targetId?: string };
222
+ const emoji = reactionEmoji(re.emoji);
223
+ const target = re.targetId ?? str(ev.messageId);
224
+ const removed =
225
+ (ev.payload as { removed?: boolean } | undefined)?.removed === true ||
226
+ / \(removed\)\]?$/.test(base.text);
227
+ const verb = removed ? 'removed from' : 'reacted to';
228
+ const label = removed ? emoji || 'reaction' : emoji || 'reacted';
229
+ const content = `${label} ${verb} message ${shortId(target)}`.trim();
230
+ await this.notify('notifications/claude/channel', {
231
+ content,
232
+ meta: {
233
+ line: base.line,
234
+ from: base.from,
235
+ station: base.station,
236
+ ...tsMeta(ev.ts),
237
+ message_id: str(ev.messageId),
238
+ line_name: str(ev.lineName),
239
+ from_name: str(ev.fromName),
240
+ ...displayNameMeta(ev.fromDisplayName),
241
+ reaction: emoji,
242
+ target_id: target,
243
+ },
244
+ });
245
+ }
246
+
247
+ private async handlePermissionReply(text: string): Promise<boolean> {
248
+ const m = PERMISSION_REPLY_RE.exec(text);
249
+ if (m?.[1] === undefined || m[2] === undefined || !this.pendingPermissions.size)
250
+ return false;
251
+ const id = m[2].toLowerCase();
252
+ if (!this.pendingPermissions.has(id)) return false;
253
+ this.pendingPermissions.delete(id);
254
+ await this.notify('notifications/claude/channel/permission', {
255
+ request_id: id,
256
+ behavior: m[1].toLowerCase().startsWith('y') ? 'allow' : 'deny',
257
+ });
258
+ return true;
259
+ }
260
+
261
+ private droppedSender(from: string, line: string): boolean {
262
+ if (
263
+ from.startsWith('metro://claude') ||
264
+ from === 'metro://user' ||
265
+ !from.startsWith('metro://')
266
+ )
267
+ return true;
268
+ if (!this.deps.senderAllowed(from, line)) {
269
+ this.deps.log('drop: sender not allowed', from);
270
+ return true;
271
+ }
272
+ return false;
273
+ }
274
+
275
+ private routable(
276
+ ev: Record<string, unknown>,
277
+ replay: boolean,
278
+ ): EventBase | null {
279
+ const rawType = ev.event ? (ev.event as { type?: string }).type : 'msg';
280
+ const evType = rawType === 'reply' ? 'msg' : rawType;
281
+ if (evType !== 'msg' && evType !== 'react' && evType !== 'system')
282
+ return null;
283
+ const station = str(ev.station);
284
+ if (!this.deps.getStations().has(station)) return null;
285
+ const from = str(ev.from);
286
+ const line = str(ev.line);
287
+ if (this.droppedSender(from, line)) return null;
288
+ const text = str(ev.text);
289
+ if (!replay && this.isDuplicate(station, line, evType, str(ev.messageId))) {
290
+ this.deps.log(
291
+ 'drop: duplicate (per-account) event',
292
+ evType,
293
+ station,
294
+ str(ev.messageId),
295
+ );
296
+ return null;
297
+ }
298
+ return { evType, station, from, line, text };
299
+ }
300
+
301
+ private async emitMessage(
302
+ ev: Record<string, unknown>,
303
+ base: EventBase,
304
+ ): Promise<void> {
305
+ if (base.evType === 'msg' && (await this.handlePermissionReply(base.text)))
306
+ return;
307
+ await this.notify('notifications/claude/channel', {
308
+ content:
309
+ base.evType === 'system'
310
+ ? buildWebhookNote(base.text, str(ev.lineName), ev.payload)
311
+ : base.text,
312
+ meta: {
313
+ line: base.line,
314
+ from: base.from,
315
+ station: base.station,
316
+ ...tsMeta(ev.ts),
317
+ message_id: str(ev.messageId),
318
+ line_name: str(ev.lineName),
319
+ from_name: str(ev.fromName),
320
+ ...displayNameMeta(ev.fromDisplayName),
321
+ },
322
+ });
323
+ }
324
+
325
+ async handleEvent(
326
+ ev: Record<string, unknown>,
327
+ replay = false,
328
+ ): Promise<void> {
329
+ if (await this.routeAttachment(ev)) return;
330
+
331
+ const base = this.routable(ev, replay);
332
+ if (!base) return;
333
+ if (base.evType !== 'system') this.lastLine = base.line;
334
+ if (base.line) {
335
+ this.allowedLines.add(base.line);
336
+ capSet(this.allowedLines, ALLOWED_LINES_MAX);
337
+ }
338
+
339
+ const atts = (ev.payload as { attachments?: PendingAtt[] } | undefined)
340
+ ?.attachments;
341
+ if (Array.isArray(atts) && atts.length) {
342
+ this.bufferAttachments(ev, base, atts);
343
+ return;
344
+ }
345
+
346
+ if (base.evType === 'react') {
347
+ await this.handleReact(ev, base);
348
+ return;
349
+ }
350
+
351
+ await this.emitMessage(ev, base);
352
+ }
353
+ }
354
+
355
+ interface EventBase {
356
+ evType: string;
357
+ station: string;
358
+ from: string;
359
+ line: string;
360
+ text: string;
361
+ }
362
+
363
+ function reactionEmoji(raw: unknown): string {
364
+ if (typeof raw === 'string') return raw;
365
+ const obj = raw as { name?: string; reaction?: string } | undefined;
366
+ return obj?.name ?? obj?.reaction ?? '';
367
+ }