@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,240 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { stationByName } from '../stations/registry.js';
3
+ import { errMsg, log } from '../daemon/log.js';
4
+ import { subscribeEvents, type MetroEvent } from '../daemon/events.js';
5
+ import { callTargetDenied, eventInScope } from '../db/agent-scope.js';
6
+ import { hasAnyKey } from '../db/key-map.js';
7
+ import {
8
+ allowedAgents,
9
+ authConfigFromEnv,
10
+ authenticate,
11
+ } from '../mcp/request-identity.js';
12
+
13
+ export type MonitorCall = (
14
+ train: string,
15
+ action: string,
16
+ args: Record<string, unknown>,
17
+ ) => Promise<{ result: unknown }>;
18
+
19
+ const KEEPALIVE_MS = 25_000;
20
+ const CALL_BODY_MAX = 256 * 1024;
21
+ const METRO_VERSION = process.env.npm_package_version ?? '0.1.0-beta.15';
22
+
23
+ function monitorEnabled(): boolean {
24
+ return hasAnyKey() || authConfigFromEnv().sessionSecret !== '';
25
+ }
26
+
27
+ function cors(req: IncomingMessage): Record<string, string> {
28
+ return {
29
+ 'access-control-allow-origin': req.headers.origin ?? '*',
30
+ 'access-control-allow-methods': 'GET, POST, OPTIONS',
31
+ 'access-control-allow-headers': 'Authorization, Content-Type',
32
+ 'access-control-max-age': '86400',
33
+ vary: 'Origin',
34
+ };
35
+ }
36
+
37
+ function sendJson(
38
+ res: ServerResponse,
39
+ req: IncomingMessage,
40
+ status: number,
41
+ body: unknown,
42
+ ): void {
43
+ res.writeHead(status, { 'content-type': 'application/json', ...cors(req) });
44
+ res.end(JSON.stringify(body));
45
+ }
46
+
47
+ function parseCallArgs(raw: string): Record<string, unknown> {
48
+ if (!raw) return {};
49
+ const parsed = JSON.parse(raw) as { args?: unknown };
50
+ const value =
51
+ parsed && typeof parsed === 'object' && 'args' in parsed
52
+ ? parsed.args
53
+ : parsed;
54
+ if (!value || typeof value !== 'object' || Array.isArray(value))
55
+ throw new Error('call body must be a JSON object');
56
+ return value as Record<string, unknown>;
57
+ }
58
+
59
+ async function readCallArgs(
60
+ req: IncomingMessage,
61
+ ): Promise<Record<string, unknown>> {
62
+ const chunks: Buffer[] = [];
63
+ let total = 0;
64
+ for await (const chunk of req) {
65
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as string);
66
+ total += buf.length;
67
+ if (total > CALL_BODY_MAX)
68
+ throw new Error(`request body exceeds ${CALL_BODY_MAX} bytes`);
69
+ chunks.push(buf);
70
+ }
71
+ return parseCallArgs(Buffer.concat(chunks).toString('utf8').trim());
72
+ }
73
+
74
+ function startTailStream(
75
+ req: IncomingMessage,
76
+ res: ServerResponse,
77
+ allowed: Set<string>,
78
+ ): void {
79
+ res.writeHead(200, {
80
+ 'content-type': 'text/event-stream',
81
+ 'cache-control': 'no-cache, no-transform',
82
+ connection: 'keep-alive',
83
+ 'x-accel-buffering': 'no',
84
+ ...cors(req),
85
+ });
86
+ const timers: { keepalive?: ReturnType<typeof setInterval> } = {};
87
+ const subs: { stop?: () => void } = {};
88
+ let done = false;
89
+ const cleanup = (): void => {
90
+ if (done) return;
91
+ done = true;
92
+ subs.stop?.();
93
+ if (timers.keepalive) clearInterval(timers.keepalive);
94
+ try {
95
+ res.end();
96
+ } catch {
97
+ log.debug('monitor: tail cleanup end failed');
98
+ }
99
+ };
100
+ const write = (chunk: string): void => {
101
+ if (done) return;
102
+ if (res.destroyed || res.writableEnded) {
103
+ cleanup();
104
+ return;
105
+ }
106
+ try {
107
+ res.write(chunk);
108
+ } catch (err) {
109
+ log.debug({ err: errMsg(err) }, 'monitor: tail write failed');
110
+ cleanup();
111
+ }
112
+ };
113
+ write(': metro monitor tail (live)\n\n');
114
+ let id = 0;
115
+ subs.stop = subscribeEvents((e: MetroEvent): void => {
116
+ if (!eventInScope(allowed, e.line)) return;
117
+ id += 1;
118
+ write(`id: ${id}\nevent: live\ndata: ${JSON.stringify(e)}\n\n`);
119
+ });
120
+ timers.keepalive = setInterval(() => {
121
+ write(': keepalive\n\n');
122
+ }, KEEPALIVE_MS);
123
+ timers.keepalive.unref?.();
124
+ req.on('close', cleanup);
125
+ req.on('error', cleanup);
126
+ }
127
+
128
+ async function handleCall(
129
+ req: IncomingMessage,
130
+ res: ServerResponse,
131
+ train: string,
132
+ action: string,
133
+ call: MonitorCall,
134
+ allowed: Set<string>,
135
+ ): Promise<void> {
136
+ let args: Record<string, unknown>;
137
+ try {
138
+ args = await readCallArgs(req);
139
+ } catch (err) {
140
+ sendJson(res, req, 400, { error: `bad JSON body: ${errMsg(err)}` });
141
+ return;
142
+ }
143
+ if (callTargetDenied(allowed, train, args)) {
144
+ sendJson(res, req, 403, {
145
+ error: 'metro: this account is outside your authorized scope',
146
+ });
147
+ return;
148
+ }
149
+ if (stationByName(train)?.hasTrain === false) {
150
+ sendJson(res, req, 400, {
151
+ error: `metro: station '${train}' runs in-core and takes no calls`,
152
+ });
153
+ return;
154
+ }
155
+ try {
156
+ const { result } = await call(train, action, args);
157
+ sendJson(res, req, 200, { result });
158
+ } catch (err) {
159
+ sendJson(res, req, 502, { error: errMsg(err) });
160
+ }
161
+ }
162
+
163
+ function routeApi(
164
+ req: IncomingMessage,
165
+ res: ServerResponse,
166
+ path: string,
167
+ call: MonitorCall,
168
+ allowed: Set<string>,
169
+ ): void {
170
+ const callMatch = /^\/api\/call\/([^/]+)\/([^/]+)$/.exec(path);
171
+ if (callMatch) {
172
+ if (req.method !== 'POST') {
173
+ sendJson(res, req, 405, { error: 'method not allowed' });
174
+ return;
175
+ }
176
+ handleCall(
177
+ req,
178
+ res,
179
+ callMatch[1] ?? '',
180
+ callMatch[2] ?? '',
181
+ call,
182
+ allowed,
183
+ ).catch((err: unknown) => {
184
+ log.warn({ err: errMsg(err) }, 'monitor: call handler error');
185
+ if (!res.headersSent) sendJson(res, req, 500, { error: errMsg(err) });
186
+ });
187
+ return;
188
+ }
189
+ if (path === '/api/tail') {
190
+ if (req.method !== 'GET') {
191
+ sendJson(res, req, 405, { error: 'method not allowed' });
192
+ return;
193
+ }
194
+ startTailStream(req, res, allowed);
195
+ return;
196
+ }
197
+ sendJson(res, req, 404, { error: 'not found' });
198
+ }
199
+
200
+ function preflight(
201
+ req: IncomingMessage,
202
+ res: ServerResponse,
203
+ path: string,
204
+ ): boolean {
205
+ if (!monitorEnabled()) {
206
+ sendJson(res, req, 404, { error: 'not found' });
207
+ return true;
208
+ }
209
+ if (req.method === 'OPTIONS') {
210
+ res.writeHead(204, cors(req)).end();
211
+ return true;
212
+ }
213
+ if (req.method === 'GET' && path === '/api/health') {
214
+ sendJson(res, req, 200, {
215
+ ok: true,
216
+ service: 'metro',
217
+ version: METRO_VERSION,
218
+ uptime_s: Math.round(process.uptime()),
219
+ });
220
+ return true;
221
+ }
222
+ return false;
223
+ }
224
+
225
+ export function handleMonitorRequest(
226
+ req: IncomingMessage,
227
+ res: ServerResponse,
228
+ call: MonitorCall,
229
+ ): boolean {
230
+ const path = (req.url ?? '').split('?', 2)[0] ?? '';
231
+ if (!path.startsWith('/api/')) return false;
232
+ if (preflight(req, res, path)) return true;
233
+ const identity = authenticate(req, authConfigFromEnv());
234
+ if (!identity) {
235
+ sendJson(res, req, 401, { error: 'unauthorized' });
236
+ return true;
237
+ }
238
+ routeApi(req, res, path, call, allowedAgents(identity));
239
+ return true;
240
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ import './daemon/boot.js';
@@ -0,0 +1,80 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { errMsg } from '../daemon/log.js';
3
+ import { chmodIfExists } from '../daemon/secure-fs.js';
4
+
5
+ export type Die = (msg: string) => never;
6
+
7
+ export interface MakeLoaderOpts<T> {
8
+ prefix: string;
9
+ file: string;
10
+ allowlistEnv: string[];
11
+ validate: (raw: T[], die: Die) => void;
12
+ }
13
+
14
+ export interface Loader<T> {
15
+ die: Die;
16
+ loadAccounts: () => T[];
17
+ }
18
+
19
+ export function makeAccountStore<T extends { id: string }>(
20
+ opts: MakeLoaderOpts<T>,
21
+ ): Loader<T> {
22
+ const die: Die = (msg) => {
23
+ process.stderr.write(`${opts.prefix}: ${msg}\n`);
24
+ process.exit(2);
25
+ };
26
+
27
+ let allowlistRaw: string | undefined;
28
+ for (const k of opts.allowlistEnv) {
29
+ allowlistRaw ??= process.env[k];
30
+ }
31
+ const allowlist = new Set(
32
+ (allowlistRaw ?? '')
33
+ .split(',')
34
+ .map((s) => s.trim())
35
+ .filter(Boolean),
36
+ );
37
+
38
+ function loadAccounts(): T[] {
39
+ if (!existsSync(opts.file))
40
+ return die(
41
+ `no accounts file ${opts.file} — the daemon writes it from the database at boot; set DATABASE_URL and populate the accounts table`,
42
+ );
43
+ chmodIfExists(opts.file);
44
+ let raw: T[];
45
+ try {
46
+ raw = JSON.parse(readFileSync(opts.file, 'utf8')) as T[];
47
+ } catch (e) {
48
+ return die(`bad ${opts.file}: ${errMsg(e)}`);
49
+ }
50
+ if (!Array.isArray(raw) || raw.length === 0)
51
+ die(`${opts.file} must be a non-empty array`);
52
+ opts.validate(raw, die);
53
+ const selected = allowlist.size
54
+ ? raw.filter((a) => allowlist.has(a.id))
55
+ : raw;
56
+ if (selected.length === 0) {
57
+ die(
58
+ `no accounts match ${opts.allowlistEnv[0]} (${[...allowlist].join(', ')})`,
59
+ );
60
+ }
61
+ return selected;
62
+ }
63
+
64
+ return { die, loadAccounts };
65
+ }
66
+
67
+ export function resolveAccountId(
68
+ accounts: Map<string, unknown>,
69
+ args: { account?: string; line?: string },
70
+ parseAccountId: (line: string) => string | undefined,
71
+ ): string {
72
+ let id = args.account;
73
+ id ??= args.line ? parseAccountId(args.line) : undefined;
74
+ id ??= (accounts.size === 1 ? [...accounts.keys()][0] : 'default') ?? 'default';
75
+ if (!accounts.has(id))
76
+ throw new Error(
77
+ `unknown account '${id}' (have: ${[...accounts.keys()].join(', ')})`,
78
+ );
79
+ return id;
80
+ }
@@ -0,0 +1,100 @@
1
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+
5
+ export const MAX_INLINE_BYTES = 8 * 1024 * 1024;
6
+
7
+ const INLINE_TEMP_PREFIX = 'metro-inline-';
8
+
9
+ const BASE64_RE = /^[A-Za-z0-9+/_-]*={0,2}$/;
10
+
11
+ const mib = (n: number): string => `${(n / (1024 * 1024)).toFixed(2)} MiB`;
12
+
13
+ const LIMIT_HINT =
14
+ `inline \`data\` is capped at ${mib(MAX_INLINE_BYTES)} (${MAX_INLINE_BYTES} bytes) of decoded ` +
15
+ 'bytes per attachment and per send, because the base64 travels inside the MCP request body. ' +
16
+ 'Pass `url`, or a `path` already on the daemon host, for anything larger.';
17
+
18
+ function assertInlineSize(bytes: number, label: string): void {
19
+ if (bytes > MAX_INLINE_BYTES)
20
+ throw new Error(`inline attachment ${label} is ${mib(bytes)}; ${LIMIT_HINT}`);
21
+ }
22
+
23
+ export function assertInlineTotal(bytes: number): void {
24
+ if (bytes > MAX_INLINE_BYTES)
25
+ throw new Error(
26
+ `inline attachments total ${mib(bytes)} in one send; ${LIMIT_HINT}`,
27
+ );
28
+ }
29
+
30
+ const padOf = (b64: string): number =>
31
+ b64.endsWith('==') ? 2 : b64.endsWith('=') ? 1 : 0;
32
+
33
+ export const decodedLengthOf = (b64: string): number =>
34
+ Math.max(0, Math.floor((b64.length * 3) / 4) - padOf(b64));
35
+
36
+ const malformed = (why: string): Error =>
37
+ new Error(
38
+ `inline \`data\` ${why}; pass the file's bytes base64-encoded ` +
39
+ '(standard or url-safe alphabet, optionally behind a `data:<mime>;base64,` prefix)',
40
+ );
41
+
42
+ const DATA_URL_RE = /^data:([^,]*),/i;
43
+
44
+ export interface InlineSource {
45
+ data: string;
46
+ mime?: string;
47
+ }
48
+
49
+ export function splitInlineData(raw: string): InlineSource {
50
+ const compact = raw.replace(/\s+/g, '');
51
+ const match = DATA_URL_RE.exec(compact);
52
+ if (!match) return { data: raw };
53
+ const params = (match[1] ?? '').split(';');
54
+ if (!params.slice(1).some((p) => p.toLowerCase() === 'base64'))
55
+ throw malformed('is a `data:` url whose payload is not base64');
56
+ const mime = params[0]?.toLowerCase();
57
+ const data = compact.slice(match[0].length);
58
+ return mime === undefined || mime === '' ? { data } : { data, mime };
59
+ }
60
+
61
+ export function decodeInline(raw: string, label: string): Uint8Array {
62
+ const b64 = raw.replace(/\s+/g, '');
63
+ if (!b64) throw malformed('is empty');
64
+ const expected = decodedLengthOf(b64);
65
+ assertInlineSize(expected, label);
66
+ if (!BASE64_RE.test(b64)) throw malformed('is not base64');
67
+ if (b64.length % 4 === 1 || (padOf(b64) > 0 && b64.length % 4 !== 0))
68
+ throw malformed('is not a whole number of base64 quanta');
69
+ const buf = Buffer.from(b64, 'base64');
70
+ if (buf.length !== expected)
71
+ throw malformed(`decoded to ${buf.length} bytes instead of ${expected}`);
72
+ return new Uint8Array(buf);
73
+ }
74
+
75
+ export const safeFileName = (name: string | undefined): string => {
76
+ const base = (name ?? '').split(/[\\/]/).pop() ?? '';
77
+ const clean = base
78
+ .replace(/[^A-Za-z0-9._-]/g, '_')
79
+ .replace(/^\.+/, '')
80
+ .slice(0, 64);
81
+ return clean || 'attachment';
82
+ };
83
+
84
+ export interface InlineTemp {
85
+ dir: string;
86
+ path: string;
87
+ }
88
+
89
+ export async function writeInlineTemp(
90
+ data: Uint8Array,
91
+ name: string | undefined,
92
+ ): Promise<InlineTemp> {
93
+ const dir = await mkdtemp(join(tmpdir(), INLINE_TEMP_PREFIX));
94
+ const path = join(dir, safeFileName(name));
95
+ await writeFile(path, data, { mode: 0o600 });
96
+ return { dir, path };
97
+ }
98
+
99
+ export const removeInlineTemp = (dir: string): Promise<void> =>
100
+ rm(dir, { recursive: true, force: true });
@@ -0,0 +1,161 @@
1
+ import { StationAttachError } from './attach.js';
2
+
3
+ export const INTERACTIVE_STATIONS = ['telegram-user', 'whatsapp'] as const;
4
+
5
+ export type InteractiveStation = (typeof INTERACTIVE_STATIONS)[number];
6
+
7
+ export type AttachStep = 'code' | 'password' | 'scan' | 'pair';
8
+
9
+ export interface AttachPrompt {
10
+ step: AttachStep;
11
+ prompt: string;
12
+ qr?: string;
13
+ pairingCode?: string;
14
+ }
15
+
16
+ export interface AttachOutcome {
17
+ config: Record<string, unknown>;
18
+ identity: Record<string, string>;
19
+ }
20
+
21
+ export interface DriverHooks {
22
+ prompt: (p: AttachPrompt) => void;
23
+ done: (o: AttachOutcome) => void;
24
+ fail: (message: string) => void;
25
+ }
26
+
27
+ export interface AttachDriver {
28
+ submit: (input: { code?: unknown; password?: unknown }) => Promise<void>;
29
+ cancel: () => Promise<void>;
30
+ }
31
+
32
+ export interface StartedAttach {
33
+ driver: AttachDriver;
34
+ prompt: AttachPrompt;
35
+ }
36
+
37
+ const CODE_PROMPT =
38
+ 'Telegram sent a login code to that number. Enter it here.';
39
+ const PASSWORD_PROMPT =
40
+ 'This account has two-step verification. Enter its password.';
41
+ const SCAN_PROMPT =
42
+ 'Open WhatsApp on your phone, go to Settings, Linked Devices, Link a Device, and scan this code.';
43
+ const PAIR_PROMPT =
44
+ 'Open WhatsApp on your phone, go to Settings, Linked Devices, Link a Device, Link with phone number, and type this code.';
45
+
46
+ export function isInteractiveStation(raw: unknown): raw is InteractiveStation {
47
+ return (
48
+ typeof raw === 'string' &&
49
+ (INTERACTIVE_STATIONS as readonly string[]).includes(raw)
50
+ );
51
+ }
52
+
53
+ function refuse(err: unknown, fallback: string): never {
54
+ throw new StationAttachError(
55
+ err instanceof Error && err.message !== '' ? err.message : fallback,
56
+ 400,
57
+ );
58
+ }
59
+
60
+ async function startTelegramUser(
61
+ input: Record<string, unknown>,
62
+ hooks: DriverHooks,
63
+ ): Promise<StartedAttach> {
64
+ const { TelegramUserLogin, TelegramUserLoginError, validateCredentials } =
65
+ await import('@metro-labs/telegram-user/login');
66
+ const isOwn = (e: unknown): boolean => e instanceof TelegramUserLoginError;
67
+ let login;
68
+ try {
69
+ login = new TelegramUserLogin(
70
+ validateCredentials({
71
+ apiId: input.apiId,
72
+ apiHash: input.apiHash,
73
+ phone: input.phone,
74
+ }),
75
+ );
76
+ await login.requestCode();
77
+ } catch (err) {
78
+ await login?.close();
79
+ refuse(isOwn(err) ? err : null, 'Telegram refused to start the sign-in');
80
+ }
81
+ const settled = login;
82
+ return {
83
+ prompt: { step: 'code', prompt: CODE_PROMPT },
84
+ driver: {
85
+ cancel: () => settled.close(),
86
+ submit: async (values) => {
87
+ try {
88
+ if (typeof values.password === 'string') {
89
+ hooks.done(await settled.submitPassword(values.password));
90
+ return;
91
+ }
92
+ if (typeof values.code !== 'string')
93
+ throw new StationAttachError('enter the login code', 400);
94
+ const result = await settled.submitCode(values.code);
95
+ if (result === 'password') {
96
+ hooks.prompt({ step: 'password', prompt: PASSWORD_PROMPT });
97
+ return;
98
+ }
99
+ hooks.done(result);
100
+ } catch (err) {
101
+ if (err instanceof StationAttachError) throw err;
102
+ refuse(isOwn(err) ? err : null, 'Telegram refused the sign-in');
103
+ }
104
+ },
105
+ },
106
+ };
107
+ }
108
+
109
+ async function startWhatsapp(
110
+ input: Record<string, unknown>,
111
+ hooks: DriverHooks,
112
+ ): Promise<StartedAttach> {
113
+ const { WhatsappLogin, WhatsappLoginError, normalizePhone } = await import(
114
+ '@metro-labs/whatsapp/login'
115
+ );
116
+ const isOwn = (e: unknown): boolean => e instanceof WhatsappLoginError;
117
+ let login;
118
+ try {
119
+ login = new WhatsappLogin(normalizePhone(input.phone), {
120
+ onQr: (qr) => {
121
+ hooks.prompt({ step: 'scan', prompt: SCAN_PROMPT, qr });
122
+ },
123
+ onPairingCode: (pairingCode) => {
124
+ hooks.prompt({ step: 'pair', prompt: PAIR_PROMPT, pairingCode });
125
+ },
126
+ onPaired: hooks.done,
127
+ onFailed: hooks.fail,
128
+ });
129
+ await login.start();
130
+ } catch (err) {
131
+ await login?.cancel();
132
+ refuse(isOwn(err) ? err : null, 'WhatsApp refused to start the pairing');
133
+ }
134
+ const settled = login;
135
+ const qrMode = settled.mode === 'qr';
136
+ return {
137
+ prompt: qrMode
138
+ ? { step: 'scan', prompt: SCAN_PROMPT }
139
+ : { step: 'pair', prompt: PAIR_PROMPT },
140
+ driver: {
141
+ cancel: () => settled.cancel(),
142
+ submit: () =>
143
+ Promise.reject(
144
+ new StationAttachError(
145
+ 'this pairing finishes on your phone, there is nothing to submit here',
146
+ 409,
147
+ ),
148
+ ),
149
+ },
150
+ };
151
+ }
152
+
153
+ export async function startInteractiveAttach(
154
+ station: InteractiveStation,
155
+ input: Record<string, unknown>,
156
+ hooks: DriverHooks,
157
+ ): Promise<StartedAttach> {
158
+ return station === 'telegram-user'
159
+ ? startTelegramUser(input, hooks)
160
+ : startWhatsapp(input, hooks);
161
+ }