@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,157 @@
1
+ export interface GroupLike {
2
+ id: string;
3
+ appData?: string;
4
+ updateAppData?: (s: string) => Promise<void>;
5
+ updateName?: (s: string) => Promise<void>;
6
+ updateDescription?: (s: string) => Promise<void>;
7
+ removeMembers?: (inboxIds: string[]) => Promise<void>;
8
+ sync?: () => Promise<unknown>;
9
+ }
10
+
11
+ const MAX_LABELS = 16;
12
+ const MAX_LABEL_LEN = 24;
13
+
14
+ export function cleanLabels(raw: unknown): string[] {
15
+ if (!Array.isArray(raw)) return [];
16
+ const out: string[] = [];
17
+ const seen = new Set<string>();
18
+ for (const item of raw) {
19
+ if (typeof item !== 'string') continue;
20
+ const label = item.trim().replace(/\s+/g, ' ').slice(0, MAX_LABEL_LEN);
21
+ const key = label.toLowerCase();
22
+ if (!label || seen.has(key)) continue;
23
+ seen.add(key);
24
+ out.push(label);
25
+ if (out.length >= MAX_LABELS) break;
26
+ }
27
+ return out;
28
+ }
29
+
30
+ function parseAppDataObject(
31
+ existingAppData: string | undefined,
32
+ ): Record<string, unknown> {
33
+ if (!existingAppData?.trim()) return {};
34
+ try {
35
+ const p: unknown = JSON.parse(existingAppData);
36
+ if (p && typeof p === 'object' && !Array.isArray(p))
37
+ return p as Record<string, unknown>;
38
+ } catch {
39
+ return {};
40
+ }
41
+ return {};
42
+ }
43
+
44
+ export function labelsBlob(
45
+ existingAppData: string | undefined,
46
+ labels: string[],
47
+ github?: string,
48
+ ): string {
49
+ const existing = parseAppDataObject(existingAppData);
50
+ const blob: Record<string, unknown> = {
51
+ ...existing,
52
+ v: 1,
53
+ labels: cleanLabels(labels),
54
+ };
55
+ if (typeof github === 'string') {
56
+ if (github.trim()) blob.github = github.trim();
57
+ else delete blob.github;
58
+ }
59
+ return JSON.stringify(blob);
60
+ }
61
+
62
+ function trimmedString(v: unknown): string | undefined {
63
+ return typeof v === 'string' && v.trim() ? v.trim() : undefined;
64
+ }
65
+
66
+ export function readAppData(appData: string | undefined): {
67
+ labels: string[];
68
+ github?: string;
69
+ preview?: string;
70
+ } {
71
+ if (!appData?.trim()) return { labels: [] };
72
+ try {
73
+ const p: unknown = JSON.parse(appData);
74
+ if (!p || typeof p !== 'object' || Array.isArray(p)) return { labels: [] };
75
+ const rec = p as Record<string, unknown>;
76
+ return {
77
+ labels: cleanLabels(rec.labels),
78
+ github: trimmedString(rec.github),
79
+ preview: trimmedString(rec.preview),
80
+ };
81
+ } catch {
82
+ return { labels: [] };
83
+ }
84
+ }
85
+
86
+ function applyMergeKey(
87
+ merged: Record<string, unknown>,
88
+ k: string,
89
+ v: unknown,
90
+ ): void {
91
+ if (k === 'labels') {
92
+ merged.labels = cleanLabels(v);
93
+ return;
94
+ }
95
+ if (k === 'github') {
96
+ const g = normalizeGithubUrl(v);
97
+ if (g) merged.github = g;
98
+ else delete merged.github;
99
+ return;
100
+ }
101
+ if (k === 'preview') {
102
+ const p = normalizePreviewUrl(v);
103
+ if (p) merged.preview = p;
104
+ else delete merged.preview;
105
+ return;
106
+ }
107
+ if (v === undefined || v === null) Reflect.deleteProperty(merged, k);
108
+ else merged[k] = v;
109
+ }
110
+
111
+ export function mergeAppData(
112
+ existingAppData: string | undefined,
113
+ patch: Record<string, unknown>,
114
+ ): { blob: string; merged: Record<string, unknown> } {
115
+ const merged: Record<string, unknown> = {
116
+ ...parseAppDataObject(existingAppData),
117
+ v: 1,
118
+ };
119
+ for (const [k, v] of Object.entries(patch)) applyMergeKey(merged, k, v);
120
+ return { blob: JSON.stringify(merged), merged };
121
+ }
122
+
123
+ export function normalizeGithubUrl(url: unknown): string {
124
+ if (typeof url !== 'string')
125
+ throw new Error('setGithub requires a `url` string');
126
+ const trimmed = url.trim();
127
+ if (!trimmed) return '';
128
+ let parsed: URL;
129
+ try {
130
+ parsed = new URL(trimmed);
131
+ } catch {
132
+ throw new Error(`invalid url: ${trimmed}`);
133
+ }
134
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
135
+ throw new Error(`github url must be http(s): ${trimmed}`);
136
+ }
137
+ if (
138
+ parsed.hostname !== 'github.com' &&
139
+ parsed.hostname !== 'www.github.com'
140
+ ) {
141
+ throw new Error(`url must be a github.com URL: ${trimmed}`);
142
+ }
143
+ return trimmed;
144
+ }
145
+
146
+ export function normalizePreviewUrl(url: unknown): string {
147
+ if (typeof url !== 'string')
148
+ throw new Error('setPreview requires a `preview` string');
149
+ const trimmed = url.trim();
150
+ if (!trimmed) return '';
151
+ try {
152
+ new URL(trimmed);
153
+ } catch {
154
+ throw new Error(`invalid preview url: ${trimmed}`);
155
+ }
156
+ return trimmed;
157
+ }
@@ -0,0 +1,40 @@
1
+ import { TrainError } from '@metro-labs/mcp/train-error';
2
+
3
+ type Args = Record<string, unknown>;
4
+
5
+ export function filterStrings(value: unknown): string[] {
6
+ if (!Array.isArray(value)) return [];
7
+ return value.filter((a) => typeof a === 'string' && a.length > 0) as string[];
8
+ }
9
+
10
+ export function resolveMembers(args: Args): {
11
+ addrs: string[];
12
+ inboxes: string[];
13
+ } {
14
+ const { addresses, inboxIds, memberAddresses, memberInboxIds } = args as {
15
+ addresses?: string[];
16
+ inboxIds?: string[];
17
+ memberAddresses?: string[];
18
+ memberInboxIds?: string[];
19
+ };
20
+ return {
21
+ addrs: filterStrings(addresses ?? memberAddresses),
22
+ inboxes: filterStrings(inboxIds ?? memberInboxIds),
23
+ };
24
+ }
25
+
26
+ export function parseMemberArgs(
27
+ args: Args,
28
+ verb: string,
29
+ ): { line: string; addrs: string[]; inboxes: string[] } {
30
+ const { line } = args as { line: string };
31
+ if (!line || typeof line !== 'string')
32
+ throw new TrainError('INVALID_ARGS', `${verb} requires a \`line\``);
33
+ const { addrs, inboxes } = resolveMembers(args);
34
+ if (addrs.length === 0 && inboxes.length === 0)
35
+ throw new TrainError(
36
+ 'INVALID_ARGS',
37
+ `${verb} requires addresses[] or inboxIds[]`,
38
+ );
39
+ return { line, addrs, inboxes };
40
+ }
@@ -0,0 +1,349 @@
1
+ import { errMsg } from '@metro-labs/mcp/log';
2
+ import { type DecodedMessage } from '@xmtp/node-sdk';
3
+ import { existsSync, readFileSync, mkdirSync } from 'node:fs';
4
+ import { dirname } from 'node:path';
5
+ import { readJson, writeJson } from '@metro-labs/mcp/secure-fs';
6
+ import { parseLine } from './accounts.js';
7
+
8
+ const FCM_SVC_PATH = `${process.env.HOME}/.config/metro/firebase-service-account.json`;
9
+ const FCM_TOKENS_PATH = `${process.env.HOME}/.cache/metro/xmtp-push-tokens.json`;
10
+ interface FcmServiceAccount {
11
+ client_email: string;
12
+ private_key: string;
13
+ project_id: string;
14
+ token_uri: string;
15
+ }
16
+
17
+ interface StoredPushToken {
18
+ token: string;
19
+ registeredAt: string;
20
+ account?: string;
21
+ inboxId?: string;
22
+ inboxIds?: string[];
23
+ platform?: string;
24
+ lastSeenAt?: string;
25
+ }
26
+
27
+ function loadFcmSvc(): FcmServiceAccount | null {
28
+ if (!existsSync(FCM_SVC_PATH)) return null;
29
+ try {
30
+ return JSON.parse(readFileSync(FCM_SVC_PATH, 'utf8')) as FcmServiceAccount;
31
+ } catch (err) {
32
+ process.stderr.write(
33
+ `fcm: bad service account: ${errMsg(err)}\n`,
34
+ );
35
+ return null;
36
+ }
37
+ }
38
+ export function loadPushTokens(): StoredPushToken[] {
39
+ return readJson<StoredPushToken[]>(FCM_TOKENS_PATH, []);
40
+ }
41
+ export function savePushTokens(tokens: StoredPushToken[]): void {
42
+ mkdirSync(dirname(FCM_TOKENS_PATH), { recursive: true });
43
+ writeJson(FCM_TOKENS_PATH, tokens);
44
+ }
45
+
46
+ function collectInboxIds(
47
+ existing: StoredPushToken | undefined,
48
+ entryInboxId: string | undefined,
49
+ ): Set<string> {
50
+ const inboxIds = new Set<string>(existing?.inboxIds ?? []);
51
+ if (existing?.inboxId) inboxIds.add(existing.inboxId);
52
+ if (entryInboxId) inboxIds.add(entryInboxId);
53
+ return inboxIds;
54
+ }
55
+
56
+ export function storePushToken(entry: {
57
+ token: string;
58
+ account?: string;
59
+ inboxId?: string;
60
+ platform?: string;
61
+ }): number {
62
+ const now = new Date().toISOString();
63
+ const all = loadPushTokens();
64
+ const existing = all.find((t) => t.token === entry.token);
65
+ const remaining = all.filter((t) => t.token !== entry.token);
66
+ const inboxIds = collectInboxIds(existing, entry.inboxId);
67
+ const row: StoredPushToken = {
68
+ token: entry.token,
69
+ registeredAt: existing?.registeredAt ?? now,
70
+ lastSeenAt: now,
71
+ };
72
+ if (entry.account) row.account = entry.account;
73
+ if (entry.inboxId) row.inboxId = entry.inboxId;
74
+ if (inboxIds.size) row.inboxIds = [...inboxIds];
75
+ if (entry.platform) row.platform = entry.platform;
76
+ remaining.push(row);
77
+ savePushTokens(remaining);
78
+ return remaining.length;
79
+ }
80
+
81
+ let cachedAccessToken: { token: string; expiresAt: number } | null = null;
82
+ async function fcmAccessToken(): Promise<string | null> {
83
+ const svc = loadFcmSvc();
84
+ if (!svc) return null;
85
+ if (cachedAccessToken && cachedAccessToken.expiresAt > Date.now() + 60_000)
86
+ return cachedAccessToken.token;
87
+ const { createSign } = await import('node:crypto');
88
+ const enc = (o: unknown): string =>
89
+ Buffer.from(JSON.stringify(o)).toString('base64url');
90
+ const now = Math.floor(Date.now() / 1000);
91
+ const header = { alg: 'RS256', typ: 'JWT' };
92
+ const payload = {
93
+ iss: svc.client_email,
94
+ scope: 'https://www.googleapis.com/auth/firebase.messaging',
95
+ aud: svc.token_uri,
96
+ iat: now,
97
+ exp: now + 3600,
98
+ };
99
+ const sigInput = `${enc(header)}.${enc(payload)}`;
100
+ const signer = createSign('RSA-SHA256');
101
+ signer.update(sigInput);
102
+ signer.end();
103
+ const sig = signer.sign(svc.private_key).toString('base64url');
104
+ const jwt = `${sigInput}.${sig}`;
105
+ const grant = encodeURIComponent(
106
+ 'urn:ietf:params:oauth:grant-type:jwt-bearer',
107
+ );
108
+ const res = await fetch(svc.token_uri, {
109
+ method: 'POST',
110
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
111
+ body: `grant_type=${grant}&assertion=${jwt}`,
112
+ signal: AbortSignal.timeout(15_000),
113
+ });
114
+ if (!res.ok) {
115
+ process.stderr.write(
116
+ `fcm token exchange ${res.status}: ${await res.text()}\n`,
117
+ );
118
+ return null;
119
+ }
120
+ const json = (await res.json()) as {
121
+ access_token: string;
122
+ expires_in: number;
123
+ };
124
+ cachedAccessToken = {
125
+ token: json.access_token,
126
+ expiresAt: Date.now() + json.expires_in * 1000,
127
+ };
128
+ return cachedAccessToken.token;
129
+ }
130
+
131
+ async function fcmPushTo(
132
+ deviceToken: string,
133
+ data: Record<string, string> = {},
134
+ ): Promise<void> {
135
+ const svc = loadFcmSvc();
136
+ if (!svc) return;
137
+ const at = await fcmAccessToken();
138
+ if (!at) return;
139
+ const url = `https://fcm.googleapis.com/v1/projects/${svc.project_id}/messages:send`;
140
+ const payloadData: Record<string, string> = { channelId: 'xmtp', ...data };
141
+ const res = await fetch(url, {
142
+ method: 'POST',
143
+ headers: {
144
+ 'Content-Type': 'application/json',
145
+ Authorization: `Bearer ${at}`,
146
+ },
147
+ body: JSON.stringify({
148
+ message: {
149
+ token: deviceToken,
150
+ android: { priority: 'HIGH' },
151
+ data: payloadData,
152
+ },
153
+ }),
154
+ signal: AbortSignal.timeout(15_000),
155
+ });
156
+ if (!res.ok) {
157
+ const txt = await res.text().catch(() => '');
158
+ const dead =
159
+ res.status === 404 ||
160
+ txt.includes('UNREGISTERED') ||
161
+ txt.includes('NOT_FOUND') ||
162
+ txt.includes('registration-token-not-registered') ||
163
+ txt.includes('NotRegistered');
164
+ if (dead) {
165
+ removePushToken(deviceToken);
166
+ process.stderr.write(
167
+ `fcm: pruned stale token ${deviceToken.slice(0, 12)}…\n`,
168
+ );
169
+ return;
170
+ }
171
+ process.stderr.write(`fcm push ${res.status}: ${txt}\n`);
172
+ }
173
+ }
174
+
175
+ function freshestPerAccount(tokens: StoredPushToken[]): StoredPushToken[] {
176
+ const freshness = (t: StoredPushToken): number =>
177
+ new Date(t.lastSeenAt ?? t.registeredAt ?? 0).getTime();
178
+ const byAccount = new Map<string, StoredPushToken>();
179
+ const unscoped: StoredPushToken[] = [];
180
+ for (const t of tokens) {
181
+ if (!t.account) {
182
+ unscoped.push(t);
183
+ continue;
184
+ }
185
+ const cur = byAccount.get(t.account);
186
+ if (!cur || freshness(t) >= freshness(cur)) byAccount.set(t.account, t);
187
+ }
188
+ return [...byAccount.values(), ...unscoped];
189
+ }
190
+
191
+ export async function fcmPushToAll(
192
+ accountId: string,
193
+ data: Record<string, string> = {},
194
+ excludeInboxId?: string,
195
+ ): Promise<void> {
196
+ const tokens = loadPushTokens().filter((t) => {
197
+ if (t.account && t.account !== accountId) return false;
198
+ if (
199
+ excludeInboxId &&
200
+ (t.inboxId === excludeInboxId || t.inboxIds?.includes(excludeInboxId))
201
+ )
202
+ return false;
203
+ return true;
204
+ });
205
+ const deduped = freshestPerAccount(tokens);
206
+ if (deduped.length === 0) return;
207
+ await Promise.all(
208
+ deduped.map((t) =>
209
+ fcmPushTo(t.token, { ...data, account: accountId }).catch(
210
+ () => undefined,
211
+ ),
212
+ ),
213
+ );
214
+ }
215
+
216
+ const METRO_CTRL_PREFIX = 'METRO_CTRL:';
217
+ const CTRL_REGISTER_PUSH = 'register-push';
218
+ const CTRL_DISABLE_PUSH = 'disable-push';
219
+
220
+ export function removePushToken(token: string): number {
221
+ const all = loadPushTokens();
222
+ const remaining = all.filter((t) => t.token !== token);
223
+ if (remaining.length === all.length) return -1;
224
+ savePushTokens(remaining);
225
+ return remaining.length;
226
+ }
227
+
228
+ function isMetroControlBody(text: unknown): text is string {
229
+ return typeof text === 'string' && text.startsWith(METRO_CTRL_PREFIX);
230
+ }
231
+
232
+ interface RegisterPushPayload {
233
+ v?: number;
234
+ token: string;
235
+ platform?: string;
236
+ address?: string;
237
+ inboxId?: string;
238
+ }
239
+ interface DisablePushPayload {
240
+ v?: number;
241
+ token: string;
242
+ address?: string;
243
+ inboxId?: string;
244
+ }
245
+
246
+ function isValidPushToken(token: unknown): token is string {
247
+ return typeof token === 'string' && token.length >= 20;
248
+ }
249
+
250
+ function handleRegisterPushCtrl(
251
+ accountId: string,
252
+ msg: DecodedMessage,
253
+ arg: string,
254
+ ): void {
255
+ const obj = JSON.parse(arg) as Partial<RegisterPushPayload>;
256
+ if (!obj || !isValidPushToken(obj.token)) {
257
+ process.stderr.write(
258
+ `xmtp[${accountId}]: register-push (ctrl-dm) ignored — bad/short token\n`,
259
+ );
260
+ return;
261
+ }
262
+ const total = storePushToken({
263
+ token: obj.token,
264
+ account: accountId,
265
+ inboxId: msg.senderInboxId,
266
+ platform: typeof obj.platform === 'string' ? obj.platform : undefined,
267
+ });
268
+ process.stderr.write(
269
+ `xmtp[${accountId}]: register-push (ctrl-dm) stored token ${obj.token.slice(0, 12)}… ` +
270
+ `from inbox ${msg.senderInboxId.slice(0, 10)}… (v=${obj.v ?? '?'}, ${total} total)\n`,
271
+ );
272
+ }
273
+
274
+ function handleDisablePushCtrl(accountId: string, arg: string): void {
275
+ const obj = JSON.parse(arg) as Partial<DisablePushPayload>;
276
+ if (!obj || !isValidPushToken(obj.token)) {
277
+ process.stderr.write(
278
+ `xmtp[${accountId}]: disable-push (ctrl-dm) ignored — bad/short token\n`,
279
+ );
280
+ return;
281
+ }
282
+ const remaining = removePushToken(obj.token);
283
+ process.stderr.write(
284
+ `xmtp[${accountId}]: disable-push (ctrl-dm) token ${obj.token.slice(0, 12)}… ` +
285
+ (remaining === -1
286
+ ? 'not found (already gone)\n'
287
+ : `removed (v=${obj.v ?? '?'}, ${remaining} remain)\n`),
288
+ );
289
+ }
290
+
291
+ export function handleControlDm(
292
+ accountId: string,
293
+ msg: DecodedMessage,
294
+ ): boolean {
295
+ const body = msg.content;
296
+ if (!isMetroControlBody(body)) return false;
297
+ const rest = body.slice(METRO_CTRL_PREFIX.length);
298
+ const sep = rest.indexOf(':');
299
+ const verb = sep === -1 ? rest : rest.slice(0, sep);
300
+ const arg = sep === -1 ? '' : rest.slice(sep + 1);
301
+ try {
302
+ if (verb === CTRL_REGISTER_PUSH) {
303
+ handleRegisterPushCtrl(accountId, msg, arg);
304
+ } else if (verb === CTRL_DISABLE_PUSH) {
305
+ handleDisablePushCtrl(accountId, arg);
306
+ } else {
307
+ process.stderr.write(
308
+ `xmtp[${accountId}]: unknown control verb '${verb}' — swallowed\n`,
309
+ );
310
+ }
311
+ } catch (err) {
312
+ process.stderr.write(
313
+ `xmtp[${accountId}]: control DM '${verb}' FAILED: ${errMsg(err)}\n`,
314
+ );
315
+ }
316
+ return true;
317
+ }
318
+
319
+ export function pushInbound(
320
+ accountId: string,
321
+ env: Record<string, unknown>,
322
+ msg: DecodedMessage,
323
+ conv?: unknown,
324
+ ): void {
325
+ const line = typeof env.line === 'string' ? env.line : '';
326
+ const messageId =
327
+ typeof env.message_id === 'string'
328
+ ? env.message_id
329
+ : typeof env.id === 'string'
330
+ ? env.id
331
+ : '';
332
+ void (async (): Promise<void> => {
333
+ const data: Record<string, string> = {
334
+ line,
335
+ messageId,
336
+ account: accountId,
337
+ };
338
+ {
339
+ const p = parseLine(line);
340
+ if (p) data.convId = p.convId.toLowerCase();
341
+ }
342
+ if (conv) {
343
+ const isDm =
344
+ typeof (conv as { peerInboxId?: unknown }).peerInboxId === 'function';
345
+ if (!isDm) data.isGroup = 'true';
346
+ }
347
+ await fcmPushToAll(accountId, data, msg.senderInboxId);
348
+ })().catch(() => undefined);
349
+ }
@@ -0,0 +1,13 @@
1
+ import type { GroupOp, Station, Verb } from '@metro-labs/mcp/stations/types';
2
+ import { XMTP_TOOLS, xmtpSendAttachments } from './tools.js';
3
+
4
+ export const xmtpStation: Station = {
5
+ name: 'xmtp',
6
+ hasAccounts: true,
7
+ hasTrain: true,
8
+ messageVerbs: new Set<Verb>(['send', 'reply', 'react', 'unreact', 'read']),
9
+ groupOps: new Set<GroupOp>(['create_group', 'add_members', 'remove_members']),
10
+ attachmentMode: 'native',
11
+ sendAttachments: xmtpSendAttachments,
12
+ tools: XMTP_TOOLS,
13
+ };
@@ -0,0 +1,126 @@
1
+ import type { CanonicalAttachment, ToolContext } from '@metro-labs/mcp/stations/types';
2
+ import { TrainError } from '@metro-labs/mcp/train-error';
3
+ import {
4
+ guessMime,
5
+ isImageMime,
6
+ isImageExt,
7
+ kindOf,
8
+ } from '@metro-labs/mcp/stations/attachments';
9
+
10
+ export const str = (v: unknown): string => (typeof v === 'string' ? v : '');
11
+
12
+ function strList(value: unknown): string[] {
13
+ return (value as unknown[] | undefined)?.map(String).filter(Boolean) ?? [];
14
+ }
15
+
16
+ interface CreatedGroup {
17
+ line?: string;
18
+ id?: string;
19
+ account?: string;
20
+ }
21
+
22
+ async function applyCreateLabels(
23
+ line: string,
24
+ labels: string[],
25
+ ctx: ToolContext,
26
+ ): Promise<unknown> {
27
+ if (!labels.length) return undefined;
28
+ return ctx.call('updateChannelMeta', { line, appData: { labels } });
29
+ }
30
+
31
+ export async function createChannel(
32
+ a: Record<string, unknown>,
33
+ ctx: ToolContext,
34
+ ) {
35
+ const addresses = strList(a.addresses);
36
+ const channelName = str(a.name);
37
+ const labels = strList(a.labels);
38
+ const account = str(a.account) || undefined;
39
+ if (!addresses.length)
40
+ return ctx.err('create_channel requires a non-empty `addresses` array');
41
+ if (!channelName) return ctx.err('create_channel requires `name`');
42
+ const groupArgs: Record<string, unknown> = { addresses, name: channelName };
43
+ if (account) groupArgs.account = account;
44
+ const { result } = (await ctx.call('newGroup', groupArgs)) as {
45
+ result: CreatedGroup | null;
46
+ };
47
+ const newLine = result?.line ?? '';
48
+ const labelResult = newLine.length
49
+ ? await applyCreateLabels(newLine, labels, ctx)
50
+ : undefined;
51
+ return ctx.okJson({
52
+ line: newLine,
53
+ convId: result?.id,
54
+ account: result?.account,
55
+ labels: labelResult,
56
+ });
57
+ }
58
+
59
+ export async function setChannelMetadata(
60
+ a: Record<string, unknown>,
61
+ ctx: ToolContext,
62
+ ) {
63
+ const line = str(a.line);
64
+ if (!line) return ctx.err('set_channel_metadata requires `line`');
65
+ const labels = a.labels as unknown[] | undefined;
66
+ const github = a.github as string | undefined;
67
+ const preview = a.preview as string | undefined;
68
+ const metaName = a.name as string | undefined;
69
+ const appData: Record<string, unknown> = {};
70
+ if (Array.isArray(labels)) appData.labels = labels.map(String);
71
+ if (typeof github === 'string') appData.github = github;
72
+ if (typeof preview === 'string') appData.preview = preview;
73
+ const hasName = typeof metaName === 'string' && metaName.length > 0;
74
+ if (!hasName && Object.keys(appData).length === 0)
75
+ return ctx.err(
76
+ 'set_channel_metadata requires at least one of `labels`, `github`, `preview`, `name`',
77
+ );
78
+ const callArgs: Record<string, unknown> = { line, appData };
79
+ if (hasName) callArgs.name = metaName;
80
+ return ctx.okJson(await ctx.call('updateChannelMeta', callArgs));
81
+ }
82
+
83
+ const XMTP_ATTACH_MAX_BYTES = 190 * 1024;
84
+
85
+ async function sendFileAttachment(
86
+ line: string,
87
+ a: CanonicalAttachment,
88
+ src: string,
89
+ ctx: ToolContext,
90
+ ): Promise<void> {
91
+ const buf = await ctx.readFile(src);
92
+ if (buf.byteLength > XMTP_ATTACH_MAX_BYTES) {
93
+ throw new TrainError(
94
+ 'attachment_too_large',
95
+ `attachment '${src}' is ${(buf.byteLength / 1024).toFixed(0)} KiB; xmtp non-image files ` +
96
+ 'over ~190 KiB (256 KiB once base64-encoded) cannot be sent via this MCP path. ' +
97
+ 'Send it as an image, host it elsewhere, or use the metro CLI directly.',
98
+ );
99
+ }
100
+ await ctx.call('sendAttachment', {
101
+ line,
102
+ name: a.name ?? src.split('/').pop() ?? 'attachment',
103
+ mime: a.mime ?? guessMime(src),
104
+ dataB64: buf.toString('base64'),
105
+ });
106
+ }
107
+
108
+ export async function xmtpSendAttachments(
109
+ line: string,
110
+ atts: CanonicalAttachment[],
111
+ ctx: ToolContext,
112
+ ): Promise<string[]> {
113
+ const sent: string[] = [];
114
+ for (const a of atts) {
115
+ const src = a.path ?? a.url ?? '';
116
+ if (!src) continue;
117
+ const mime = a.mime ?? guessMime(src);
118
+ if (isImageMime(mime) || isImageExt(src)) {
119
+ await ctx.call('sendImage', { line, path: src });
120
+ } else {
121
+ await sendFileAttachment(line, a, src, ctx);
122
+ }
123
+ sent.push(kindOf(mime, src));
124
+ }
125
+ return sent;
126
+ }