@stage-labs/metro 0.1.0-beta.2 → 0.1.0-beta.21

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 (225) hide show
  1. package/dist/api.js +103 -0
  2. package/dist/cli.js +134 -565
  3. package/dist/control.js +102 -0
  4. package/dist/prompt.js +10 -0
  5. package/dist/runtime.js +94 -0
  6. package/dist/store.js +70 -0
  7. package/dist/update.js +33 -0
  8. package/dist/version.js +97 -0
  9. package/package.json +34 -22
  10. package/runtime/node_modules/@metro-labs/discord/package.json +28 -0
  11. package/runtime/node_modules/@metro-labs/discord/src/accounts.ts +119 -0
  12. package/runtime/node_modules/@metro-labs/discord/src/actions.ts +388 -0
  13. package/runtime/node_modules/@metro-labs/discord/src/api-base.ts +1 -0
  14. package/runtime/node_modules/@metro-labs/discord/src/attachments.ts +29 -0
  15. package/runtime/node_modules/@metro-labs/discord/src/format.ts +210 -0
  16. package/runtime/node_modules/@metro-labs/discord/src/group-actions.ts +50 -0
  17. package/runtime/node_modules/@metro-labs/discord/src/group.ts +115 -0
  18. package/runtime/node_modules/@metro-labs/discord/src/index.ts +127 -0
  19. package/runtime/node_modules/@metro-labs/discord/src/members.ts +215 -0
  20. package/runtime/node_modules/@metro-labs/discord/src/send-files.ts +43 -0
  21. package/runtime/node_modules/@metro-labs/discord/src/station.ts +19 -0
  22. package/runtime/node_modules/@metro-labs/discord/src/verify.ts +82 -0
  23. package/runtime/node_modules/@metro-labs/discord/src/voice-speak.ts +195 -0
  24. package/runtime/node_modules/@metro-labs/discord/src/voice-transcribe.ts +292 -0
  25. package/runtime/node_modules/@metro-labs/discord/src/voice.ts +197 -0
  26. package/runtime/node_modules/@metro-labs/discord/src/wire.ts +5 -0
  27. package/runtime/node_modules/@metro-labs/mcp/package.json +66 -0
  28. package/runtime/node_modules/@metro-labs/mcp/src/channels/dedupe.ts +24 -0
  29. package/runtime/node_modules/@metro-labs/mcp/src/channels/inbound.ts +367 -0
  30. package/runtime/node_modules/@metro-labs/mcp/src/channels/media-note.ts +85 -0
  31. package/runtime/node_modules/@metro-labs/mcp/src/channels/pending.ts +75 -0
  32. package/runtime/node_modules/@metro-labs/mcp/src/channels/relay.ts +129 -0
  33. package/runtime/node_modules/@metro-labs/mcp/src/channels/webhook-note.ts +51 -0
  34. package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +311 -0
  35. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +335 -0
  36. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-error.ts +8 -0
  37. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +129 -0
  38. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +99 -0
  39. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-owner.ts +32 -0
  40. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +124 -0
  41. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +278 -0
  42. package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +322 -0
  43. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +111 -0
  44. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair.ts +20 -0
  45. package/runtime/node_modules/@metro-labs/mcp/src/daemon/collection-api.ts +202 -0
  46. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +338 -0
  47. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-json.ts +31 -0
  48. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth-routes.ts +157 -0
  49. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +127 -0
  50. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +100 -0
  51. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +57 -0
  52. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +333 -0
  53. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cors.ts +29 -0
  54. package/runtime/node_modules/@metro-labs/mcp/src/daemon/crash-guard.ts +49 -0
  55. package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +117 -0
  56. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-auth.ts +164 -0
  57. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-oauth.ts +208 -0
  58. package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +357 -0
  59. package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +79 -0
  60. package/runtime/node_modules/@metro-labs/mcp/src/daemon/log.ts +42 -0
  61. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +162 -0
  62. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +120 -0
  63. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +65 -0
  64. package/runtime/node_modules/@metro-labs/mcp/src/daemon/pair-codes.ts +58 -0
  65. package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +134 -0
  66. package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +214 -0
  67. package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +224 -0
  68. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +125 -0
  69. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-pair.ts +18 -0
  70. package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-source.ts +105 -0
  71. package/runtime/node_modules/@metro-labs/mcp/src/daemon/secure-fs.ts +57 -0
  72. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +27 -0
  73. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +41 -0
  74. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +218 -0
  75. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor-io.ts +97 -0
  76. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor.ts +323 -0
  77. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-call.ts +22 -0
  78. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-error.ts +53 -0
  79. package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +169 -0
  80. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +295 -0
  81. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-store.ts +181 -0
  82. package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +91 -0
  83. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +215 -0
  84. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-map.ts +71 -0
  85. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-scope.ts +64 -0
  86. package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +37 -0
  87. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-collections.ts +218 -0
  88. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +147 -0
  89. package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +329 -0
  90. package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +19 -0
  91. package/runtime/node_modules/@metro-labs/mcp/src/db/key-map.ts +45 -0
  92. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +364 -0
  93. package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +300 -0
  94. package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +31 -0
  95. package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +105 -0
  96. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +131 -0
  97. package/runtime/node_modules/@metro-labs/mcp/src/db/token-carry.ts +45 -0
  98. package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +57 -0
  99. package/runtime/node_modules/@metro-labs/mcp/src/db/whatsapp-login.ts +18 -0
  100. package/runtime/node_modules/@metro-labs/mcp/src/mcp/accounts.ts +145 -0
  101. package/runtime/node_modules/@metro-labs/mcp/src/mcp/call-tools.ts +203 -0
  102. package/runtime/node_modules/@metro-labs/mcp/src/mcp/channel-owner.ts +29 -0
  103. package/runtime/node_modules/@metro-labs/mcp/src/mcp/ctx.ts +62 -0
  104. package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +91 -0
  105. package/runtime/node_modules/@metro-labs/mcp/src/mcp/frame-scope.ts +21 -0
  106. package/runtime/node_modules/@metro-labs/mcp/src/mcp/group-tools.ts +129 -0
  107. package/runtime/node_modules/@metro-labs/mcp/src/mcp/index.ts +192 -0
  108. package/runtime/node_modules/@metro-labs/mcp/src/mcp/member-tools.ts +72 -0
  109. package/runtime/node_modules/@metro-labs/mcp/src/mcp/permission-relay.ts +66 -0
  110. package/runtime/node_modules/@metro-labs/mcp/src/mcp/raw-get-stream.ts +207 -0
  111. package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +73 -0
  112. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-registry.ts +172 -0
  113. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +43 -0
  114. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session.ts +211 -0
  115. package/runtime/node_modules/@metro-labs/mcp/src/mcp/str.ts +5 -0
  116. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-dispatch.ts +212 -0
  117. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +397 -0
  118. package/runtime/node_modules/@metro-labs/mcp/src/mcp/upload-tool.ts +66 -0
  119. package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +240 -0
  120. package/runtime/node_modules/@metro-labs/mcp/src/server.ts +2 -0
  121. package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +80 -0
  122. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-inline.ts +100 -0
  123. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +161 -0
  124. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +200 -0
  125. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-xmtp.ts +111 -0
  126. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +182 -0
  127. package/runtime/node_modules/@metro-labs/mcp/src/stations/attachments.ts +196 -0
  128. package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +82 -0
  129. package/runtime/node_modules/@metro-labs/mcp/src/stations/messaging-normalize.ts +88 -0
  130. package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +35 -0
  131. package/runtime/node_modules/@metro-labs/mcp/src/stations/station-runtime.ts +64 -0
  132. package/runtime/node_modules/@metro-labs/mcp/src/stations/types.ts +107 -0
  133. package/runtime/node_modules/@metro-labs/telegram/package.json +25 -0
  134. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +120 -0
  135. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +295 -0
  136. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +128 -0
  137. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +172 -0
  138. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +103 -0
  139. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +166 -0
  140. package/runtime/node_modules/@metro-labs/telegram/src/members.ts +47 -0
  141. package/runtime/node_modules/@metro-labs/telegram/src/station.ts +17 -0
  142. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +48 -0
  143. package/runtime/node_modules/@metro-labs/telegram/src/verify.ts +38 -0
  144. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +3 -0
  145. package/runtime/node_modules/@metro-labs/telegram-user/package.json +28 -0
  146. package/runtime/node_modules/@metro-labs/telegram-user/src/accounts.ts +81 -0
  147. package/runtime/node_modules/@metro-labs/telegram-user/src/actions.ts +254 -0
  148. package/runtime/node_modules/@metro-labs/telegram-user/src/attachments.ts +87 -0
  149. package/runtime/node_modules/@metro-labs/telegram-user/src/client.ts +77 -0
  150. package/runtime/node_modules/@metro-labs/telegram-user/src/format.ts +141 -0
  151. package/runtime/node_modules/@metro-labs/telegram-user/src/group.ts +261 -0
  152. package/runtime/node_modules/@metro-labs/telegram-user/src/history.ts +52 -0
  153. package/runtime/node_modules/@metro-labs/telegram-user/src/inbound.ts +78 -0
  154. package/runtime/node_modules/@metro-labs/telegram-user/src/index.ts +46 -0
  155. package/runtime/node_modules/@metro-labs/telegram-user/src/login.ts +166 -0
  156. package/runtime/node_modules/@metro-labs/telegram-user/src/media-actions.ts +80 -0
  157. package/runtime/node_modules/@metro-labs/telegram-user/src/members.ts +75 -0
  158. package/runtime/node_modules/@metro-labs/telegram-user/src/normalize.ts +22 -0
  159. package/runtime/node_modules/@metro-labs/telegram-user/src/reactions.ts +167 -0
  160. package/runtime/node_modules/@metro-labs/telegram-user/src/station.ts +24 -0
  161. package/runtime/node_modules/@metro-labs/telegram-user/src/types.ts +7 -0
  162. package/runtime/node_modules/@metro-labs/telegram-user/src/wire.ts +1 -0
  163. package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
  164. package/runtime/node_modules/@metro-labs/webhook/src/station.ts +40 -0
  165. package/runtime/node_modules/@metro-labs/whatsapp/package.json +29 -0
  166. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +67 -0
  167. package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
  168. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +178 -0
  169. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
  170. package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
  171. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +318 -0
  172. package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
  173. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +132 -0
  174. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
  175. package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
  176. package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
  177. package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
  178. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
  179. package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
  180. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
  181. package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
  182. package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +141 -0
  183. package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +17 -0
  184. package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
  185. package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
  186. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
  187. package/runtime/node_modules/@metro-labs/xmtp/package.json +33 -0
  188. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +113 -0
  189. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
  190. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
  191. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
  192. package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
  193. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +375 -0
  194. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
  195. package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
  196. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
  197. package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
  198. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +333 -0
  199. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +78 -0
  200. package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
  201. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
  202. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +33 -0
  203. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
  204. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
  205. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
  206. package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
  207. package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +13 -0
  208. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
  209. package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
  210. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
  211. package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +84 -0
  212. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +39 -0
  213. package/runtime/runtime.json +3 -0
  214. package/runtime/trains/.keep +0 -0
  215. package/LICENSE +0 -21
  216. package/README.md +0 -78
  217. package/dist/channels/discord.js +0 -148
  218. package/dist/channels/telegram.js +0 -137
  219. package/dist/lib/address.js +0 -21
  220. package/dist/lib/codex-rc.js +0 -274
  221. package/dist/lib/dotenv.js +0 -31
  222. package/dist/log.js +0 -12
  223. package/dist/paths.js +0 -45
  224. package/dist/tail.js +0 -161
  225. package/skills/metro/SKILL.md +0 -122
@@ -0,0 +1,67 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import {
4
+ makeAccountStore,
5
+ resolveAccountId,
6
+ type Die,
7
+ } from '@metro-labs/mcp/stations/account-store';
8
+ import type { WhatsAppAccount } from './types.js';
9
+
10
+ const ACCOUNTS_FILE =
11
+ process.env.WHATSAPP_ACCOUNTS_FILE ??
12
+ join(homedir(), '.metro', 'whatsapp-accounts.json');
13
+
14
+ const isJid = (s: string): boolean => s.includes('@');
15
+
16
+ function validateAccount(
17
+ a: WhatsAppAccount,
18
+ seen: Set<string>,
19
+ die: Die,
20
+ ): void {
21
+ if (!a.id) die('account missing id');
22
+ if (!a.phone || typeof a.phone !== 'string')
23
+ die(`account '${a.id}' missing phone`);
24
+ if (seen.has(a.id)) die(`duplicate account id '${a.id}'`);
25
+ seen.add(a.id);
26
+ }
27
+
28
+ export const { loadAccounts } = makeAccountStore<WhatsAppAccount>({
29
+ prefix: 'whatsapp',
30
+ file: ACCOUNTS_FILE,
31
+ allowlistEnv: ['WHATSAPP_ONLY_ACCOUNTS', 'WHATSAPP_ACCOUNTS'],
32
+ validate(raw, die) {
33
+ const seen = new Set<string>();
34
+ for (const a of raw) validateAccount(a, seen, die);
35
+ },
36
+ });
37
+
38
+ export const accounts = new Map<string, WhatsAppAccount>();
39
+
40
+ export function accountFor(args: { account?: string; line?: string }): string {
41
+ return resolveAccountId(accounts, args, (line) => targetOf(line)?.accountId);
42
+ }
43
+
44
+ export function lineOf(accountId: string, jid: string): string {
45
+ return `metro://whatsapp/${accountId}/${jid}`;
46
+ }
47
+
48
+ interface Target {
49
+ accountId: string;
50
+ jid: string;
51
+ }
52
+
53
+ function splitScoped(path: string[]): { accountId: string; jid?: string } {
54
+ const first = path[0];
55
+ if (path.length >= 2 && first !== undefined && !isJid(first))
56
+ return { accountId: first, jid: path[1] };
57
+ return { accountId: 'default', jid: first };
58
+ }
59
+
60
+ export function targetOf(line: string): Target | undefined {
61
+ const prefix = 'metro://whatsapp/';
62
+ if (!line.startsWith(prefix)) return undefined;
63
+ const path = line.slice(prefix.length).split('/').filter(Boolean);
64
+ const { accountId, jid } = splitScoped(path);
65
+ if (jid === undefined || !isJid(jid)) return undefined;
66
+ return { accountId, jid };
67
+ }
@@ -0,0 +1,101 @@
1
+ import { TrainError } from '@metro-labs/mcp/train-error';
2
+ import type { BinaryNode, WASocket } from 'baileys';
3
+
4
+ const DEFAULT_ACK_WAIT_MS = 5000;
5
+
6
+ export function ackWaitMs(raw: string | undefined): number {
7
+ if (raw === undefined || raw.trim() === '') return DEFAULT_ACK_WAIT_MS;
8
+ const ms = Number(raw);
9
+ return Number.isFinite(ms) && ms >= 0 ? ms : DEFAULT_ACK_WAIT_MS;
10
+ }
11
+
12
+ export const ACK_WAIT_MS = ackWaitMs(process.env.METRO_WHATSAPP_ACK_WAIT_MS);
13
+
14
+ const RECENT_MAX = 256;
15
+
16
+ const RESTRICTION = '463';
17
+
18
+ export interface SendAck {
19
+ messageId: string;
20
+ jid: string;
21
+ error?: string;
22
+ }
23
+
24
+ export function ackOf(node: BinaryNode): SendAck | undefined {
25
+ const { id, from, error } = node.attrs;
26
+ if (!id || !from) return undefined;
27
+ return { messageId: id, jid: from, ...(error ? { error } : {}) };
28
+ }
29
+
30
+ export function rejection(ack: SendAck): TrainError | undefined {
31
+ if (!ack.error) return undefined;
32
+ if (ack.error === RESTRICTION)
33
+ return new TrainError(
34
+ 'whatsapp_account_restricted',
35
+ `WhatsApp refused the message to ${ack.jid} with ack error 463 (MessageAccountRestriction): the send carried no trusted-contact token, so WhatsApp counted it as reaching out to a stranger and this account is under a reach-out timelock. Nothing was delivered, and it must not be retried — every retry counts as another reach-out and deepens the lock.`,
36
+ { retryable: false },
37
+ );
38
+ return new TrainError(
39
+ 'whatsapp_send_refused',
40
+ `WhatsApp refused the message to ${ack.jid} with ack error ${ack.error}, so it was not delivered`,
41
+ { retryable: false },
42
+ );
43
+ }
44
+
45
+ export interface AckWatch {
46
+ record(node: BinaryNode): SendAck | undefined;
47
+ wait(messageId: string, timeoutMs: number): Promise<SendAck | undefined>;
48
+ }
49
+
50
+ export function makeAckWatch(max: number = RECENT_MAX): AckWatch {
51
+ const recent = new Map<string, SendAck>();
52
+ const waiting = new Map<string, (ack: SendAck) => void>();
53
+ return {
54
+ record(node) {
55
+ const ack = ackOf(node);
56
+ if (!ack) return undefined;
57
+ const waiter = waiting.get(ack.messageId);
58
+ if (waiter) {
59
+ waiting.delete(ack.messageId);
60
+ waiter(ack);
61
+ return ack;
62
+ }
63
+ recent.delete(ack.messageId);
64
+ recent.set(ack.messageId, ack);
65
+ while (recent.size > max) {
66
+ const oldest = recent.keys().next().value;
67
+ if (oldest === undefined) break;
68
+ recent.delete(oldest);
69
+ }
70
+ return ack;
71
+ },
72
+ wait(messageId, timeoutMs) {
73
+ const seen = recent.get(messageId);
74
+ if (seen) {
75
+ recent.delete(messageId);
76
+ return Promise.resolve(seen);
77
+ }
78
+ return new Promise<SendAck | undefined>((resolve) => {
79
+ const timer = setTimeout(() => {
80
+ waiting.delete(messageId);
81
+ resolve(undefined);
82
+ }, timeoutMs);
83
+ waiting.set(messageId, (ack) => {
84
+ clearTimeout(timer);
85
+ resolve(ack);
86
+ });
87
+ });
88
+ },
89
+ };
90
+ }
91
+
92
+ export function bindAcks(
93
+ sock: WASocket,
94
+ watch: AckWatch,
95
+ onError: (ack: SendAck) => void,
96
+ ): void {
97
+ sock.ws.on('CB:ack,class:message', (node: BinaryNode) => {
98
+ const ack = watch.record(node);
99
+ if (ack?.error) onError(ack);
100
+ });
101
+ }
@@ -0,0 +1,178 @@
1
+ import { TrainError } from '@metro-labs/mcp/train-error';
2
+ import { errMsg } from '@metro-labs/mcp/log';
3
+ import { kindOf } from '@metro-labs/mcp/stations/attachments';
4
+ import {
5
+ makeStation,
6
+ respond,
7
+ type CallMsg,
8
+ type StationHandler,
9
+ } from '@metro-labs/mcp/stations/station-runtime';
10
+ import { accountFor, accounts, targetOf } from './accounts.js';
11
+ import { normalizeWhatsApp } from './normalize.js';
12
+ import type { WAClient } from './client.js';
13
+
14
+ type Args = Record<string, unknown>;
15
+
16
+ interface Resolved {
17
+ accountId: string;
18
+ client: WAClient;
19
+ jid: string;
20
+ }
21
+
22
+ type ClientFor = (accountId: string) => WAClient;
23
+
24
+ function str(v: unknown): string | undefined {
25
+ return typeof v === 'string' ? v : undefined;
26
+ }
27
+
28
+ function resolve(args: Args, clientFor: ClientFor): Resolved {
29
+ const line = str(args.line);
30
+ if (!line) throw new TrainError('bad_request', 'missing line');
31
+ const target = targetOf(line);
32
+ if (!target) throw new TrainError('bad_request', `bad line '${line}'`);
33
+ const accountId = accountFor({ account: str(args.account), line });
34
+ return { accountId, client: clientFor(accountId), jid: target.jid };
35
+ }
36
+
37
+ async function guard<T>(run: () => Promise<T>): Promise<T> {
38
+ try {
39
+ return await run();
40
+ } catch (e) {
41
+ if (e instanceof TrainError) throw e;
42
+ const msg = errMsg(e);
43
+ if (/rate.?over.?limit|too many|429/i.test(msg))
44
+ throw new TrainError('rate_limited', msg);
45
+ throw new TrainError('whatsapp_call', msg);
46
+ }
47
+ }
48
+
49
+ interface WireAttachment {
50
+ kind?: string;
51
+ path?: string;
52
+ url?: string;
53
+ mime?: string;
54
+ name?: string;
55
+ }
56
+
57
+ function attachmentsOf(args: Args): WireAttachment[] {
58
+ const raw = args.attachments;
59
+ if (!Array.isArray(raw)) return [];
60
+ return (raw as WireAttachment[]).filter((a) => Boolean(a.path ?? a.url));
61
+ }
62
+
63
+ async function sendMediaSet(
64
+ client: WAClient,
65
+ jid: string,
66
+ atts: WireAttachment[],
67
+ text: string,
68
+ replyTo: string | undefined,
69
+ ): Promise<{ messageId: string; labels: string[] }> {
70
+ const labels: string[] = [];
71
+ let messageId = '';
72
+ for (let i = 0; i < atts.length; i++) {
73
+ const att = atts[i];
74
+ if (!att) continue;
75
+ const path = att.path ?? att.url ?? '';
76
+ const kind = att.kind ?? kindOf(att.mime ?? '', path);
77
+ messageId = await guard(() =>
78
+ client.sendMedia(
79
+ jid,
80
+ {
81
+ kind,
82
+ path,
83
+ mime: att.mime ?? 'application/octet-stream',
84
+ name: att.name ?? path.split('/').pop() ?? 'attachment',
85
+ ...(i === 0 && text ? { caption: text } : {}),
86
+ },
87
+ replyTo,
88
+ ),
89
+ );
90
+ labels.push(kind);
91
+ }
92
+ return { messageId, labels };
93
+ }
94
+
95
+ function makeSend(clientFor: ClientFor): StationHandler {
96
+ return async (id, args) => {
97
+ const { accountId, client, jid } = resolve(args, clientFor);
98
+ const text = str(args.text) ?? '';
99
+ const replyTo = str(args.replyTo);
100
+ const atts = attachmentsOf(args);
101
+ if (atts.length) {
102
+ const { messageId, labels } = await sendMediaSet(
103
+ client,
104
+ jid,
105
+ atts,
106
+ text,
107
+ replyTo,
108
+ );
109
+ respond(id, {
110
+ result: { messageId, account: accountId, attachments: labels },
111
+ });
112
+ return;
113
+ }
114
+ const messageId = await guard(() => client.sendText(jid, text, replyTo));
115
+ respond(id, { result: { messageId, account: accountId } });
116
+ };
117
+ }
118
+
119
+ function makeReact(clientFor: ClientFor): StationHandler {
120
+ return async (id, args) => {
121
+ const { accountId, client, jid } = resolve(args, clientFor);
122
+ const messageId = str(args.messageId) ?? '';
123
+ const emoji = str(args.emoji) ?? '';
124
+ await guard(() => client.sendReaction(jid, messageId, emoji));
125
+ respond(id, { result: { ok: true, account: accountId } });
126
+ };
127
+ }
128
+
129
+ function makeEdit(clientFor: ClientFor): StationHandler {
130
+ return async (id, args) => {
131
+ const { accountId, client, jid } = resolve(args, clientFor);
132
+ const messageId = str(args.messageId) ?? '';
133
+ const text = str(args.text) ?? '';
134
+ await guard(() => client.editMessage(jid, messageId, text));
135
+ respond(id, { result: { ok: true, account: accountId } });
136
+ };
137
+ }
138
+
139
+ function makeDelete(clientFor: ClientFor): StationHandler {
140
+ return async (id, args) => {
141
+ const { accountId, client, jid } = resolve(args, clientFor);
142
+ const messageId = str(args.messageId) ?? '';
143
+ await guard(() => client.deleteMessage(jid, messageId));
144
+ respond(id, { result: { ok: true, account: accountId } });
145
+ };
146
+ }
147
+
148
+ function makeAccounts(clientFor: ClientFor): StationHandler {
149
+ return (id) => {
150
+ const list = [...accounts.values()].map((a) => {
151
+ const phone = clientFor(a.id).self();
152
+ return {
153
+ id: a.id,
154
+ handle: phone === null ? null : `+${phone}`,
155
+ url: phone === null ? null : `https://wa.me/${phone}`,
156
+ connected: phone !== null,
157
+ owner: a.owner ?? null,
158
+ };
159
+ });
160
+ respond(id, { result: { accounts: list } });
161
+ return Promise.resolve();
162
+ };
163
+ }
164
+
165
+ export function makeHandleCall(
166
+ clientFor: ClientFor,
167
+ ): (msg: CallMsg) => Promise<void> {
168
+ return makeStation({
169
+ handlers: {
170
+ accounts: makeAccounts(clientFor),
171
+ send: makeSend(clientFor),
172
+ react: makeReact(clientFor),
173
+ edit: makeEdit(clientFor),
174
+ delete: makeDelete(clientFor),
175
+ },
176
+ normalize: normalizeWhatsApp,
177
+ });
178
+ }
@@ -0,0 +1,72 @@
1
+ import { downloadMediaMessage, type WAMessage } from 'baileys';
2
+ import {
3
+ assertAttachmentSize,
4
+ saveStreamToCache,
5
+ type SavedAttachment,
6
+ } from '@metro-labs/mcp/stations/attachments';
7
+ import { TrainError } from '@metro-labs/mcp/train-error';
8
+ import { baileysLogger } from './logger.js';
9
+ import type { WAMediaRef } from './media.js';
10
+
11
+ export type { SavedAttachment };
12
+
13
+ const IDLE_TIMEOUT_MS = 60_000;
14
+
15
+ export type ReuploadRequest = (m: WAMessage) => Promise<WAMessage>;
16
+
17
+ export async function* withIdleTimeout(
18
+ chunks: AsyncIterable<Uint8Array>,
19
+ ms: number,
20
+ ): AsyncGenerator<Uint8Array> {
21
+ const iterator = chunks[Symbol.asyncIterator]();
22
+ for (;;) {
23
+ let timer: ReturnType<typeof setTimeout> | undefined;
24
+ const idle = new Promise<never>((_resolve, reject) => {
25
+ timer = setTimeout(() => {
26
+ reject(
27
+ new TrainError(
28
+ 'whatsapp_media_stalled',
29
+ `WhatsApp sent no media bytes for ${ms}ms, so the download was abandoned`,
30
+ ),
31
+ );
32
+ }, ms);
33
+ });
34
+ try {
35
+ const next = await Promise.race([iterator.next(), idle]);
36
+ if (next.done === true) return;
37
+ yield next.value;
38
+ } finally {
39
+ clearTimeout(timer);
40
+ }
41
+ }
42
+ }
43
+
44
+ export async function saveWhatsAppMedia(
45
+ accountId: string,
46
+ raw: WAMessage,
47
+ ref: WAMediaRef,
48
+ messageId: string,
49
+ index = 0,
50
+ reupload?: ReuploadRequest,
51
+ ): Promise<SavedAttachment> {
52
+ if (ref.bytes !== undefined) assertAttachmentSize(ref.bytes);
53
+ const stream = await downloadMediaMessage(
54
+ raw,
55
+ 'stream',
56
+ {},
57
+ reupload
58
+ ? { reuploadRequest: reupload, logger: baileysLogger(accountId) }
59
+ : undefined,
60
+ );
61
+ try {
62
+ return await saveStreamToCache(
63
+ withIdleTimeout(stream, IDLE_TIMEOUT_MS),
64
+ messageId,
65
+ index,
66
+ { mime: ref.mime, name: ref.name },
67
+ );
68
+ } catch (err) {
69
+ stream.destroy();
70
+ throw err;
71
+ }
72
+ }
@@ -0,0 +1,129 @@
1
+ import {
2
+ BufferJSON,
3
+ initAuthCreds,
4
+ proto,
5
+ type AuthenticationCreds,
6
+ type AuthenticationState,
7
+ type SignalDataSet,
8
+ type SignalDataTypeMap,
9
+ type SignalKeyStore,
10
+ } from 'baileys';
11
+ import { TrainError } from '@metro-labs/mcp/train-error';
12
+ import {
13
+ isPersistedKeyType,
14
+ tokenStoreFor,
15
+ type KeyTable,
16
+ } from './token-store.js';
17
+
18
+ interface AuthBlob {
19
+ creds: AuthenticationCreds;
20
+ keys: KeyTable;
21
+ }
22
+
23
+ function encode(value: unknown): unknown {
24
+ return JSON.parse(JSON.stringify(value, BufferJSON.replacer));
25
+ }
26
+
27
+ function decode(value: unknown): unknown {
28
+ return JSON.parse(JSON.stringify(value), BufferJSON.reviver);
29
+ }
30
+
31
+ function loadBlob(raw: unknown): AuthBlob {
32
+ const obj = raw as { creds?: unknown; keys?: KeyTable } | null;
33
+ const creds = obj?.creds;
34
+ if (creds === undefined || creds === null)
35
+ throw new TrainError(
36
+ 'whatsapp_auth',
37
+ 'stored credentials blob has no creds',
38
+ );
39
+ return {
40
+ creds: decode(creds) as AuthenticationCreds,
41
+ keys: obj?.keys ?? {},
42
+ };
43
+ }
44
+
45
+ function makeKeyStore(
46
+ table: KeyTable,
47
+ onPersistedWrite?: (table: KeyTable) => void,
48
+ ): SignalKeyStore {
49
+ return {
50
+ get: <T extends keyof SignalDataTypeMap>(type: T, ids: string[]) => {
51
+ const bucket = table[type] ?? {};
52
+ const data: Record<string, SignalDataTypeMap[T]> = {};
53
+ for (const id of ids) {
54
+ const raw = bucket[id];
55
+ if (raw === undefined || raw === null) continue;
56
+ const revived = decode(raw);
57
+ data[id] = (
58
+ type === 'app-state-sync-key'
59
+ ? proto.Message.AppStateSyncKeyData.fromObject(
60
+ revived as Record<string, unknown>,
61
+ )
62
+ : revived
63
+ ) as SignalDataTypeMap[T];
64
+ }
65
+ return data;
66
+ },
67
+ set: (data: SignalDataSet) => {
68
+ let persisted = false;
69
+ for (const category of Object.keys(data) as (keyof SignalDataSet)[]) {
70
+ const items = data[category];
71
+ if (!items) continue;
72
+ if (isPersistedKeyType(category)) persisted = true;
73
+ const bucket = (table[category] ??= {});
74
+ for (const id of Object.keys(items)) {
75
+ const value = items[id];
76
+ bucket[id] = value ? encode(value) : undefined;
77
+ }
78
+ }
79
+ if (persisted) onPersistedWrite?.(table);
80
+ },
81
+ };
82
+ }
83
+
84
+ export function inMemoryAuthState(
85
+ raw?: unknown,
86
+ opts?: { seed?: KeyTable; onPersistedWrite?: (table: KeyTable) => void },
87
+ ): {
88
+ state: AuthenticationState;
89
+ serialize: () => unknown;
90
+ } {
91
+ const blob: AuthBlob =
92
+ raw === undefined || raw === null
93
+ ? { creds: initAuthCreds(), keys: {} }
94
+ : loadBlob(raw);
95
+ for (const [type, bucket] of Object.entries(opts?.seed ?? {}))
96
+ blob.keys[type] = { ...blob.keys[type], ...bucket };
97
+ const state: AuthenticationState = {
98
+ creds: blob.creds,
99
+ keys: makeKeyStore(blob.keys, opts?.onPersistedWrite),
100
+ };
101
+ return {
102
+ state,
103
+ serialize: () => ({ creds: encode(blob.creds), keys: blob.keys }),
104
+ };
105
+ }
106
+
107
+ export function useAccountAuthState(
108
+ credentials: unknown,
109
+ accountId: string,
110
+ ): {
111
+ state: AuthenticationState;
112
+ saveCreds: () => Promise<void>;
113
+ } {
114
+ if (credentials === undefined || credentials === null)
115
+ throw new TrainError(
116
+ 'whatsapp_auth',
117
+ `no WhatsApp credentials in accounts for '${accountId}' — run scripts/login.ts to pair`,
118
+ );
119
+ const store = tokenStoreFor(accountId, (message) => {
120
+ process.stderr.write(`whatsapp[${accountId}] token store: ${message}\n`);
121
+ });
122
+ const { state } = inMemoryAuthState(credentials, {
123
+ seed: store.load(),
124
+ onPersistedWrite: (table) => {
125
+ store.scheduleSave(table);
126
+ },
127
+ });
128
+ return { state, saveCreds: () => Promise.resolve() };
129
+ }