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

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 (235) 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 +106 -0
  6. package/dist/claude.js +15 -0
  7. package/dist/cli.js +164 -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 +93 -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 +332 -0
  43. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-connector-api.ts +150 -0
  44. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-pair.ts +20 -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 +152 -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 +129 -0
  50. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +278 -0
  51. package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +330 -0
  52. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +125 -0
  53. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +313 -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 +303 -0
  76. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +139 -0
  77. package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-source.ts +105 -0
  78. package/runtime/node_modules/@metro-labs/mcp/src/daemon/secure-fs.ts +57 -0
  79. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +27 -0
  80. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +48 -0
  81. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +218 -0
  82. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor-io.ts +97 -0
  83. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor.ts +323 -0
  84. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-call.ts +22 -0
  85. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-error.ts +53 -0
  86. package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +169 -0
  87. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +295 -0
  88. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-store.ts +181 -0
  89. package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +91 -0
  90. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +215 -0
  91. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-connectors.ts +143 -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-config.ts +147 -0
  96. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +126 -0
  97. package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +308 -0
  98. package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +19 -0
  99. package/runtime/node_modules/@metro-labs/mcp/src/db/key-map.ts +45 -0
  100. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +382 -0
  101. package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +300 -0
  102. package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +24 -0
  103. package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +105 -0
  104. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +117 -0
  105. package/runtime/node_modules/@metro-labs/mcp/src/db/token-carry.ts +45 -0
  106. package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +62 -0
  107. package/runtime/node_modules/@metro-labs/mcp/src/db/whatsapp-login.ts +18 -0
  108. package/runtime/node_modules/@metro-labs/mcp/src/mcp/accounts.ts +145 -0
  109. package/runtime/node_modules/@metro-labs/mcp/src/mcp/call-tools.ts +203 -0
  110. package/runtime/node_modules/@metro-labs/mcp/src/mcp/channel-owner.ts +29 -0
  111. package/runtime/node_modules/@metro-labs/mcp/src/mcp/ctx.ts +62 -0
  112. package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +91 -0
  113. package/runtime/node_modules/@metro-labs/mcp/src/mcp/frame-scope.ts +21 -0
  114. package/runtime/node_modules/@metro-labs/mcp/src/mcp/group-tools.ts +129 -0
  115. package/runtime/node_modules/@metro-labs/mcp/src/mcp/index.ts +192 -0
  116. package/runtime/node_modules/@metro-labs/mcp/src/mcp/member-tools.ts +72 -0
  117. package/runtime/node_modules/@metro-labs/mcp/src/mcp/permission-relay.ts +66 -0
  118. package/runtime/node_modules/@metro-labs/mcp/src/mcp/raw-get-stream.ts +207 -0
  119. package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +73 -0
  120. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-registry.ts +172 -0
  121. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +43 -0
  122. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session.ts +211 -0
  123. package/runtime/node_modules/@metro-labs/mcp/src/mcp/str.ts +5 -0
  124. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-dispatch.ts +212 -0
  125. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +397 -0
  126. package/runtime/node_modules/@metro-labs/mcp/src/mcp/upload-tool.ts +66 -0
  127. package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +240 -0
  128. package/runtime/node_modules/@metro-labs/mcp/src/server.ts +2 -0
  129. package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +80 -0
  130. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-inline.ts +100 -0
  131. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +161 -0
  132. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +200 -0
  133. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-xmtp.ts +111 -0
  134. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +182 -0
  135. package/runtime/node_modules/@metro-labs/mcp/src/stations/attachments.ts +196 -0
  136. package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +82 -0
  137. package/runtime/node_modules/@metro-labs/mcp/src/stations/messaging-normalize.ts +88 -0
  138. package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +35 -0
  139. package/runtime/node_modules/@metro-labs/mcp/src/stations/station-runtime.ts +64 -0
  140. package/runtime/node_modules/@metro-labs/mcp/src/stations/types.ts +107 -0
  141. package/runtime/node_modules/@metro-labs/telegram/package.json +28 -0
  142. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +81 -0
  143. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +254 -0
  144. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +87 -0
  145. package/runtime/node_modules/@metro-labs/telegram/src/client.ts +77 -0
  146. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +141 -0
  147. package/runtime/node_modules/@metro-labs/telegram/src/group.ts +261 -0
  148. package/runtime/node_modules/@metro-labs/telegram/src/history.ts +52 -0
  149. package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +78 -0
  150. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +46 -0
  151. package/runtime/node_modules/@metro-labs/telegram/src/login.ts +166 -0
  152. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +80 -0
  153. package/runtime/node_modules/@metro-labs/telegram/src/members.ts +75 -0
  154. package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +22 -0
  155. package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +167 -0
  156. package/runtime/node_modules/@metro-labs/telegram/src/station.ts +24 -0
  157. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +7 -0
  158. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -0
  159. package/runtime/node_modules/@metro-labs/telegram-bot/package.json +25 -0
  160. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +120 -0
  161. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +295 -0
  162. package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
  163. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +172 -0
  164. package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +103 -0
  165. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
  166. package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
  167. package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +17 -0
  168. package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +48 -0
  169. package/runtime/node_modules/@metro-labs/telegram-bot/src/verify.ts +38 -0
  170. package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +3 -0
  171. package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
  172. package/runtime/node_modules/@metro-labs/webhook/src/station.ts +40 -0
  173. package/runtime/node_modules/@metro-labs/whatsapp/package.json +29 -0
  174. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +67 -0
  175. package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
  176. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +178 -0
  177. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
  178. package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
  179. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +318 -0
  180. package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
  181. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +132 -0
  182. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
  183. package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
  184. package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
  185. package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
  186. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
  187. package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
  188. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
  189. package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
  190. package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +141 -0
  191. package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +17 -0
  192. package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
  193. package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
  194. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
  195. package/runtime/node_modules/@metro-labs/xmtp/package.json +33 -0
  196. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +113 -0
  197. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
  198. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
  199. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
  200. package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
  201. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +375 -0
  202. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
  203. package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
  204. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
  205. package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
  206. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +333 -0
  207. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +78 -0
  208. package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
  209. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
  210. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +33 -0
  211. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
  212. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
  213. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
  214. package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
  215. package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +13 -0
  216. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
  217. package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
  218. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
  219. package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +84 -0
  220. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +39 -0
  221. package/runtime/runtime.json +3 -0
  222. package/runtime/trains/.keep +0 -0
  223. package/LICENSE +0 -21
  224. package/README.md +0 -114
  225. package/dist/agents/claude.js +0 -229
  226. package/dist/agents/codex.js +0 -282
  227. package/dist/agents/types.js +0 -3
  228. package/dist/channels/discord.js +0 -142
  229. package/dist/channels/telegram.js +0 -209
  230. package/dist/lib/dotenv.js +0 -31
  231. package/dist/lib/scope-cache.js +0 -85
  232. package/dist/lib/streaming.js +0 -207
  233. package/dist/log.js +0 -12
  234. package/dist/orchestrator.js +0 -399
  235. package/dist/paths.js +0 -33
@@ -0,0 +1,43 @@
1
+ import { appendFile } from '@metro-labs/mcp/stations/attachments';
2
+
3
+ export interface OutgoingFile {
4
+ path: string;
5
+ name: string;
6
+ kind: string;
7
+ }
8
+
9
+ const basenameOf = (path: string, index: number): string =>
10
+ path.split('/').pop() ?? `file-${index}`;
11
+
12
+ export function outgoingFiles(
13
+ paths: string[],
14
+ names: string[] | undefined,
15
+ kinds: string[] | undefined,
16
+ ): OutgoingFile[] {
17
+ const out: OutgoingFile[] = [];
18
+ for (let i = 0; i < paths.length; i++) {
19
+ const path = paths[i];
20
+ if (path === undefined || path === '') continue;
21
+ const given = names?.[i];
22
+ out.push({
23
+ path,
24
+ name: given !== undefined && given !== '' ? given : basenameOf(path, i),
25
+ kind: kinds?.[i] ?? 'file',
26
+ });
27
+ }
28
+ return out;
29
+ }
30
+
31
+ export async function appendFiles(
32
+ form: FormData,
33
+ files: OutgoingFile[],
34
+ ): Promise<string[]> {
35
+ const delivered: string[] = [];
36
+ for (let i = 0; i < files.length; i++) {
37
+ const file = files[i];
38
+ if (!file) continue;
39
+ await appendFile(form, `files[${i}]`, file.path, file.name);
40
+ delivered.push(file.kind);
41
+ }
42
+ return delivered;
43
+ }
@@ -0,0 +1,19 @@
1
+ import type { GroupOp, Station, Verb } from '@metro-labs/mcp/stations/types';
2
+
3
+ export const discordBotStation: Station = {
4
+ name: 'discord-bot',
5
+ hasAccounts: true,
6
+ hasTrain: true,
7
+ messageVerbs: new Set<Verb>([
8
+ 'send',
9
+ 'reply',
10
+ 'react',
11
+ 'unreact',
12
+ 'edit',
13
+ 'delete',
14
+ 'read',
15
+ ]),
16
+ groupOps: new Set<GroupOp>(['create_group', 'add_members', 'remove_members']),
17
+ attachmentMode: 'canonical',
18
+ tools: [],
19
+ };
@@ -0,0 +1,82 @@
1
+ import { API } from './api-base.js';
2
+
3
+ export class DiscordTokenError extends Error {}
4
+
5
+ export interface DiscordBotIdentity {
6
+ userId: string;
7
+ username: string;
8
+ messageContent: boolean;
9
+ }
10
+
11
+ const MESSAGE_CONTENT = 1 << 18;
12
+ const MESSAGE_CONTENT_LIMITED = 1 << 19;
13
+ const TIMEOUT_MS = 10_000;
14
+
15
+ function botFetch(token: string, path: string): Promise<Response> {
16
+ return fetch(`${API}${path}`, {
17
+ headers: {
18
+ authorization: `Bot ${token}`,
19
+ 'user-agent': 'metro-daemon (https://github.com/bonustrack/metro)',
20
+ },
21
+ signal: AbortSignal.timeout(TIMEOUT_MS),
22
+ });
23
+ }
24
+
25
+ async function readJson(res: Response): Promise<Record<string, unknown>> {
26
+ try {
27
+ const body: unknown = await res.json();
28
+ return typeof body === 'object' && body !== null
29
+ ? (body as Record<string, unknown>)
30
+ : {};
31
+ } catch {
32
+ throw new DiscordTokenError('Discord returned a response Metro could not read');
33
+ }
34
+ }
35
+
36
+ async function selfUser(token: string): Promise<Record<string, unknown>> {
37
+ let res: Response;
38
+ try {
39
+ res = await botFetch(token, '/users/@me');
40
+ } catch {
41
+ throw new DiscordTokenError(
42
+ 'could not reach the Discord API to check that bot token',
43
+ );
44
+ }
45
+ if (res.status === 401 || res.status === 403)
46
+ throw new DiscordTokenError('Discord rejected that bot token');
47
+ if (!res.ok)
48
+ throw new DiscordTokenError(
49
+ `Discord answered ${res.status} while checking that bot token`,
50
+ );
51
+ return readJson(res);
52
+ }
53
+
54
+ async function messageContentEnabled(token: string): Promise<boolean> {
55
+ try {
56
+ const res = await botFetch(token, '/applications/@me');
57
+ if (!res.ok) return true;
58
+ const flags = (await readJson(res)).flags;
59
+ if (typeof flags !== 'number') return true;
60
+ return (flags & (MESSAGE_CONTENT | MESSAGE_CONTENT_LIMITED)) !== 0;
61
+ } catch {
62
+ return true;
63
+ }
64
+ }
65
+
66
+ export async function verifyDiscordBotToken(
67
+ token: string,
68
+ ): Promise<DiscordBotIdentity> {
69
+ const me = await selfUser(token);
70
+ const userId = typeof me.id === 'string' ? me.id : '';
71
+ if (userId === '')
72
+ throw new DiscordTokenError('Discord returned no bot identity for that token');
73
+ if (me.bot !== true)
74
+ throw new DiscordTokenError(
75
+ 'that token belongs to a Discord user, not a bot application',
76
+ );
77
+ return {
78
+ userId,
79
+ username: typeof me.username === 'string' ? me.username : '',
80
+ messageContent: await messageContentEnabled(token),
81
+ };
82
+ }
@@ -0,0 +1,195 @@
1
+ import { errMsg } from '@metro-labs/mcp/log';
2
+ import { spawn } from 'node:child_process';
3
+ import { unlink } from 'node:fs/promises';
4
+ import {
5
+ createAudioPlayer,
6
+ createAudioResource,
7
+ getVoiceConnection,
8
+ entersState,
9
+ AudioPlayerStatus,
10
+ StreamType,
11
+ NoSubscriberBehavior,
12
+ type VoiceConnection,
13
+ } from '@discordjs/voice';
14
+ import type { Client } from 'discord.js';
15
+ import { accountFor, accounts, routeOf } from './accounts.js';
16
+ import { respond } from './wire.js';
17
+
18
+ const TTS_VOICE = 'Samantha';
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 run(cmd: string, args: string[]): Promise<void> {
28
+ return new Promise((resolve, reject) => {
29
+ const p = spawn(cmd, args, { stdio: ['ignore', 'ignore', 'pipe'] });
30
+ let err = '';
31
+ p.stderr?.on('data', (d: Buffer) => {
32
+ err += d.toString();
33
+ });
34
+ p.on('error', reject);
35
+ p.on('close', (code) => {
36
+ if (code === 0) resolve();
37
+ else reject(new Error(`${cmd} exited ${code}: ${err.slice(-400)}`));
38
+ });
39
+ });
40
+ }
41
+
42
+ function channelName(ch: unknown, fallback: string): string {
43
+ return ch && typeof ch === 'object' && 'name' in ch
44
+ ? String((ch as { name?: string }).name)
45
+ : fallback;
46
+ }
47
+
48
+ function resolveChannelId(args: {
49
+ channelId?: string;
50
+ line?: string;
51
+ account?: string;
52
+ }): string | undefined {
53
+ if (args.channelId) return args.channelId;
54
+ if (!args.line) return undefined;
55
+ try {
56
+ return routeOf(args.line, args.account).channelId;
57
+ } catch {
58
+ return undefined;
59
+ }
60
+ }
61
+
62
+ function connByChannel(
63
+ client: Client,
64
+ channelId: string | undefined,
65
+ ): { conn: VoiceConnection; channelName: string } | null {
66
+ if (!channelId) return null;
67
+ const ch = client.channels.cache.get(channelId);
68
+ const gid =
69
+ ch && 'guildId' in ch ? (ch as { guildId: string }).guildId : undefined;
70
+ if (!gid) return null;
71
+ const conn = getVoiceConnection(gid);
72
+ if (!conn) return null;
73
+ return { conn, channelName: channelName(ch, gid) };
74
+ }
75
+
76
+ function connByGuildScan(
77
+ client: Client,
78
+ ): { conn: VoiceConnection; channelName: string } | null {
79
+ for (const guild of client.guilds.cache.values()) {
80
+ const conn = getVoiceConnection(guild.id);
81
+ if (conn) {
82
+ const chId = (conn.joinConfig as { channelId?: string }).channelId;
83
+ const ch = chId ? client.channels.cache.get(chId) : undefined;
84
+ return { conn, channelName: channelName(ch, guild.name) };
85
+ }
86
+ }
87
+ return null;
88
+ }
89
+
90
+ function resolveConnection(
91
+ client: Client,
92
+ args: { channelId?: string; line?: string; account?: string },
93
+ ): { conn: VoiceConnection; channelName: string } | { error: string } {
94
+ const channelId = resolveChannelId(args);
95
+ return (
96
+ connByChannel(client, channelId) ??
97
+ connByGuildScan(client) ?? {
98
+ error: 'no active voice connection — run joinVoice first',
99
+ }
100
+ );
101
+ }
102
+
103
+ export async function speak(
104
+ id: string,
105
+ rawArgs: Record<string, unknown>,
106
+ ): Promise<void> {
107
+ const args = rawArgs as {
108
+ account?: string;
109
+ text?: string;
110
+ channelId?: string;
111
+ line?: string;
112
+ voice?: string;
113
+ };
114
+ const text = (args.text ?? '').trim();
115
+ if (!text) {
116
+ respond(id, { result: { ok: false, error: 'text is required' } });
117
+ return;
118
+ }
119
+
120
+ const { accountId, client } = clientFor(args.account);
121
+ if (!client.isReady()) {
122
+ respond(id, {
123
+ result: { ok: false, error: `gateway not ready for '${accountId}'` },
124
+ });
125
+ return;
126
+ }
127
+
128
+ const resolved = resolveConnection(client, args);
129
+ if ('error' in resolved) {
130
+ respond(id, { result: { ok: false, error: resolved.error } });
131
+ return;
132
+ }
133
+ const { conn, channelName } = resolved;
134
+
135
+ const ts = Date.now();
136
+ const aiff = `/tmp/metro-tts-${ts}.aiff`;
137
+ const ogg = `/tmp/metro-tts-${ts}.ogg`;
138
+ const cleanup = async () => {
139
+ await Promise.allSettled([unlink(aiff), unlink(ogg)]);
140
+ };
141
+
142
+ try {
143
+ await run('say', ['-v', args.voice ?? TTS_VOICE, '-o', aiff, text]);
144
+ await run('ffmpeg', [
145
+ '-hide_banner',
146
+ '-loglevel',
147
+ 'error',
148
+ '-y',
149
+ '-i',
150
+ aiff,
151
+ '-ac',
152
+ '2',
153
+ '-ar',
154
+ '48000',
155
+ '-c:a',
156
+ 'libopus',
157
+ '-b:a',
158
+ '64k',
159
+ '-f',
160
+ 'ogg',
161
+ ogg,
162
+ ]);
163
+
164
+ const player = createAudioPlayer({
165
+ behaviors: { noSubscriber: NoSubscriberBehavior.Play },
166
+ });
167
+ const resource = createAudioResource(ogg, {
168
+ inputType: StreamType.OggOpus,
169
+ });
170
+ conn.subscribe(player);
171
+ try {
172
+ (conn as { rejoin: (c: object) => void }).rejoin({ selfMute: false });
173
+ } catch {
174
+ }
175
+ player.play(resource);
176
+
177
+ await entersState(player, AudioPlayerStatus.Playing, 10_000);
178
+ await entersState(player, AudioPlayerStatus.Idle, 120_000);
179
+ player.stop();
180
+ try {
181
+ (conn as { rejoin: (c: object) => void }).rejoin({ selfMute: true });
182
+ } catch {
183
+ }
184
+
185
+ respond(id, {
186
+ result: { ok: true, spoke: text, channelName, account: accountId },
187
+ });
188
+ } catch (err) {
189
+ respond(id, {
190
+ result: { ok: false, error: errMsg(err), account: accountId },
191
+ });
192
+ } finally {
193
+ await cleanup();
194
+ }
195
+ }
@@ -0,0 +1,292 @@
1
+ import { errMsg } from '@metro-labs/mcp/log';
2
+ import {
3
+ EndBehaviorType,
4
+ type VoiceConnection,
5
+ type VoiceReceiver,
6
+ } from '@discordjs/voice';
7
+ import type { Client } from 'discord.js';
8
+ import { spawn, type ChildProcess } from 'node:child_process';
9
+ import {
10
+ createWriteStream,
11
+ existsSync,
12
+ mkdtempSync,
13
+ readFileSync,
14
+ rmSync,
15
+ } from 'node:fs';
16
+ import { tmpdir } from 'node:os';
17
+ import { join } from 'node:path';
18
+ import { pipeline } from 'node:stream/promises';
19
+ import prism from 'prism-media';
20
+ import { emitInbound } from './format.js';
21
+ import { mintId } from './wire.js';
22
+
23
+ const WHISPER_BIN = process.env.WHISPER_CLI ?? 'whisper-cli';
24
+ const WHISPER_MODEL =
25
+ process.env.WHISPER_MODEL ??
26
+ join(process.env.HOME ?? '', '.whisper-models', 'ggml-base.en.bin');
27
+ const FFMPEG_BIN = process.env.FFMPEG_BIN ?? 'ffmpeg';
28
+ const MIN_PCM_BYTES = 0.5 * 48000 * 2 * 2;
29
+ const TRANSCRIBE_TIMEOUT_MS = 120_000;
30
+
31
+ function killAfter(p: ChildProcess, ms: number): () => void {
32
+ const t = setTimeout(() => {
33
+ p.kill('SIGKILL');
34
+ }, ms);
35
+ return () => {
36
+ clearTimeout(t);
37
+ };
38
+ }
39
+
40
+ interface Session {
41
+ receiver: VoiceReceiver;
42
+ accountId: string;
43
+ guildId: string;
44
+ channelId: string;
45
+ client: Client;
46
+ active: Set<string>;
47
+ tmp: string;
48
+ enabled: boolean;
49
+ onStart?: (userId: string) => void;
50
+ }
51
+
52
+ const sessions = new Map<string, Session>();
53
+
54
+ function run(bin: string, args: string[]): Promise<number> {
55
+ return new Promise((resolve, reject) => {
56
+ const p = spawn(bin, args, { stdio: ['ignore', 'ignore', 'ignore'] });
57
+ const cancel = killAfter(p, TRANSCRIBE_TIMEOUT_MS);
58
+ p.on('error', (e) => {
59
+ cancel();
60
+ reject(e);
61
+ });
62
+ p.on('close', (code) => {
63
+ cancel();
64
+ resolve(code ?? 0);
65
+ });
66
+ });
67
+ }
68
+
69
+ async function captureUtterance(
70
+ s: Session,
71
+ userId: string,
72
+ pcmPath: string,
73
+ ): Promise<number> {
74
+ const opus = s.receiver.subscribe(userId, {
75
+ end: { behavior: EndBehaviorType.AfterSilence, duration: 1000 },
76
+ });
77
+ const decoder = new prism.opus.Decoder({
78
+ rate: 48000,
79
+ channels: 2,
80
+ frameSize: 960,
81
+ });
82
+ let bytes = 0;
83
+ decoder.on('data', (c: Buffer) => {
84
+ bytes += c.length;
85
+ });
86
+ const out = createWriteStream(pcmPath);
87
+ await pipeline(opus, decoder, out);
88
+ return bytes;
89
+ }
90
+
91
+ async function pcmToWav(pcmPath: string, wavPath: string): Promise<void> {
92
+ await run(FFMPEG_BIN, [
93
+ '-hide_banner',
94
+ '-loglevel',
95
+ 'error',
96
+ '-y',
97
+ '-f',
98
+ 's16le',
99
+ '-ar',
100
+ '48000',
101
+ '-ac',
102
+ '2',
103
+ '-i',
104
+ pcmPath,
105
+ '-ar',
106
+ '16000',
107
+ '-ac',
108
+ '1',
109
+ wavPath,
110
+ ]);
111
+ }
112
+
113
+ function transcribe(wavPath: string): Promise<string> {
114
+ const ofBase = wavPath.replace(/\.wav$/, '');
115
+ return new Promise((resolve) => {
116
+ const p = spawn(
117
+ WHISPER_BIN,
118
+ [
119
+ '-m',
120
+ WHISPER_MODEL,
121
+ '-f',
122
+ wavPath,
123
+ '-otxt',
124
+ '-of',
125
+ ofBase,
126
+ '-np',
127
+ '-nt',
128
+ ],
129
+ { stdio: ['ignore', 'pipe', 'ignore'] },
130
+ );
131
+ const cancel = killAfter(p, TRANSCRIBE_TIMEOUT_MS);
132
+ let out = '';
133
+ p.stdout.on('data', (d: Buffer) => {
134
+ out += d.toString();
135
+ });
136
+ p.on('error', () => {
137
+ cancel();
138
+ resolve('');
139
+ });
140
+ p.on('close', () => {
141
+ cancel();
142
+ let text = out.trim();
143
+ if (!text && existsSync(`${ofBase}.txt`)) {
144
+ try {
145
+ text = readFileSync(`${ofBase}.txt`, 'utf8').trim();
146
+ } catch {
147
+ }
148
+ }
149
+ resolve(text);
150
+ });
151
+ });
152
+ }
153
+
154
+ const NOISE = new Set([
155
+ '',
156
+ '[blank_audio]',
157
+ '[silence]',
158
+ '[ Silence ]',
159
+ '(silence)',
160
+ 'you',
161
+ 'Thank you.',
162
+ ]);
163
+
164
+ function speakerName(s: Session, userId: string): string {
165
+ const member = s.client.guilds.cache
166
+ .get(s.guildId)
167
+ ?.members.cache.get(userId);
168
+ return member?.displayName ?? member?.user.username ?? userId;
169
+ }
170
+
171
+ function emitTranscript(
172
+ s: Session,
173
+ userId: string,
174
+ stamp: string,
175
+ text: string,
176
+ ): void {
177
+ emitInbound(s.accountId, {
178
+ kind: 'inbound',
179
+ id: mintId(),
180
+ ts: new Date().toISOString(),
181
+ station: 'discord-bot',
182
+ line: `metro://discord-bot/${s.accountId}/voice/${s.channelId}`,
183
+ line_name: 'voice',
184
+ from: `metro://discord-bot/${s.accountId}/user/${userId}`,
185
+ from_name: speakerName(s, userId),
186
+ message_id: stamp,
187
+ text,
188
+ is_private: false,
189
+ payload: {
190
+ kind: 'voice_transcript',
191
+ channel_id: s.channelId,
192
+ user_id: userId,
193
+ },
194
+ });
195
+ }
196
+
197
+ function cleanupFiles(pcmPath: string, wavPath: string): void {
198
+ for (const f of [pcmPath, wavPath, `${wavPath.replace(/\.wav$/, '')}.txt`]) {
199
+ try {
200
+ rmSync(f, { force: true });
201
+ } catch {
202
+ }
203
+ }
204
+ }
205
+
206
+ async function transcribeUtterance(
207
+ s: Session,
208
+ userId: string,
209
+ pcmPath: string,
210
+ wavPath: string,
211
+ stamp: string,
212
+ ): Promise<void> {
213
+ const bytes = await captureUtterance(s, userId, pcmPath);
214
+ if (bytes < MIN_PCM_BYTES) return;
215
+ await pcmToWav(pcmPath, wavPath);
216
+ const text = await transcribe(wavPath);
217
+ if (NOISE.has(text) || text.length < 2) return;
218
+ emitTranscript(s, userId, stamp, text);
219
+ }
220
+
221
+ async function handleSpeaker(s: Session, userId: string): Promise<void> {
222
+ if (!s.enabled || s.active.has(userId)) return;
223
+ s.active.add(userId);
224
+ const stamp = `${userId}_${Date.now()}`;
225
+ const pcmPath = join(s.tmp, `${stamp}.pcm`);
226
+ const wavPath = join(s.tmp, `${stamp}.wav`);
227
+ try {
228
+ await transcribeUtterance(s, userId, pcmPath, wavPath, stamp);
229
+ } catch (err) {
230
+ process.stderr.write(
231
+ `discord-bot voice transcribe error (${userId}): ${errMsg(err)}\n`,
232
+ );
233
+ } finally {
234
+ s.active.delete(userId);
235
+ cleanupFiles(pcmPath, wavPath);
236
+ }
237
+ }
238
+
239
+ export function startTranscription(
240
+ guildId: string,
241
+ channelId: string,
242
+ accountId: string,
243
+ client: Client,
244
+ conn: VoiceConnection,
245
+ ): void {
246
+ stopTranscription(guildId);
247
+ const receiver = conn.receiver;
248
+ const s: Session = {
249
+ receiver,
250
+ accountId,
251
+ guildId,
252
+ channelId,
253
+ client,
254
+ active: new Set(),
255
+ tmp: mkdtempSync(join(tmpdir(), 'metro-voice-')),
256
+ enabled: true,
257
+ };
258
+ s.onStart = (userId: string) => {
259
+ handleSpeaker(s, userId).catch((err: unknown) => {
260
+ process.stderr.write(
261
+ `discord-bot[${accountId}] voice speaker failed: ${errMsg(err)}\n`,
262
+ );
263
+ });
264
+ };
265
+ receiver.speaking.on('start', s.onStart);
266
+ sessions.set(guildId, s);
267
+ process.stderr.write(
268
+ `discord-bot[${accountId}] voice transcription armed on ${channelId}\n`,
269
+ );
270
+ }
271
+
272
+ export function setTranscription(guildId: string, on: boolean): boolean {
273
+ const s = sessions.get(guildId);
274
+ if (!s) return false;
275
+ s.enabled = on;
276
+ return true;
277
+ }
278
+
279
+ export function stopTranscription(guildId: string): void {
280
+ const s = sessions.get(guildId);
281
+ if (!s) return;
282
+ try {
283
+ if (s.onStart) s.receiver.speaking.removeListener('start', s.onStart);
284
+ } catch {
285
+ }
286
+ s.enabled = false;
287
+ try {
288
+ rmSync(s.tmp, { recursive: true, force: true });
289
+ } catch {
290
+ }
291
+ sessions.delete(guildId);
292
+ }