@stage-labs/metro 0.1.0-beta.3 → 0.1.0-beta.30

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 (236) hide show
  1. package/dist/api.js +131 -0
  2. package/dist/bedrock-eventstream.js +120 -0
  3. package/dist/bedrock-proxy.js +260 -0
  4. package/dist/bedrock-upstream.js +72 -0
  5. package/dist/bedrock.js +104 -0
  6. package/dist/claude.js +15 -0
  7. package/dist/cli.js +157 -253
  8. package/dist/control.js +158 -0
  9. package/dist/plugin.js +82 -0
  10. package/dist/prompt.js +61 -0
  11. package/dist/runtime.js +94 -0
  12. package/dist/store.js +70 -0
  13. package/dist/tail.js +120 -0
  14. package/dist/update.js +33 -0
  15. package/dist/version.js +97 -0
  16. package/package.json +34 -21
  17. package/runtime/node_modules/@metro-labs/discord-bot/package.json +28 -0
  18. package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +119 -0
  19. package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +388 -0
  20. package/runtime/node_modules/@metro-labs/discord-bot/src/api-base.ts +1 -0
  21. package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +29 -0
  22. package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +210 -0
  23. package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +50 -0
  24. package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +115 -0
  25. package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +127 -0
  26. package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +215 -0
  27. package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +43 -0
  28. package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +19 -0
  29. package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +82 -0
  30. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-speak.ts +195 -0
  31. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-transcribe.ts +292 -0
  32. package/runtime/node_modules/@metro-labs/discord-bot/src/voice.ts +197 -0
  33. package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +5 -0
  34. package/runtime/node_modules/@metro-labs/mcp/package.json +66 -0
  35. package/runtime/node_modules/@metro-labs/mcp/src/channels/dedupe.ts +24 -0
  36. package/runtime/node_modules/@metro-labs/mcp/src/channels/inbound.ts +367 -0
  37. package/runtime/node_modules/@metro-labs/mcp/src/channels/media-note.ts +85 -0
  38. package/runtime/node_modules/@metro-labs/mcp/src/channels/pending.ts +75 -0
  39. package/runtime/node_modules/@metro-labs/mcp/src/channels/relay.ts +129 -0
  40. package/runtime/node_modules/@metro-labs/mcp/src/channels/webhook-note.ts +51 -0
  41. package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +311 -0
  42. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +337 -0
  43. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-error.ts +8 -0
  44. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +129 -0
  45. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +99 -0
  46. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-owner.ts +32 -0
  47. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +129 -0
  48. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +278 -0
  49. package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +321 -0
  50. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +123 -0
  51. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair.ts +20 -0
  52. package/runtime/node_modules/@metro-labs/mcp/src/daemon/collection-api.ts +202 -0
  53. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +339 -0
  54. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-json.ts +21 -0
  55. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth-routes.ts +157 -0
  56. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +127 -0
  57. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +100 -0
  58. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +57 -0
  59. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +333 -0
  60. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cors.ts +29 -0
  61. package/runtime/node_modules/@metro-labs/mcp/src/daemon/crash-guard.ts +49 -0
  62. package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +117 -0
  63. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-auth.ts +164 -0
  64. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-oauth.ts +208 -0
  65. package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +359 -0
  66. package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +79 -0
  67. package/runtime/node_modules/@metro-labs/mcp/src/daemon/log.ts +42 -0
  68. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +162 -0
  69. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +120 -0
  70. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +65 -0
  71. package/runtime/node_modules/@metro-labs/mcp/src/daemon/pair-codes.ts +58 -0
  72. package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +134 -0
  73. package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +214 -0
  74. package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +224 -0
  75. package/runtime/node_modules/@metro-labs/mcp/src/daemon/relay.ts +302 -0
  76. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +130 -0
  77. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-pair.ts +18 -0
  78. package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-source.ts +105 -0
  79. package/runtime/node_modules/@metro-labs/mcp/src/daemon/secure-fs.ts +57 -0
  80. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +27 -0
  81. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +43 -0
  82. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +218 -0
  83. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor-io.ts +97 -0
  84. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor.ts +323 -0
  85. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-call.ts +22 -0
  86. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-error.ts +53 -0
  87. package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +169 -0
  88. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +295 -0
  89. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-store.ts +181 -0
  90. package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +91 -0
  91. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +215 -0
  92. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-map.ts +71 -0
  93. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-scope.ts +64 -0
  94. package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +37 -0
  95. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-collections.ts +218 -0
  96. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +147 -0
  97. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +126 -0
  98. package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +308 -0
  99. package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +19 -0
  100. package/runtime/node_modules/@metro-labs/mcp/src/db/key-map.ts +45 -0
  101. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +382 -0
  102. package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +300 -0
  103. package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +31 -0
  104. package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +105 -0
  105. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +131 -0
  106. package/runtime/node_modules/@metro-labs/mcp/src/db/token-carry.ts +45 -0
  107. package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +57 -0
  108. package/runtime/node_modules/@metro-labs/mcp/src/db/whatsapp-login.ts +18 -0
  109. package/runtime/node_modules/@metro-labs/mcp/src/mcp/accounts.ts +145 -0
  110. package/runtime/node_modules/@metro-labs/mcp/src/mcp/call-tools.ts +203 -0
  111. package/runtime/node_modules/@metro-labs/mcp/src/mcp/channel-owner.ts +29 -0
  112. package/runtime/node_modules/@metro-labs/mcp/src/mcp/ctx.ts +62 -0
  113. package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +91 -0
  114. package/runtime/node_modules/@metro-labs/mcp/src/mcp/frame-scope.ts +21 -0
  115. package/runtime/node_modules/@metro-labs/mcp/src/mcp/group-tools.ts +129 -0
  116. package/runtime/node_modules/@metro-labs/mcp/src/mcp/index.ts +192 -0
  117. package/runtime/node_modules/@metro-labs/mcp/src/mcp/member-tools.ts +72 -0
  118. package/runtime/node_modules/@metro-labs/mcp/src/mcp/permission-relay.ts +66 -0
  119. package/runtime/node_modules/@metro-labs/mcp/src/mcp/raw-get-stream.ts +207 -0
  120. package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +73 -0
  121. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-registry.ts +172 -0
  122. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +43 -0
  123. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session.ts +211 -0
  124. package/runtime/node_modules/@metro-labs/mcp/src/mcp/str.ts +5 -0
  125. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-dispatch.ts +212 -0
  126. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +397 -0
  127. package/runtime/node_modules/@metro-labs/mcp/src/mcp/upload-tool.ts +66 -0
  128. package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +240 -0
  129. package/runtime/node_modules/@metro-labs/mcp/src/server.ts +2 -0
  130. package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +80 -0
  131. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-inline.ts +100 -0
  132. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +161 -0
  133. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +200 -0
  134. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-xmtp.ts +111 -0
  135. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +182 -0
  136. package/runtime/node_modules/@metro-labs/mcp/src/stations/attachments.ts +196 -0
  137. package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +82 -0
  138. package/runtime/node_modules/@metro-labs/mcp/src/stations/messaging-normalize.ts +88 -0
  139. package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +35 -0
  140. package/runtime/node_modules/@metro-labs/mcp/src/stations/station-runtime.ts +64 -0
  141. package/runtime/node_modules/@metro-labs/mcp/src/stations/types.ts +107 -0
  142. package/runtime/node_modules/@metro-labs/telegram/package.json +28 -0
  143. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +81 -0
  144. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +254 -0
  145. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +87 -0
  146. package/runtime/node_modules/@metro-labs/telegram/src/client.ts +77 -0
  147. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +141 -0
  148. package/runtime/node_modules/@metro-labs/telegram/src/group.ts +261 -0
  149. package/runtime/node_modules/@metro-labs/telegram/src/history.ts +52 -0
  150. package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +78 -0
  151. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +46 -0
  152. package/runtime/node_modules/@metro-labs/telegram/src/login.ts +166 -0
  153. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +80 -0
  154. package/runtime/node_modules/@metro-labs/telegram/src/members.ts +75 -0
  155. package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +22 -0
  156. package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +167 -0
  157. package/runtime/node_modules/@metro-labs/telegram/src/station.ts +24 -0
  158. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +7 -0
  159. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -0
  160. package/runtime/node_modules/@metro-labs/telegram-bot/package.json +25 -0
  161. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +120 -0
  162. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +295 -0
  163. package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
  164. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +172 -0
  165. package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +103 -0
  166. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
  167. package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
  168. package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +17 -0
  169. package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +48 -0
  170. package/runtime/node_modules/@metro-labs/telegram-bot/src/verify.ts +38 -0
  171. package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +3 -0
  172. package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
  173. package/runtime/node_modules/@metro-labs/webhook/src/station.ts +40 -0
  174. package/runtime/node_modules/@metro-labs/whatsapp/package.json +29 -0
  175. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +67 -0
  176. package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
  177. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +178 -0
  178. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
  179. package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
  180. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +318 -0
  181. package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
  182. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +132 -0
  183. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
  184. package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
  185. package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
  186. package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
  187. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
  188. package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
  189. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
  190. package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
  191. package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +141 -0
  192. package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +17 -0
  193. package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
  194. package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
  195. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
  196. package/runtime/node_modules/@metro-labs/xmtp/package.json +33 -0
  197. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +113 -0
  198. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
  199. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
  200. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
  201. package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
  202. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +375 -0
  203. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
  204. package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
  205. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
  206. package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
  207. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +333 -0
  208. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +78 -0
  209. package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
  210. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
  211. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +33 -0
  212. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
  213. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
  214. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
  215. package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
  216. package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +13 -0
  217. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
  218. package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
  219. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
  220. package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +84 -0
  221. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +39 -0
  222. package/runtime/runtime.json +3 -0
  223. package/runtime/trains/.keep +0 -0
  224. package/LICENSE +0 -21
  225. package/README.md +0 -114
  226. package/dist/agents/claude.js +0 -229
  227. package/dist/agents/codex.js +0 -282
  228. package/dist/agents/types.js +0 -3
  229. package/dist/channels/discord.js +0 -142
  230. package/dist/channels/telegram.js +0 -209
  231. package/dist/lib/dotenv.js +0 -31
  232. package/dist/lib/scope-cache.js +0 -85
  233. package/dist/lib/streaming.js +0 -207
  234. package/dist/log.js +0 -12
  235. package/dist/orchestrator.js +0 -399
  236. package/dist/paths.js +0 -33
@@ -0,0 +1,224 @@
1
+ import { readdirSync, statSync } from 'node:fs';
2
+ import { join, parse as parsePath } from 'node:path';
3
+ import { errMsg, log } from './log.js';
4
+ import { coerceErrorInfo, type TrainErrorInfo } from './train-error.js';
5
+ import type { StructuredEvent } from './events.js';
6
+ import type { CallMsg } from '../stations/station-runtime.js';
7
+
8
+ export {
9
+ TrainError,
10
+ serializeTrainError,
11
+ type TrainErrorInfo,
12
+ } from './train-error.js';
13
+
14
+ export const CALL_TIMEOUT_MS = 60_000;
15
+
16
+ export interface Pending {
17
+ resolve: (r: TrainCallResponse) => void;
18
+ reject: (err: Error) => void;
19
+ timer: ReturnType<typeof setTimeout>;
20
+ }
21
+ export interface CallTarget {
22
+ name: string;
23
+ pending: Map<string, Pending>;
24
+ proc: ({ stdin?: unknown } & Record<string, unknown>) | null;
25
+ }
26
+
27
+ export const STDOUT_LINE_MAX = 4 * 1024 * 1024;
28
+
29
+ export type TrainEvent = {
30
+ station?: string;
31
+ line?: string;
32
+ line_name?: string;
33
+ from?: string;
34
+ from_name?: string;
35
+ from_display_name?: string;
36
+ to?: string;
37
+ message_id?: string;
38
+ reply_to?: string;
39
+ is_private?: boolean;
40
+ text?: string;
41
+ emoji?: string;
42
+ payload?: unknown;
43
+ ts?: string;
44
+ id?: string;
45
+ event?: StructuredEvent;
46
+ } & Record<string, unknown>;
47
+
48
+ export interface TrainCallResponse {
49
+ result?: unknown;
50
+ error?: string;
51
+ errorInfo?: TrainErrorInfo;
52
+ }
53
+
54
+ export type TrainMessage =
55
+ | {
56
+ op: 'response';
57
+ id: string;
58
+ result?: unknown;
59
+ error?: string;
60
+ errorInfo?: TrainErrorInfo;
61
+ }
62
+ | { op: 'log'; text?: string }
63
+ | { op: 'event'; event: TrainEvent }
64
+ | { op: 'ignore' };
65
+
66
+ export function parseTrainLine(
67
+ name: string,
68
+ line: string,
69
+ ): TrainMessage | null {
70
+ let msg: {
71
+ op?: string;
72
+ id?: string;
73
+ result?: unknown;
74
+ error?: string;
75
+ errorInfo?: unknown;
76
+ text?: string;
77
+ } & Record<string, unknown>;
78
+ try {
79
+ msg = JSON.parse(line) as typeof msg;
80
+ } catch (err) {
81
+ log.warn(
82
+ { name, err: errMsg(err), line: line.slice(0, 200) },
83
+ 'train: bad JSON',
84
+ );
85
+ return null;
86
+ }
87
+ if (msg.op === 'response') {
88
+ if (typeof msg.id !== 'string') return { op: 'ignore' };
89
+ return {
90
+ op: 'response',
91
+ id: msg.id,
92
+ result: msg.result,
93
+ error: msg.error,
94
+ errorInfo: coerceErrorInfo(msg.errorInfo),
95
+ };
96
+ }
97
+ if (msg.op === 'log') return { op: 'log', text: msg.text };
98
+ if (typeof msg.line !== 'string') {
99
+ log.warn(
100
+ { name, line: line.slice(0, 200) },
101
+ 'train: event missing `line` (string) — dropped',
102
+ );
103
+ return { op: 'ignore' };
104
+ }
105
+ return { op: 'event', event: msg };
106
+ }
107
+
108
+ export function drainLines(
109
+ name: string,
110
+ buf: string,
111
+ onLine: (line: string) => void,
112
+ ): string {
113
+ if (buf.length > STDOUT_LINE_MAX && !buf.includes('\n')) {
114
+ log.warn(
115
+ { name, bytes: buf.length },
116
+ 'train: dropping oversized stdout line',
117
+ );
118
+ return '';
119
+ }
120
+ let nl;
121
+ while ((nl = buf.indexOf('\n')) !== -1) {
122
+ const line = buf.slice(0, nl).trim();
123
+ buf = buf.slice(nl + 1);
124
+ if (!line) continue;
125
+ onLine(line);
126
+ }
127
+ return buf;
128
+ }
129
+
130
+ export function readCalls(
131
+ name: string,
132
+ handleCall: (msg: CallMsg) => Promise<void>,
133
+ ): void {
134
+ let buf = '';
135
+ process.stdin.setEncoding('utf8');
136
+ process.stdin.on('data', (chunk: Buffer | string) => {
137
+ buf += typeof chunk === 'string' ? chunk : chunk.toString('utf8');
138
+ buf = drainLines(name, buf, (line) => {
139
+ try {
140
+ const msg = JSON.parse(line) as Partial<CallMsg>;
141
+ if (msg.op === 'call')
142
+ handleCall(msg as CallMsg).catch((e: unknown) => {
143
+ process.stderr.write(`call failed: ${errMsg(e)}\n`);
144
+ });
145
+ } catch (err: unknown) {
146
+ process.stderr.write(`bad stdin line: ${errMsg(err)}\n`);
147
+ }
148
+ });
149
+ });
150
+ }
151
+
152
+ export function encodeCall(id: string, action: string, args: unknown): string {
153
+ return JSON.stringify({ op: 'call', id, action, args }) + '\n';
154
+ }
155
+
156
+ function isTrainFile(name: string): boolean {
157
+ return (
158
+ /\.(ts|js|mjs)$/.test(name) &&
159
+ !name.startsWith('_') &&
160
+ !name.startsWith('.')
161
+ );
162
+ }
163
+
164
+ export function listTrainFiles(dir: string): { name: string; path: string }[] {
165
+ return readdirSync(dir)
166
+ .filter(isTrainFile)
167
+ .map((f) => ({ name: parsePath(f).name, path: join(dir, f) }))
168
+ .filter((t) => {
169
+ try {
170
+ return statSync(t.path).isFile();
171
+ } catch {
172
+ return false;
173
+ }
174
+ });
175
+ }
176
+
177
+ export function mintCallId(seq: number): string {
178
+ return `req_${seq}_${Math.random().toString(36).slice(2, 8)}`;
179
+ }
180
+
181
+ export function failAllPending(
182
+ pending: Map<string, Pending>,
183
+ reason: string,
184
+ ): void {
185
+ for (const p of pending.values()) {
186
+ clearTimeout(p.timer);
187
+ p.reject(new Error(reason));
188
+ }
189
+ pending.clear();
190
+ }
191
+
192
+ export function sendCall(
193
+ t: CallTarget,
194
+ id: string,
195
+ action: string,
196
+ args: unknown,
197
+ ): Promise<TrainCallResponse> {
198
+ return new Promise<TrainCallResponse>((resolve, reject) => {
199
+ const timer = setTimeout(() => {
200
+ t.pending.delete(id);
201
+ reject(
202
+ new Error(
203
+ `train '${t.name}' call '${action}' timed out after ${CALL_TIMEOUT_MS}ms`,
204
+ ),
205
+ );
206
+ }, CALL_TIMEOUT_MS);
207
+ t.pending.set(id, { resolve, reject, timer });
208
+ try {
209
+ const stdin = (
210
+ t.proc as {
211
+ stdin?: { write: (s: string) => void; flush: () => void };
212
+ } | null
213
+ )?.stdin;
214
+ if (!stdin || typeof stdin === 'number')
215
+ throw new Error('stdin not piped');
216
+ stdin.write(encodeCall(id, action, args));
217
+ stdin.flush();
218
+ } catch (err) {
219
+ clearTimeout(timer);
220
+ t.pending.delete(id);
221
+ reject(new Error(`train '${t.name}' stdin write failed: ${errMsg(err)}`));
222
+ }
223
+ });
224
+ }
@@ -0,0 +1,302 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import type { RelayTarget } from '../db/connector-relay.js';
3
+ import { cliIdentity } from './api-http.js';
4
+ import { ApiError } from './api-error.js';
5
+ import { errMsg, log } from './log.js';
6
+
7
+ export interface RelayApiDeps {
8
+ target: (
9
+ collectionId: string,
10
+ connectorId: string,
11
+ force: boolean,
12
+ ) => Promise<RelayTarget>;
13
+ }
14
+
15
+ const ID_PATH_RE = /^\/relay\/([A-Za-z0-9][A-Za-z0-9_-]{10})$/;
16
+ const METHODS = new Set(['POST', 'GET', 'DELETE']);
17
+ const RELAY_BODY_MAX = 8 * 1024 * 1024;
18
+ const FIRST_BYTE_MS = 120_000;
19
+ const PASS_REQ = [
20
+ 'accept',
21
+ 'content-type',
22
+ 'mcp-session-id',
23
+ 'mcp-protocol-version',
24
+ 'last-event-id',
25
+ ] as const;
26
+ const PASS_RES = [
27
+ 'content-type',
28
+ 'mcp-session-id',
29
+ 'mcp-protocol-version',
30
+ 'cache-control',
31
+ ] as const;
32
+
33
+ const keepaliveMs = (): number =>
34
+ Number(process.env.METRO_RELAY_KEEPALIVE_MS) || 25_000;
35
+
36
+ const reconnectUrl = (id: string): string =>
37
+ `https://metro.box/#/connector/${id}`;
38
+
39
+ function answer(
40
+ res: ServerResponse,
41
+ status: number,
42
+ body: Record<string, unknown>,
43
+ ): void {
44
+ if (res.headersSent) return;
45
+ res.writeHead(status, { 'content-type': 'application/json' });
46
+ res.end(JSON.stringify(body));
47
+ }
48
+
49
+ const signinAnswer = (res: ServerResponse, id: string): void => {
50
+ answer(res, 424, {
51
+ error: 'this connector needs signing in again',
52
+ reconnect: reconnectUrl(id),
53
+ });
54
+ };
55
+
56
+ function closeIfBodyUnread(req: IncomingMessage, res: ServerResponse): void {
57
+ if (req.readableEnded) return;
58
+ const drop = (): void => {
59
+ req.socket?.destroy();
60
+ };
61
+ if (res.writableFinished) drop();
62
+ else res.once('finish', drop);
63
+ }
64
+
65
+ async function readCapped(req: IncomingMessage): Promise<Uint8Array<ArrayBuffer>> {
66
+ const declared = Number(req.headers['content-length']);
67
+ if (Number.isFinite(declared) && declared > RELAY_BODY_MAX)
68
+ throw new ApiError('relay body exceeds 8 MiB', 413);
69
+ const chunks: Buffer[] = [];
70
+ let total = 0;
71
+ for await (const chunk of req) {
72
+ const buf = chunk as Buffer;
73
+ total += buf.length;
74
+ if (total > RELAY_BODY_MAX)
75
+ throw new ApiError('relay body exceeds 8 MiB', 413);
76
+ chunks.push(buf);
77
+ }
78
+ const merged = Buffer.concat(chunks);
79
+ const out = new Uint8Array(new ArrayBuffer(merged.byteLength));
80
+ out.set(merged);
81
+ return out;
82
+ }
83
+
84
+ function upstreamHeaders(
85
+ req: IncomingMessage,
86
+ injected: Record<string, string>,
87
+ ): Record<string, string> {
88
+ const out: Record<string, string> = {};
89
+ for (const name of PASS_REQ) {
90
+ const value = req.headers[name];
91
+ if (typeof value === 'string') out[name] = value;
92
+ }
93
+ return { ...out, ...injected };
94
+ }
95
+
96
+ async function forward(
97
+ req: IncomingMessage,
98
+ target: { url: string; headers: Record<string, string> },
99
+ body: Uint8Array<ArrayBuffer> | null,
100
+ signal: AbortSignal,
101
+ ): Promise<Response> {
102
+ const firstByte = setTimeout(() => {
103
+ log.warn({ url: target.url }, 'relay: upstream sent no response in time');
104
+ }, FIRST_BYTE_MS);
105
+ firstByte.unref?.();
106
+ try {
107
+ return await fetch(target.url, {
108
+ method: req.method,
109
+ headers: upstreamHeaders(req, target.headers),
110
+ ...(body === null ? {} : { body }),
111
+ signal,
112
+ redirect: 'manual',
113
+ });
114
+ } finally {
115
+ clearTimeout(firstByte);
116
+ }
117
+ }
118
+
119
+ interface Keepalive {
120
+ touch: () => void;
121
+ stop: () => void;
122
+ }
123
+
124
+ function startKeepalive(res: ServerResponse): Keepalive {
125
+ let timer: ReturnType<typeof setTimeout> | undefined;
126
+ const arm = (): void => {
127
+ timer = setTimeout(() => {
128
+ try {
129
+ res.write(': keepalive\n\n');
130
+ } catch {
131
+ log.debug('relay: keepalive write failed');
132
+ }
133
+ arm();
134
+ }, keepaliveMs());
135
+ timer.unref?.();
136
+ };
137
+ arm();
138
+ return {
139
+ touch: (): void => {
140
+ if (timer) clearTimeout(timer);
141
+ arm();
142
+ },
143
+ stop: (): void => {
144
+ if (timer) clearTimeout(timer);
145
+ },
146
+ };
147
+ }
148
+
149
+ async function pumpBody(
150
+ res: ServerResponse,
151
+ body: ReadableStream<Uint8Array>,
152
+ touch: () => void,
153
+ ): Promise<void> {
154
+ const reader = body.getReader();
155
+ for (;;) {
156
+ const { done, value } = await reader.read();
157
+ if (done) return;
158
+ touch();
159
+ const ok = res.write(value);
160
+ if (!ok)
161
+ await new Promise((resolve) => {
162
+ res.once('drain', resolve);
163
+ });
164
+ }
165
+ }
166
+
167
+ async function pipe(res: ServerResponse, upstream: Response): Promise<void> {
168
+ const headers: Record<string, string> = {};
169
+ for (const name of PASS_RES) {
170
+ const value = upstream.headers.get(name);
171
+ if (value !== null) headers[name] = value;
172
+ }
173
+ const sse = (headers['content-type'] ?? '').includes('text/event-stream');
174
+ if (sse) headers['x-accel-buffering'] = 'no';
175
+ res.writeHead(upstream.status, headers);
176
+ if (upstream.body === null) {
177
+ res.end();
178
+ return;
179
+ }
180
+ const keepalive = sse ? startKeepalive(res) : null;
181
+ try {
182
+ await pumpBody(res, upstream.body, () => keepalive?.touch());
183
+ } finally {
184
+ keepalive?.stop();
185
+ }
186
+ res.end();
187
+ }
188
+
189
+ const authFailed = (status: number): boolean =>
190
+ status === 401 || status === 403;
191
+
192
+ type Exchanged =
193
+ | { kind: 'response'; upstream: Response }
194
+ | { kind: 'missing' }
195
+ | { kind: 'signin' };
196
+
197
+ async function exchange(
198
+ req: IncomingMessage,
199
+ collectionId: string,
200
+ connectorId: string,
201
+ deps: RelayApiDeps,
202
+ body: Uint8Array<ArrayBuffer> | null,
203
+ signal: AbortSignal,
204
+ ): Promise<Exchanged> {
205
+ const target = await deps.target(collectionId, connectorId, false);
206
+ if (target.kind !== 'ok') return { kind: target.kind };
207
+ let upstream = await forward(req, target, body, signal);
208
+ if (!authFailed(upstream.status)) return { kind: 'response', upstream };
209
+ await upstream.body?.cancel();
210
+ const fresh = await deps.target(collectionId, connectorId, true);
211
+ if (fresh.kind !== 'ok') return { kind: 'signin' };
212
+ upstream = await forward(req, fresh, body, signal);
213
+ if (!authFailed(upstream.status)) return { kind: 'response', upstream };
214
+ await upstream.body?.cancel();
215
+ return { kind: 'signin' };
216
+ }
217
+
218
+ async function relayExchange(
219
+ req: IncomingMessage,
220
+ res: ServerResponse,
221
+ collectionId: string,
222
+ connectorId: string,
223
+ deps: RelayApiDeps,
224
+ ): Promise<void> {
225
+ const control = new AbortController();
226
+ const bail = (): void => {
227
+ if (!res.writableEnded) control.abort();
228
+ };
229
+ res.once('close', bail);
230
+ res.socket?.once('close', bail);
231
+ res.once('finish', () => {
232
+ res.socket?.removeListener('close', bail);
233
+ });
234
+ const body = req.method === 'POST' ? await readCapped(req) : null;
235
+ const out = await exchange(
236
+ req,
237
+ collectionId,
238
+ connectorId,
239
+ deps,
240
+ body,
241
+ control.signal,
242
+ );
243
+ if (out.kind === 'missing') {
244
+ answer(res, 404, { error: 'no such connector' });
245
+ return;
246
+ }
247
+ if (out.kind === 'signin') {
248
+ signinAnswer(res, connectorId);
249
+ return;
250
+ }
251
+ if (out.upstream.status >= 300 && out.upstream.status < 400) {
252
+ await out.upstream.body?.cancel();
253
+ answer(res, 502, { error: 'the connector redirected; metro does not follow' });
254
+ return;
255
+ }
256
+ await pipe(res, out.upstream);
257
+ }
258
+
259
+ function dispatch(
260
+ req: IncomingMessage,
261
+ res: ServerResponse,
262
+ connectorId: string,
263
+ deps: RelayApiDeps,
264
+ ): void {
265
+ const who = cliIdentity(req);
266
+ if (who === null) {
267
+ answer(res, 401, { error: 'unauthorized' });
268
+ return;
269
+ }
270
+ relayExchange(req, res, who.collectionId, connectorId, deps).catch(
271
+ (err: unknown) => {
272
+ if (err instanceof ApiError) {
273
+ answer(res, err.status, { error: err.message });
274
+ } else {
275
+ log.warn({ err: errMsg(err), connector: connectorId }, 'relay: failed');
276
+ answer(res, 502, { error: 'metro could not reach the connector' });
277
+ if (!res.writableEnded) res.end();
278
+ }
279
+ closeIfBodyUnread(req, res);
280
+ },
281
+ );
282
+ }
283
+
284
+ export function handleRelayRequest(
285
+ req: IncomingMessage,
286
+ res: ServerResponse,
287
+ deps: RelayApiDeps,
288
+ ): boolean {
289
+ const path = (req.url ?? '').split('?')[0] ?? '';
290
+ if (path !== '/relay' && !path.startsWith('/relay/')) return false;
291
+ const match = ID_PATH_RE.exec(path);
292
+ if (match?.[1] === undefined) {
293
+ answer(res, 404, { error: 'no such connector' });
294
+ return true;
295
+ }
296
+ if (!METHODS.has(req.method ?? '')) {
297
+ answer(res, 405, { error: 'method not allowed' });
298
+ return true;
299
+ }
300
+ dispatch(req, res, match[1], deps);
301
+ return true;
302
+ }
@@ -0,0 +1,130 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { log } from './log.js';
3
+ import {
4
+ apiFailure,
5
+ bodyField,
6
+ cors,
7
+ readJsonBody,
8
+ runIdentity,
9
+ sendJson,
10
+ } from './api-http.js';
11
+ import { RUN_CODE_RE, takeRunCode } from './run-pair.js';
12
+ import { sessionTtlFromEnv } from './google-oauth.js';
13
+ import { signRunToken } from './session.js';
14
+ import type { LoadedAgent } from '../db/materialize.js';
15
+ import type { RuntimeLease } from '../db/runtimes.js';
16
+
17
+ const CLAIM_PATH = '/api/run/claim';
18
+ const CONFIG_PATH = '/api/run/config';
19
+
20
+ const ALLOWED: Record<string, string> = {
21
+ [CLAIM_PATH]: 'POST',
22
+ [CONFIG_PATH]: 'GET',
23
+ };
24
+
25
+ export interface RunApiDeps {
26
+ claimRuntime: (agentId: string, label: string) => Promise<RuntimeLease>;
27
+ fenceRuntime: (runtimeId: string, agentId: string) => Promise<void>;
28
+ touchRuntime: (runtimeId: string) => Promise<void>;
29
+ loadAgent: (agentId: string) => Promise<LoadedAgent>;
30
+ }
31
+
32
+ function secretOrNull(): string | null {
33
+ const secret = process.env.METRO_SESSION_SECRET?.trim() ?? '';
34
+ return secret === '' ? null : secret;
35
+ }
36
+
37
+ function labelOf(body: unknown): string {
38
+ const raw = bodyField(body, 'label');
39
+ const label = typeof raw === 'string' ? raw.trim().slice(0, 64) : '';
40
+ return label === '' ? 'unnamed machine' : label;
41
+ }
42
+
43
+ async function handleClaim(
44
+ req: IncomingMessage,
45
+ res: ServerResponse,
46
+ deps: RunApiDeps,
47
+ ): Promise<void> {
48
+ const secret = secretOrNull();
49
+ if (secret === null) {
50
+ sendJson(req, res, 503, { error: 'sign-in is not configured' });
51
+ return;
52
+ }
53
+ const body = await readJsonBody(req);
54
+ const raw = bodyField(body, 'code');
55
+ const code = typeof raw === 'string' ? raw.trim() : '';
56
+ if (!RUN_CODE_RE.test(code)) {
57
+ sendJson(req, res, 400, {
58
+ error:
59
+ 'that does not look like a runtime code — metro start wants the mr_… code from the agent page',
60
+ });
61
+ return;
62
+ }
63
+ const taken = takeRunCode(code);
64
+ if (taken === undefined) {
65
+ sendJson(req, res, 400, {
66
+ error: 'that code has expired or was already used',
67
+ });
68
+ return;
69
+ }
70
+ const lease = await deps.claimRuntime(taken.agentId, labelOf(body));
71
+ const token = signRunToken(
72
+ { email: taken.email, agentId: lease.agentId, runtimeId: lease.runtimeId },
73
+ secret,
74
+ { ttlSec: sessionTtlFromEnv() },
75
+ );
76
+ log.info(
77
+ { agent: lease.agentId, runtime: lease.runtimeId, label: lease.label },
78
+ 'run: agent claimed by a local runtime',
79
+ );
80
+ sendJson(req, res, 200, {
81
+ token,
82
+ agent: lease.agentId,
83
+ label: lease.label,
84
+ });
85
+ }
86
+
87
+ async function handleConfig(
88
+ req: IncomingMessage,
89
+ res: ServerResponse,
90
+ deps: RunApiDeps,
91
+ ): Promise<void> {
92
+ const who = runIdentity(req);
93
+ if (who === null) {
94
+ sendJson(req, res, 401, { error: 'unauthorized' });
95
+ return;
96
+ }
97
+ await deps.fenceRuntime(who.runtimeId, who.agentId);
98
+ await deps.touchRuntime(who.runtimeId);
99
+ sendJson(req, res, 200, { agent: await deps.loadAgent(who.agentId) });
100
+ }
101
+
102
+ export function handleRunApiRequest(
103
+ req: IncomingMessage,
104
+ res: ServerResponse,
105
+ deps: RunApiDeps,
106
+ ): boolean {
107
+ const path = (req.url ?? '').split('?')[0] ?? '';
108
+ const method = ALLOWED[path];
109
+ if (method === undefined) {
110
+ if (!path.startsWith('/api/run/') && path !== '/api/run') return false;
111
+ sendJson(req, res, 404, { error: 'no such route' });
112
+ return true;
113
+ }
114
+ if (req.method === 'OPTIONS') {
115
+ res.writeHead(204, cors(req)).end();
116
+ return true;
117
+ }
118
+ if (req.method !== method) {
119
+ sendJson(req, res, 405, { error: 'method not allowed' });
120
+ return true;
121
+ }
122
+ const work =
123
+ path === CLAIM_PATH
124
+ ? handleClaim(req, res, deps)
125
+ : handleConfig(req, res, deps);
126
+ work.catch((err: unknown) => {
127
+ apiFailure(req, res, err, 'run-api');
128
+ });
129
+ return true;
130
+ }
@@ -0,0 +1,18 @@
1
+ import { makeCodeStore } from './pair-codes.js';
2
+
3
+ export interface RunCode {
4
+ email: string;
5
+ agentId: string;
6
+ }
7
+
8
+ const store = makeCodeStore<RunCode>('mr');
9
+
10
+ export const RUN_CODE_RE = store.pattern;
11
+
12
+ export const mintRunCode = (
13
+ entry: RunCode,
14
+ now?: number,
15
+ ): { code: string; expiresAt: number } => store.mint(entry, now);
16
+
17
+ export const takeRunCode = (code: string, now?: number): RunCode | undefined =>
18
+ store.take(code, now);