@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,357 @@
1
+ import { handleSessionApis, type SessionApis } from './session-apis.js';
2
+ import {
3
+ createServer,
4
+ type IncomingMessage,
5
+ type Server,
6
+ type ServerResponse,
7
+ } from 'node:http';
8
+ import { Line } from '../stations/lines.js';
9
+ import { errMsg, log } from './log.js';
10
+ import {
11
+ classifyEvent,
12
+ formatDisplay,
13
+ mintId,
14
+ publishEvent,
15
+ userSelf,
16
+ type MetroEvent,
17
+ } from './events.js';
18
+ import type { TrainEvent } from './protocol.js';
19
+ import { agentForLine, agentIdForLine } from '../db/agent-map.js';
20
+ import {
21
+ findEndpointByWebhookId,
22
+ listEndpoints,
23
+ tokenMatches,
24
+ webhookPort,
25
+ type Endpoint,
26
+ } from './tunnel.js';
27
+ import { attachmentEventUrl, handleAttachRequest } from './attach-serve.js';
28
+ import { webhookEntry } from '@metro-labs/webhook';
29
+ import { handleGoogleAuthRequest } from './google-oauth.js';
30
+ import { handleUploadRequest } from './upload-api.js';
31
+ import {
32
+ handleMonitorRequest,
33
+ type MonitorCall,
34
+ } from '../monitor/api.js';
35
+ import { applyMcpCors, handleMcpPreflight } from './cors.js';
36
+
37
+ const LRU_CAP = 2_000;
38
+
39
+ const METRO_VERSION = process.env.npm_package_version ?? '0.1.0-beta.15';
40
+
41
+ function dedupKey(
42
+ e: Pick<MetroEvent, 'station' | 'line' | 'messageId'>,
43
+ ): string | null {
44
+ if (!e.messageId) return null;
45
+ return `${e.station} ${e.line} ${e.messageId}`;
46
+ }
47
+
48
+ export interface DedupSeq {
49
+ admit(entry: MetroEvent): number | null;
50
+ }
51
+
52
+ export function makeDedupSeq(): DedupSeq {
53
+ const seen = new Map<string, true>();
54
+ const seqByLine = new Map<string, number>();
55
+
56
+ log.info('dedup+seq: live-from-boot (no persisted seed)');
57
+
58
+ const isInbound = (e: MetroEvent): boolean => !Line.isLocal(e.from);
59
+
60
+ return {
61
+ admit(entry: MetroEvent): number | null {
62
+ const key = dedupKey(entry);
63
+ if (key && isInbound(entry)) {
64
+ if (seen.has(key)) {
65
+ log.debug(
66
+ {
67
+ station: entry.station,
68
+ line: entry.line,
69
+ messageId: entry.messageId,
70
+ },
71
+ 'dedup: dropped duplicate inbound message',
72
+ );
73
+ return null;
74
+ }
75
+ seen.set(key, true);
76
+ while (seen.size > LRU_CAP) {
77
+ const oldest = seen.keys().next();
78
+ if (oldest.done) break;
79
+ seen.delete(oldest.value);
80
+ }
81
+ }
82
+ const next = (seqByLine.get(entry.line) ?? 0) + 1;
83
+ seqByLine.set(entry.line, next);
84
+ while (seqByLine.size > LRU_CAP) {
85
+ const oldest = seqByLine.keys().next();
86
+ if (oldest.done) break;
87
+ seqByLine.delete(oldest.value);
88
+ }
89
+ return next;
90
+ },
91
+ };
92
+ }
93
+
94
+ function withAttachmentUrl(entry: MetroEvent): MetroEvent {
95
+ const payload = entry.payload;
96
+ if (!payload || typeof payload !== 'object') return entry;
97
+ const agentId = agentIdForLine(entry.line);
98
+ if (agentId === undefined) return entry;
99
+ const url = attachmentEventUrl(payload as Record<string, unknown>, agentId);
100
+ if (!url) return entry;
101
+ return { ...entry, payload: { ...payload, url } };
102
+ }
103
+
104
+ type Emit = (entry: MetroEvent) => void;
105
+
106
+ export function makeEmit(dedupSeq?: DedupSeq): Emit {
107
+ const tracker = dedupSeq ?? makeDedupSeq();
108
+ return function emit(entry: MetroEvent): void {
109
+ const seq = tracker.admit(entry);
110
+ if (seq === null) return;
111
+ const enriched: MetroEvent = withAttachmentUrl({
112
+ ...entry,
113
+ seq,
114
+ agent: entry.agent ?? agentForLine(entry.line),
115
+ display: entry.display ?? formatDisplay(entry),
116
+ event: entry.event ?? classifyEvent(entry),
117
+ });
118
+ process.stdout.write(JSON.stringify(enriched) + '\n');
119
+ publishEvent(enriched);
120
+ };
121
+ }
122
+
123
+ function eventText(env: TrainEvent): string | undefined {
124
+ if (env.text !== undefined) return env.text;
125
+ return env.emoji ? `[react ${env.emoji}]` : undefined;
126
+ }
127
+
128
+ export function trainEventToMetroEvent(
129
+ env: TrainEvent,
130
+ trainName: string,
131
+ ): MetroEvent | null {
132
+ const line = env.line;
133
+ if (typeof line !== 'string') {
134
+ log.warn({ train: trainName }, 'train: dropped event without `line`');
135
+ return null;
136
+ }
137
+ const station = env.station ?? Line.station(line) ?? trainName;
138
+ const isPrivate = env.is_private === true;
139
+ const text = eventText(env);
140
+ return {
141
+ event: env.event,
142
+ agent: agentForLine(line),
143
+ id: env.id ?? mintId(),
144
+ ts: env.ts ?? new Date().toISOString(),
145
+ station,
146
+ line: line as MetroEvent['line'],
147
+ lineName: env.line_name,
148
+ from: (env.from ?? `metro://${station}`) as MetroEvent['from'],
149
+ fromName: env.from_name,
150
+ fromDisplayName: env.from_display_name,
151
+ to: (env.to ?? (isPrivate ? userSelf() : line)) as MetroEvent['to'],
152
+ text,
153
+ messageId: env.message_id,
154
+ replyTo: env.reply_to,
155
+ payload: env.payload,
156
+ };
157
+ }
158
+
159
+ type McpHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>;
160
+
161
+ export async function startWebhookServer(
162
+ emit: Emit,
163
+ apis: SessionApis = {},
164
+ mcp?: McpHandler,
165
+ monitorCall?: MonitorCall,
166
+ ): Promise<Server> {
167
+ const port = webhookPort();
168
+ const server = createServer((req, res) => {
169
+ handleRequest(req, res, emit, apis, mcp, monitorCall).catch(
170
+ (err: unknown) => {
171
+ log.warn({ err: errMsg(err) }, 'webhook handler error');
172
+ if (!res.headersSent) res.writeHead(500).end();
173
+ });
174
+ });
175
+ await new Promise<void>((resolve, reject) => {
176
+ server.once('error', reject);
177
+ const host = process.env.METRO_HTTP_HOST ?? '127.0.0.1';
178
+ server.listen(port, host, () => {
179
+ log.info(
180
+ {
181
+ host,
182
+ port,
183
+ endpoints: listEndpoints().length,
184
+ mcp: mcp ? '/' : 'off',
185
+ },
186
+ 'webhook + mcp ready',
187
+ );
188
+ resolve();
189
+ });
190
+ });
191
+ return server;
192
+ }
193
+
194
+ function isMcpPath(req: IncomingMessage): boolean {
195
+ const path = (req.url ?? '').split('?')[0];
196
+ return path === '/' || path === '/mcp';
197
+ }
198
+
199
+ export const WEBHOOK_BODY_MAX = 25 * 1024 * 1024;
200
+
201
+ export class BodyTooLargeError extends Error {
202
+ constructor(readonly limit: number) {
203
+ super(`request body exceeds ${limit} bytes`);
204
+ }
205
+ }
206
+
207
+ export async function readBody(
208
+ req: IncomingMessage,
209
+ maxBytes: number,
210
+ ): Promise<Buffer> {
211
+ const chunks: Buffer[] = [];
212
+ let total = 0;
213
+ for await (const c of req) {
214
+ const buf = c as Buffer;
215
+ total += buf.length;
216
+ if (total > maxBytes) throw new BodyTooLargeError(maxBytes);
217
+ chunks.push(buf);
218
+ }
219
+ return Buffer.concat(chunks);
220
+ }
221
+
222
+ function flatHeaders(req: IncomingMessage): Record<string, string> {
223
+ return Object.fromEntries(
224
+ Object.entries(req.headers).map(([k, v]) => [
225
+ k,
226
+ Array.isArray(v) ? v.join(',') : (v ?? ''),
227
+ ]),
228
+ );
229
+ }
230
+
231
+ function parseBody(raw: Buffer): unknown {
232
+ const text = raw.toString('utf8');
233
+ try {
234
+ return JSON.parse(text);
235
+ } catch {
236
+ return text;
237
+ }
238
+ }
239
+
240
+ async function handleWebhookPost(
241
+ req: IncomingMessage,
242
+ res: ServerResponse,
243
+ emit: Emit,
244
+ endpointId: string,
245
+ endpoint: Endpoint,
246
+ ): Promise<void> {
247
+ let raw: Buffer;
248
+ try {
249
+ raw = await readBody(req, WEBHOOK_BODY_MAX);
250
+ } catch (err) {
251
+ if (!(err instanceof BodyTooLargeError)) throw err;
252
+ log.warn({ endpoint: endpointId, limit: err.limit }, 'webhook body too large — rejecting');
253
+ res.writeHead(413).end('payload too large');
254
+ return;
255
+ }
256
+ const body = parseBody(raw);
257
+ emit(
258
+ webhookEntry(
259
+ endpoint,
260
+ flatHeaders(req),
261
+ body,
262
+ req.method ?? 'POST',
263
+ hookPath(endpointId),
264
+ ),
265
+ );
266
+ res.writeHead(200).end('ok');
267
+ }
268
+
269
+ function handleHealth(req: IncomingMessage, res: ServerResponse): boolean {
270
+ const reqPath = (req.url ?? '').split('?')[0];
271
+ if (reqPath !== '/health' && reqPath !== '/healthz') return false;
272
+ res.writeHead(200, { 'content-type': 'application/json' }).end(
273
+ JSON.stringify({
274
+ status: 'ok',
275
+ version: METRO_VERSION,
276
+ uptime: Math.round(process.uptime()),
277
+ }),
278
+ );
279
+ return true;
280
+ }
281
+
282
+ const HOOK_PATH =
283
+ /^\/api\/webhooks\/([0-9]{17,20})\/([A-Za-z0-9_-]{32,128})$/;
284
+
285
+ const HOOK_PREFIX = '/api/webhooks/';
286
+
287
+ const hookPath = (webhookId: string): string => `${HOOK_PREFIX}${webhookId}`;
288
+
289
+ function hookTarget(
290
+ path: string,
291
+ ): { id: string; endpoint: Endpoint } | null {
292
+ const m = HOOK_PATH.exec(path);
293
+ const webhookId = m?.[1];
294
+ const token = m?.[2];
295
+ if (webhookId === undefined || token === undefined) return null;
296
+ const endpoint = findEndpointByWebhookId(webhookId);
297
+ if (!endpoint?.secret || !tokenMatches(endpoint.secret, token)) return null;
298
+ return { id: webhookId, endpoint };
299
+ }
300
+
301
+ async function handleWebhookRoute(
302
+ req: IncomingMessage,
303
+ res: ServerResponse,
304
+ emit: Emit,
305
+ ): Promise<boolean> {
306
+ const path = (req.url ?? '').split('?')[0] ?? '';
307
+ if (!path.startsWith(HOOK_PREFIX)) return false;
308
+ const target = hookTarget(path);
309
+ if (target === null) {
310
+ res.writeHead(404).end();
311
+ return true;
312
+ }
313
+ if (req.method === 'GET') {
314
+ res.writeHead(200).end(`metro webhook ${target.id} ready\n`);
315
+ return true;
316
+ }
317
+ if (req.method !== 'POST') {
318
+ res.writeHead(405).end();
319
+ return true;
320
+ }
321
+ await handleWebhookPost(req, res, emit, target.id, target.endpoint);
322
+ return true;
323
+ }
324
+
325
+ async function handlePreMcpRoutes(
326
+ req: IncomingMessage,
327
+ res: ServerResponse,
328
+ emit: Emit,
329
+ apis: SessionApis,
330
+ monitorCall?: MonitorCall,
331
+ ): Promise<boolean> {
332
+ if (handleHealth(req, res)) return true;
333
+ if (await handleGoogleAuthRequest(req, res)) return true;
334
+ if (handleSessionApis(req, res, apis)) return true;
335
+ if (handleUploadRequest(req, res)) return true;
336
+ if (handleAttachRequest(req, res)) return true;
337
+ if (await handleWebhookRoute(req, res, emit)) return true;
338
+ return Boolean(monitorCall && handleMonitorRequest(req, res, monitorCall));
339
+ }
340
+
341
+ async function handleRequest(
342
+ req: IncomingMessage,
343
+ res: ServerResponse,
344
+ emit: Emit,
345
+ apis: SessionApis = {},
346
+ mcp?: McpHandler,
347
+ monitorCall?: MonitorCall,
348
+ ): Promise<void> {
349
+ if (await handlePreMcpRoutes(req, res, emit, apis, monitorCall)) return;
350
+ if (mcp && isMcpPath(req)) {
351
+ applyMcpCors(req, res);
352
+ if (handleMcpPreflight(req, res)) return;
353
+ await mcp(req, res);
354
+ return;
355
+ }
356
+ res.writeHead(404).end();
357
+ }
@@ -0,0 +1,79 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { errMsg, log } from './log.js';
3
+ import { Line } from '../stations/lines.js';
4
+
5
+ const TTL_MS = 5_000;
6
+ type Cache = { id: string; at: number } | null;
7
+
8
+ function memo(loader: () => string): () => string {
9
+ let cache: Cache = null;
10
+ return () => {
11
+ if (cache && Date.now() - cache.at < TTL_MS) return cache.id;
12
+ const id = loader();
13
+ cache = { id, at: Date.now() };
14
+ return id;
15
+ };
16
+ }
17
+
18
+ const claudeAccountId = memo(() => {
19
+ let raw: string;
20
+ try {
21
+ raw = execFileSync('claude', ['auth', 'status', '--json'], {
22
+ encoding: 'utf8',
23
+ stdio: ['ignore', 'pipe', 'pipe'],
24
+ });
25
+ } catch (e) {
26
+ throw new Error(
27
+ `metro: failed to run 'claude auth status --json' — is Claude Code installed? (${errMsg(e)})`,
28
+ );
29
+ }
30
+ let p: { loggedIn?: boolean; orgId?: string };
31
+ try {
32
+ p = JSON.parse(raw) as { loggedIn?: boolean; orgId?: string };
33
+ } catch {
34
+ throw new Error(
35
+ `metro: 'claude auth status --json' returned non-JSON: ${raw.slice(0, 200)}`,
36
+ );
37
+ }
38
+ if (!p.loggedIn || !p.orgId)
39
+ throw new Error(
40
+ "metro: Claude Code is not logged in — run 'claude auth login'",
41
+ );
42
+ return p.orgId;
43
+ });
44
+
45
+ export const claudeUserId = (): string =>
46
+ process.env.METRO_USER_ID ?? claudeAccountId();
47
+
48
+ export const claudeSessionId = (): string | null =>
49
+ process.env.METRO_USER_SESSION_ID ??
50
+ process.env.CLAUDE_CODE_SESSION_ID ??
51
+ null;
52
+
53
+ export function userSelf(): Line {
54
+ const explicit = process.env.METRO_FROM;
55
+ if (explicit) return explicit as Line;
56
+ if (!process.env.CLAUDECODE) return 'metro://user' as Line;
57
+ try {
58
+ return Line.user('claude', claudeUserId());
59
+ } catch (e) {
60
+ log.warn(
61
+ { reason: errMsg(e) },
62
+ 'could not resolve a Claude account id; relaying as metro://user',
63
+ );
64
+ return 'metro://user' as Line;
65
+ }
66
+ }
67
+
68
+ export function daemonSelf(): Line {
69
+ const explicit = process.env.METRO_FROM ?? process.env.METRO_SELF_URI;
70
+ return (explicit ?? 'metro://user') as Line;
71
+ }
72
+
73
+ export function selfLine(): Line | null {
74
+ if (process.env.CLAUDECODE) {
75
+ const s = claudeSessionId();
76
+ return s ? Line.claude(claudeUserId(), s) : null;
77
+ }
78
+ return null;
79
+ }
@@ -0,0 +1,42 @@
1
+ import { writeSync } from 'node:fs';
2
+ import pino from 'pino';
3
+ import pinoPretty from 'pino-pretty';
4
+
5
+ const stream = process.stderr.isTTY
6
+ ? pinoPretty({
7
+ colorize: true,
8
+ translateTime: 'HH:MM:ss',
9
+ ignore: 'pid,hostname,name',
10
+ destination: 2,
11
+ })
12
+ : pino.destination(2);
13
+
14
+ export const log = pino(
15
+ { base: { name: 'metro' }, level: process.env.METRO_LOG_LEVEL ?? 'info' },
16
+ stream,
17
+ );
18
+
19
+ export const logFatalSync = (
20
+ fields: Record<string, unknown>,
21
+ msg: string,
22
+ ): void => {
23
+ const line = JSON.stringify({
24
+ level: 60,
25
+ time: Date.now(),
26
+ name: 'metro',
27
+ ...fields,
28
+ msg,
29
+ });
30
+ try {
31
+ writeSync(2, `${line}\n`);
32
+ } catch {
33
+ return;
34
+ }
35
+ };
36
+
37
+ export const errMsg = (err: unknown): string => {
38
+ if (err instanceof Error) return err.message;
39
+ if (err && typeof err === 'object' && 'message' in err)
40
+ return String(err.message);
41
+ return String(err);
42
+ };
@@ -0,0 +1,162 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import {
3
+ ConnectorVerifyError,
4
+ type OAuthTokens,
5
+ } from './connector-verify.js';
6
+ import type { OAuthServer } from './oauth-discovery.js';
7
+
8
+ const TOKEN_TIMEOUT_MS = 15_000;
9
+ const CLIENT_NAME = 'Metro';
10
+
11
+ export interface OAuthClient {
12
+ clientId: string;
13
+ clientSecret?: string;
14
+ }
15
+
16
+ function refused(message: string): ConnectorVerifyError {
17
+ return new ConnectorVerifyError(message, 400);
18
+ }
19
+
20
+ function isRecord(value: unknown): value is Record<string, unknown> {
21
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
22
+ }
23
+
24
+ export function newVerifier(): string {
25
+ return randomBytes(32).toString('base64url');
26
+ }
27
+
28
+ export function challengeOf(verifier: string): string {
29
+ return createHash('sha256').update(verifier).digest('base64url');
30
+ }
31
+
32
+ export async function registerClient(
33
+ server: OAuthServer,
34
+ redirectUri: string,
35
+ ): Promise<OAuthClient> {
36
+ if (server.registrationEndpoint === null)
37
+ throw refused(
38
+ 'That server requires OAuth but does not accept client registration, so Metro cannot sign in to it.',
39
+ );
40
+ let res: Response;
41
+ try {
42
+ res = await fetch(server.registrationEndpoint, {
43
+ method: 'POST',
44
+ redirect: 'manual',
45
+ signal: AbortSignal.timeout(TOKEN_TIMEOUT_MS),
46
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
47
+ body: JSON.stringify({
48
+ client_name: CLIENT_NAME,
49
+ redirect_uris: [redirectUri],
50
+ grant_types: ['authorization_code', 'refresh_token'],
51
+ response_types: ['code'],
52
+ token_endpoint_auth_method: 'none',
53
+ }),
54
+ });
55
+ } catch {
56
+ throw refused('Metro could not reach that server to register.');
57
+ }
58
+ const body: unknown = await res.json().catch(() => null);
59
+ if (!res.ok || !isRecord(body) || typeof body.client_id !== 'string')
60
+ throw refused('That server refused to register Metro as a client.');
61
+ const secret = body.client_secret;
62
+ return {
63
+ clientId: body.client_id,
64
+ ...(typeof secret === 'string' ? { clientSecret: secret } : {}),
65
+ };
66
+ }
67
+
68
+ export interface AuthorizeInput {
69
+ server: OAuthServer;
70
+ client: OAuthClient;
71
+ redirectUri: string;
72
+ state: string;
73
+ verifier: string;
74
+ resource: string;
75
+ }
76
+
77
+ export function authorizeUrl(input: AuthorizeInput): string {
78
+ const url = new URL(input.server.authorizationEndpoint);
79
+ url.searchParams.set('response_type', 'code');
80
+ url.searchParams.set('client_id', input.client.clientId);
81
+ url.searchParams.set('redirect_uri', input.redirectUri);
82
+ url.searchParams.set('state', input.state);
83
+ url.searchParams.set('code_challenge', challengeOf(input.verifier));
84
+ url.searchParams.set('code_challenge_method', 'S256');
85
+ url.searchParams.set('resource', input.resource);
86
+ return url.toString();
87
+ }
88
+
89
+ function tokensFrom(body: Record<string, unknown>): OAuthTokens {
90
+ const access = body.access_token;
91
+ if (typeof access !== 'string' || access === '')
92
+ throw refused('That server did not return an access token.');
93
+ const refresh = body.refresh_token;
94
+ const expires = body.expires_in;
95
+ return {
96
+ accessToken: access,
97
+ ...(typeof refresh === 'string' ? { refreshToken: refresh } : {}),
98
+ ...(typeof expires === 'number'
99
+ ? { expiresAt: Date.now() + expires * 1000 }
100
+ : {}),
101
+ };
102
+ }
103
+
104
+ async function postToken(
105
+ server: OAuthServer,
106
+ client: OAuthClient,
107
+ form: Record<string, string>,
108
+ ): Promise<OAuthTokens> {
109
+ const params = new URLSearchParams({
110
+ ...form,
111
+ client_id: client.clientId,
112
+ ...(client.clientSecret === undefined
113
+ ? {}
114
+ : { client_secret: client.clientSecret }),
115
+ });
116
+ let res: Response;
117
+ try {
118
+ res = await fetch(server.tokenEndpoint, {
119
+ method: 'POST',
120
+ redirect: 'manual',
121
+ signal: AbortSignal.timeout(TOKEN_TIMEOUT_MS),
122
+ headers: {
123
+ 'content-type': 'application/x-www-form-urlencoded',
124
+ accept: 'application/json',
125
+ },
126
+ body: params.toString(),
127
+ });
128
+ } catch {
129
+ throw refused('Metro could not reach that server to finish signing in.');
130
+ }
131
+ const body: unknown = await res.json().catch(() => null);
132
+ if (!res.ok || !isRecord(body))
133
+ throw refused('That server refused to complete the sign-in.');
134
+ return tokensFrom(body);
135
+ }
136
+
137
+ export function exchangeCode(
138
+ server: OAuthServer,
139
+ client: OAuthClient,
140
+ input: { code: string; redirectUri: string; verifier: string; resource: string },
141
+ ): Promise<OAuthTokens> {
142
+ return postToken(server, client, {
143
+ grant_type: 'authorization_code',
144
+ code: input.code,
145
+ redirect_uri: input.redirectUri,
146
+ code_verifier: input.verifier,
147
+ resource: input.resource,
148
+ });
149
+ }
150
+
151
+ export function refreshTokens(
152
+ server: OAuthServer,
153
+ client: OAuthClient,
154
+ refreshToken: string,
155
+ resource: string,
156
+ ): Promise<OAuthTokens> {
157
+ return postToken(server, client, {
158
+ grant_type: 'refresh_token',
159
+ refresh_token: refreshToken,
160
+ resource,
161
+ });
162
+ }