@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,66 @@
1
+ {
2
+ "name": "@metro-labs/mcp",
3
+ "version": "0.1.0-beta.15",
4
+ "private": true,
5
+ "description": "Event-interception wire. Supervises train subprocesses in ~/.metro/trains/, multiplexes their JSON event stream onto stdout, and routes outbound action calls back via stdin. Per-platform code is written by the agent (or user) as train scripts — metro core is pure transport.",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/bonustrack/metro.git"
10
+ },
11
+ "homepage": "https://github.com/bonustrack/metro#readme",
12
+ "bugs": "https://github.com/bonustrack/metro/issues",
13
+ "type": "module",
14
+ "engines": {
15
+ "node": ">=22"
16
+ },
17
+ "bin": {
18
+ "metro-daemon": "./dist/server.js"
19
+ },
20
+ "exports": {
21
+ ".": "./src/mcp/index.ts",
22
+ "./server": "./dist/server.js",
23
+ "./log": "./src/daemon/log.ts",
24
+ "./train-error": "./src/daemon/train-error.ts",
25
+ "./secure-fs": "./src/daemon/secure-fs.ts",
26
+ "./lines": "./src/stations/lines.ts",
27
+ "./events": "./src/daemon/events.ts",
28
+ "./endpoints": "./src/daemon/tunnel.ts",
29
+ "./trains/protocol": "./src/daemon/protocol.ts",
30
+ "./stations/types": "./src/stations/types.ts",
31
+ "./stations/station-runtime": "./src/stations/station-runtime.ts",
32
+ "./stations/account-store": "./src/stations/account-store.ts",
33
+ "./stations/attachments": "./src/stations/attachments.ts",
34
+ "./stations/messaging-normalize": "./src/stations/messaging-normalize.ts",
35
+ "./db/whatsapp-login": "./src/db/whatsapp-login.ts"
36
+ },
37
+ "scripts": {
38
+ "start": "bun src/server.ts",
39
+ "build": "tsc",
40
+ "db:generate": "drizzle-kit generate",
41
+ "db:migrate": "drizzle-kit migrate",
42
+ "test": "tsc --noEmit && DYLD_FALLBACK_LIBRARY_PATH=/usr/lib METRO_STATE_DIR=\"$(mktemp -d /tmp/metro-test.XXXXXX)\" bun test test/"
43
+ },
44
+ "dependencies": {
45
+ "@metro-labs/discord": "workspace:*",
46
+ "@metro-labs/telegram": "workspace:*",
47
+ "@metro-labs/telegram-user": "workspace:*",
48
+ "@metro-labs/webhook": "workspace:*",
49
+ "@metro-labs/whatsapp": "workspace:*",
50
+ "@metro-labs/xmtp": "workspace:*",
51
+ "@modelcontextprotocol/sdk": "^1.12.0",
52
+ "drizzle-kit": "^0.31.0",
53
+ "drizzle-orm": "^0.44.0",
54
+ "pino": "^9.5.0",
55
+ "pino-pretty": "^13.1.3",
56
+ "postgres": "^3.4.5",
57
+ "zod": "^3.23.8"
58
+ },
59
+ "devDependencies": {
60
+ "@stage-labs/config": "0.1.0-beta.2",
61
+ "eslint": "^10.3.0",
62
+ "knip": "^6.15.0",
63
+ "madge": "^8.0.0",
64
+ "typescript": "^5"
65
+ }
66
+ }
@@ -0,0 +1,24 @@
1
+ import { accountFromLine, agentIdForAccount } from '../db/agent-map.js';
2
+
3
+ const accountStrippedLine = (line: string): string => {
4
+ const parts = line.split('/');
5
+ if (parts.length < 5) return line;
6
+ return [parts[0], parts[1], parts[2], ...parts.slice(4)].join('/');
7
+ };
8
+
9
+ export const dedupeOwner = (line: string): string => {
10
+ const acct = accountFromLine(line);
11
+ if (!acct) return 'unowned';
12
+ const agentId = agentIdForAccount(acct.station, acct.accountId);
13
+ return agentId === undefined
14
+ ? `account:${acct.accountId}`
15
+ : `agent:${agentId}`;
16
+ };
17
+
18
+ export const dedupeKey = (
19
+ station: string,
20
+ line: string,
21
+ kind: string,
22
+ messageId: string,
23
+ ): string =>
24
+ `${dedupeOwner(line)} ${station} ${accountStrippedLine(line)} ${kind} ${messageId}`;
@@ -0,0 +1,367 @@
1
+ import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { str } from '../mcp/str.js';
3
+ import { dedupeKey } from './dedupe.js';
4
+ import {
5
+ buildMediaFailureNote,
6
+ buildMediaNote,
7
+ type MediaNote,
8
+ type SavedMedia,
9
+ } from './media-note.js';
10
+ import { buildWebhookNote } from './webhook-note.js';
11
+ import {
12
+ capSet,
13
+ displayNameMeta,
14
+ senderMeta,
15
+ tsMeta,
16
+ takeMediaCtx,
17
+ type MediaCtx,
18
+ type PendingAtt,
19
+ type PendingMsg,
20
+ } from './pending.js';
21
+
22
+ interface InboundDeps {
23
+ mcp: Server;
24
+ log: (...a: unknown[]) => void;
25
+ getStations: () => Set<string>;
26
+ senderAllowed: (from: string, line: string) => boolean;
27
+ }
28
+
29
+ const ATTACH_TIMEOUT_MS = 15_000;
30
+ const DEDUPE_TTL_MS = 30_000;
31
+ const DEDUPE_MAX = 2_000;
32
+ const ALLOWED_LINES_MAX = 2_000;
33
+ const PENDING_PERMISSIONS_MAX = 500;
34
+
35
+ const PERMISSION_REPLY_RE = /^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i;
36
+
37
+ const shortId = (id: string): string =>
38
+ id.length > 10 ? `${id.slice(0, 6)}…` : id;
39
+
40
+ export class InboundRelay {
41
+ private readonly deps: InboundDeps;
42
+ private readonly pendingAttachments = new Map<string, PendingMsg>();
43
+ private readonly seenEvents = new Map<string, number>();
44
+ private readonly allowedLines = new Set<string>();
45
+ private readonly pendingPermissions = new Set<string>();
46
+ private lastLine: string | undefined;
47
+
48
+ constructor(deps: InboundDeps) {
49
+ this.deps = deps;
50
+ }
51
+
52
+ get knownLine(): string | undefined {
53
+ return this.lastLine;
54
+ }
55
+
56
+ registerPermission(requestId: string): void {
57
+ this.pendingPermissions.add(requestId);
58
+ capSet(this.pendingPermissions, PENDING_PERMISSIONS_MAX);
59
+ }
60
+
61
+ private notify(method: string, params: Record<string, unknown>): Promise<void> {
62
+ return this.deps.mcp.notification({ method, params });
63
+ }
64
+
65
+ private isDuplicate(
66
+ station: string,
67
+ line: string,
68
+ kind: string,
69
+ messageId: string,
70
+ ): boolean {
71
+ if (!messageId) return false;
72
+ const key = dedupeKey(station, line, kind, messageId);
73
+ const now = Date.now();
74
+ if (this.seenEvents.size >= DEDUPE_MAX) {
75
+ for (const [k, t] of this.seenEvents) {
76
+ if (now - t > DEDUPE_TTL_MS) this.seenEvents.delete(k);
77
+ }
78
+ }
79
+ const prev = this.seenEvents.get(key);
80
+ if (prev !== undefined && now - prev < DEDUPE_TTL_MS) return true;
81
+ this.seenEvents.set(key, now);
82
+ return false;
83
+ }
84
+
85
+ private surfaceNote(
86
+ ctx: MediaCtx,
87
+ p: SavedMedia,
88
+ note: MediaNote,
89
+ extra: Record<string, unknown>,
90
+ ): Promise<void> {
91
+ return this.notify('notifications/claude/channel', {
92
+ content: note.content,
93
+ meta: {
94
+ line: ctx.line,
95
+ from: ctx.from,
96
+ station: ctx.station,
97
+ ...senderMeta(ctx),
98
+ kind: note.kind,
99
+ mime: p.mime ?? '',
100
+ name: note.name,
101
+ ...extra,
102
+ },
103
+ });
104
+ }
105
+
106
+ private async surfaceMedia(ctx: MediaCtx, p: SavedMedia): Promise<void> {
107
+ const note = await buildMediaNote(p, ctx.text ?? '');
108
+ if (!note) return;
109
+ await this.surfaceNote(ctx, p, note, {
110
+ ...(p.url ? { url: p.url } : {}),
111
+ local_path: note.path,
112
+ });
113
+ }
114
+
115
+ private surfaceMediaFailure(ctx: MediaCtx, p: SavedMedia): Promise<void> {
116
+ return this.surfaceNote(ctx, p, buildMediaFailureNote(p, ctx.text ?? ''), {
117
+ attachment_error: p.reason ?? '',
118
+ });
119
+ }
120
+
121
+ private async flushPendingFallback(id: string): Promise<void> {
122
+ const e = this.pendingAttachments.get(id);
123
+ if (!e) return;
124
+ this.pendingAttachments.delete(id);
125
+ const missing = e.attachments.filter((_, i) => !e.saved.has(i));
126
+ if (!missing.length) return;
127
+ const names = missing
128
+ .map((a) => a.name ?? a.kind ?? 'attachment')
129
+ .join(', ');
130
+ await this.notify('notifications/claude/channel', {
131
+ content:
132
+ (e.text ? `${e.text}\n` : '') +
133
+ `[attachment(s) could not be fetched in time: ${names}]`,
134
+ meta: {
135
+ line: e.line,
136
+ from: e.from,
137
+ station: e.station,
138
+ ...tsMeta(e.ts),
139
+ message_id: e.messageId,
140
+ line_name: e.lineName,
141
+ from_name: e.fromName,
142
+ ...displayNameMeta(e.fromDisplayName),
143
+ },
144
+ });
145
+ }
146
+
147
+ private mediaCtxFor(
148
+ ev: Record<string, unknown>,
149
+ payload: SavedMedia,
150
+ ): MediaCtx | null {
151
+ const forId = str(payload.attachmentFor);
152
+ const buf = forId ? this.pendingAttachments.get(forId) : undefined;
153
+ if (buf) {
154
+ buf.saved.add(typeof payload.index === 'number' ? payload.index : 0);
155
+ const ctx = takeMediaCtx(buf);
156
+ if (buf.saved.size >= buf.attachments.length) {
157
+ clearTimeout(buf.timer);
158
+ this.pendingAttachments.delete(forId);
159
+ }
160
+ return ctx;
161
+ }
162
+ const line = str(ev.line);
163
+ if (!line || !this.allowedLines.has(line)) return null;
164
+ return {
165
+ line,
166
+ from: 'metro://attachment',
167
+ station: str(ev.station) || 'xmtp',
168
+ ts: str(ev.ts),
169
+ };
170
+ }
171
+
172
+ private async routeAttachment(ev: Record<string, unknown>): Promise<boolean> {
173
+ const p = ev.payload as SavedMedia | undefined;
174
+ if (!p) return false;
175
+ const type = p.contentType;
176
+ if (type !== 'attachmentSaved' && type !== 'attachmentFailed') return false;
177
+ const ctx = this.mediaCtxFor(ev, p);
178
+ if (!ctx) return true;
179
+ if (type === 'attachmentFailed') await this.surfaceMediaFailure(ctx, p);
180
+ else await this.surfaceMedia(ctx, p);
181
+ return true;
182
+ }
183
+
184
+ private bufferAttachments(
185
+ ev: Record<string, unknown>,
186
+ base: { line: string; from: string; station: string; text: string },
187
+ atts: PendingAtt[],
188
+ ): void {
189
+ const id = str(ev.id);
190
+ if (!id) return;
191
+ const existing = this.pendingAttachments.get(id);
192
+ if (existing) clearTimeout(existing.timer);
193
+ this.pendingAttachments.set(id, {
194
+ ...base,
195
+ ts: str(ev.ts),
196
+ messageId: str(ev.messageId),
197
+ lineName: str(ev.lineName),
198
+ fromName: str(ev.fromName),
199
+ fromDisplayName: str(ev.fromDisplayName),
200
+ attachments: atts.map((a) => ({ kind: a.kind, name: a.name })),
201
+ saved: new Set<number>(),
202
+ timer: setTimeout(() => {
203
+ this.flushPendingFallback(id).catch((err: unknown) => {
204
+ this.deps.log(
205
+ 'inbound: attachment fallback not delivered',
206
+ 'id',
207
+ id,
208
+ 'line',
209
+ base.line,
210
+ err,
211
+ );
212
+ });
213
+ }, ATTACH_TIMEOUT_MS),
214
+ });
215
+ }
216
+
217
+ private async handleReact(
218
+ ev: Record<string, unknown>,
219
+ base: { line: string; from: string; station: string; text: string },
220
+ ): Promise<void> {
221
+ const re = ev.event as { emoji?: unknown; targetId?: string };
222
+ const emoji = reactionEmoji(re.emoji);
223
+ const target = re.targetId ?? str(ev.messageId);
224
+ const removed =
225
+ (ev.payload as { removed?: boolean } | undefined)?.removed === true ||
226
+ / \(removed\)\]?$/.test(base.text);
227
+ const verb = removed ? 'removed from' : 'reacted to';
228
+ const label = removed ? emoji || 'reaction' : emoji || 'reacted';
229
+ const content = `${label} ${verb} message ${shortId(target)}`.trim();
230
+ await this.notify('notifications/claude/channel', {
231
+ content,
232
+ meta: {
233
+ line: base.line,
234
+ from: base.from,
235
+ station: base.station,
236
+ ...tsMeta(ev.ts),
237
+ message_id: str(ev.messageId),
238
+ line_name: str(ev.lineName),
239
+ from_name: str(ev.fromName),
240
+ ...displayNameMeta(ev.fromDisplayName),
241
+ reaction: emoji,
242
+ target_id: target,
243
+ },
244
+ });
245
+ }
246
+
247
+ private async handlePermissionReply(text: string): Promise<boolean> {
248
+ const m = PERMISSION_REPLY_RE.exec(text);
249
+ if (m?.[1] === undefined || m[2] === undefined || !this.pendingPermissions.size)
250
+ return false;
251
+ const id = m[2].toLowerCase();
252
+ if (!this.pendingPermissions.has(id)) return false;
253
+ this.pendingPermissions.delete(id);
254
+ await this.notify('notifications/claude/channel/permission', {
255
+ request_id: id,
256
+ behavior: m[1].toLowerCase().startsWith('y') ? 'allow' : 'deny',
257
+ });
258
+ return true;
259
+ }
260
+
261
+ private droppedSender(from: string, line: string): boolean {
262
+ if (
263
+ from.startsWith('metro://claude') ||
264
+ from === 'metro://user' ||
265
+ !from.startsWith('metro://')
266
+ )
267
+ return true;
268
+ if (!this.deps.senderAllowed(from, line)) {
269
+ this.deps.log('drop: sender not allowed', from);
270
+ return true;
271
+ }
272
+ return false;
273
+ }
274
+
275
+ private routable(
276
+ ev: Record<string, unknown>,
277
+ replay: boolean,
278
+ ): EventBase | null {
279
+ const rawType = ev.event ? (ev.event as { type?: string }).type : 'msg';
280
+ const evType = rawType === 'reply' ? 'msg' : rawType;
281
+ if (evType !== 'msg' && evType !== 'react' && evType !== 'system')
282
+ return null;
283
+ const station = str(ev.station);
284
+ if (!this.deps.getStations().has(station)) return null;
285
+ const from = str(ev.from);
286
+ const line = str(ev.line);
287
+ if (this.droppedSender(from, line)) return null;
288
+ const text = str(ev.text);
289
+ if (!replay && this.isDuplicate(station, line, evType, str(ev.messageId))) {
290
+ this.deps.log(
291
+ 'drop: duplicate (per-account) event',
292
+ evType,
293
+ station,
294
+ str(ev.messageId),
295
+ );
296
+ return null;
297
+ }
298
+ return { evType, station, from, line, text };
299
+ }
300
+
301
+ private async emitMessage(
302
+ ev: Record<string, unknown>,
303
+ base: EventBase,
304
+ ): Promise<void> {
305
+ if (base.evType === 'msg' && (await this.handlePermissionReply(base.text)))
306
+ return;
307
+ await this.notify('notifications/claude/channel', {
308
+ content:
309
+ base.evType === 'system'
310
+ ? buildWebhookNote(base.text, str(ev.lineName), ev.payload)
311
+ : base.text,
312
+ meta: {
313
+ line: base.line,
314
+ from: base.from,
315
+ station: base.station,
316
+ ...tsMeta(ev.ts),
317
+ message_id: str(ev.messageId),
318
+ line_name: str(ev.lineName),
319
+ from_name: str(ev.fromName),
320
+ ...displayNameMeta(ev.fromDisplayName),
321
+ },
322
+ });
323
+ }
324
+
325
+ async handleEvent(
326
+ ev: Record<string, unknown>,
327
+ replay = false,
328
+ ): Promise<void> {
329
+ if (await this.routeAttachment(ev)) return;
330
+
331
+ const base = this.routable(ev, replay);
332
+ if (!base) return;
333
+ if (base.evType !== 'system') this.lastLine = base.line;
334
+ if (base.line) {
335
+ this.allowedLines.add(base.line);
336
+ capSet(this.allowedLines, ALLOWED_LINES_MAX);
337
+ }
338
+
339
+ const atts = (ev.payload as { attachments?: PendingAtt[] } | undefined)
340
+ ?.attachments;
341
+ if (Array.isArray(atts) && atts.length) {
342
+ this.bufferAttachments(ev, base, atts);
343
+ return;
344
+ }
345
+
346
+ if (base.evType === 'react') {
347
+ await this.handleReact(ev, base);
348
+ return;
349
+ }
350
+
351
+ await this.emitMessage(ev, base);
352
+ }
353
+ }
354
+
355
+ interface EventBase {
356
+ evType: string;
357
+ station: string;
358
+ from: string;
359
+ line: string;
360
+ text: string;
361
+ }
362
+
363
+ function reactionEmoji(raw: unknown): string {
364
+ if (typeof raw === 'string') return raw;
365
+ const obj = raw as { name?: string; reaction?: string } | undefined;
366
+ return obj?.name ?? obj?.reaction ?? '';
367
+ }
@@ -0,0 +1,85 @@
1
+ import { stat } from 'node:fs/promises';
2
+
3
+ const MAX_INLINE_BYTES = 4 * 1024 * 1024;
4
+
5
+ export interface SavedMedia {
6
+ contentType?: string;
7
+ attachmentFor?: string;
8
+ attachmentPath?: string;
9
+ localPath?: string;
10
+ url?: string;
11
+ mime?: string;
12
+ name?: string;
13
+ kind?: string;
14
+ index?: number;
15
+ reason?: string;
16
+ }
17
+
18
+ export interface MediaNote {
19
+ content: string;
20
+ kind: string;
21
+ name: string;
22
+ path: string;
23
+ }
24
+
25
+ export function mediaKind(mime?: string, name?: string): string {
26
+ const m = (mime ?? '').toLowerCase();
27
+ if (m.startsWith('image/')) return 'image';
28
+ if (m.startsWith('video/')) return 'video';
29
+ if (m.startsWith('audio/')) return 'audio';
30
+ const n = name ?? '';
31
+ if (/\.(png|jpe?g|gif|webp|heic)$/i.test(n)) return 'image';
32
+ if (/\.(mp4|mov|webm|m4v)$/i.test(n)) return 'video';
33
+ if (/\.(m4a|mp3|ogg|wav)$/i.test(n)) return 'audio';
34
+ return 'file';
35
+ }
36
+
37
+ async function fileSize(path: string): Promise<number> {
38
+ try {
39
+ return (await stat(path)).size;
40
+ } catch {
41
+ return 0;
42
+ }
43
+ }
44
+
45
+ function howToRead(url: string | undefined, tooBig: boolean): string {
46
+ if (!url)
47
+ return 'That path is on the daemon host, not on yours - this file is readable only by an agent running there.';
48
+ if (tooBig)
49
+ return 'Large file - fetch the Public URL only if you need the bytes, and do not inline it. The path is on the daemon host, not on yours.';
50
+ return 'Fetch the Public URL to view it. The path is on the daemon host, so the Read tool works only for an agent running there.';
51
+ }
52
+
53
+ export async function buildMediaNote(
54
+ p: SavedMedia,
55
+ caption: string,
56
+ ): Promise<MediaNote | null> {
57
+ const path = p.attachmentPath ?? p.localPath;
58
+ if (!path) return null;
59
+ const kind = p.kind ?? mediaKind(p.mime, p.name);
60
+ const name = p.name ?? path.split('/').pop() ?? 'attachment';
61
+ const size = await fileSize(path);
62
+ const sizeNote = size ? ` (${(size / 1024 / 1024).toFixed(2)} MB)` : '';
63
+ const content =
64
+ (caption ? `${caption}\n` : '') +
65
+ `[${kind} attachment received: ${name}${p.mime ? `, ${p.mime}` : ''}${sizeNote}]\n` +
66
+ (p.url ? `Public URL: ${p.url}\n` : '') +
67
+ `Daemon-host path: ${path}\n` +
68
+ howToRead(p.url, size > MAX_INLINE_BYTES);
69
+ return { content, kind, name, path };
70
+ }
71
+
72
+ export function buildMediaFailureNote(
73
+ p: SavedMedia,
74
+ caption: string,
75
+ ): MediaNote {
76
+ const kind = p.kind ?? mediaKind(p.mime, p.name);
77
+ const name = p.name ?? 'attachment';
78
+ const reason = p.reason ?? 'no reason reported';
79
+ const content =
80
+ (caption ? `${caption}\n` : '') +
81
+ `[${kind} attachment could not be fetched: ${name}]\n` +
82
+ `Reason: ${reason}\n` +
83
+ 'The sender still has the file; nothing was saved. Ask them to resend it, or say why it could not be taken.';
84
+ return { content, kind, name, path: '' };
85
+ }
@@ -0,0 +1,75 @@
1
+ import { str } from '../mcp/str.js';
2
+
3
+ export interface PendingAtt {
4
+ kind?: string;
5
+ name?: string;
6
+ }
7
+
8
+ export interface PendingMsg {
9
+ line: string;
10
+ from: string;
11
+ station: string;
12
+ text: string;
13
+ ts: string;
14
+ messageId: string;
15
+ lineName: string;
16
+ fromName: string;
17
+ fromDisplayName: string;
18
+ attachments: PendingAtt[];
19
+ saved: Set<number>;
20
+ timer: ReturnType<typeof setTimeout>;
21
+ }
22
+
23
+ export interface MediaCtx {
24
+ line: string;
25
+ from: string;
26
+ station: string;
27
+ text?: string;
28
+ ts?: string;
29
+ messageId?: string;
30
+ lineName?: string;
31
+ fromName?: string;
32
+ fromDisplayName?: string;
33
+ }
34
+
35
+ export function capSet(set: Set<string>, max: number): void {
36
+ while (set.size > max) {
37
+ const oldest = set.values().next();
38
+ if (oldest.done) break;
39
+ set.delete(oldest.value);
40
+ }
41
+ }
42
+
43
+ export const tsMeta = (v: unknown): Record<string, string> => {
44
+ const ts = str(v);
45
+ return ts ? { ts } : {};
46
+ };
47
+
48
+ export const displayNameMeta = (v: unknown): Record<string, string> => {
49
+ const name = str(v);
50
+ return name ? { from_display_name: name } : {};
51
+ };
52
+
53
+ export const senderMeta = (c: MediaCtx): Record<string, string> => ({
54
+ ...tsMeta(c.ts),
55
+ ...(c.messageId ? { message_id: c.messageId } : {}),
56
+ ...(c.lineName ? { line_name: c.lineName } : {}),
57
+ ...(c.fromName ? { from_name: c.fromName } : {}),
58
+ ...displayNameMeta(c.fromDisplayName),
59
+ });
60
+
61
+ export function takeMediaCtx(buf: PendingMsg): MediaCtx {
62
+ const ctx: MediaCtx = {
63
+ line: buf.line,
64
+ from: buf.from,
65
+ station: buf.station,
66
+ text: buf.text,
67
+ ts: buf.ts,
68
+ messageId: buf.messageId,
69
+ lineName: buf.lineName,
70
+ fromName: buf.fromName,
71
+ fromDisplayName: buf.fromDisplayName,
72
+ };
73
+ buf.text = '';
74
+ return ctx;
75
+ }