@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,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,125 @@
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 code = bodyField(body, 'code');
55
+ const taken =
56
+ typeof code === 'string' && RUN_CODE_RE.test(code)
57
+ ? takeRunCode(code)
58
+ : undefined;
59
+ if (taken === undefined) {
60
+ sendJson(req, res, 400, {
61
+ error: 'that code has expired or was already used',
62
+ });
63
+ return;
64
+ }
65
+ const lease = await deps.claimRuntime(taken.agentId, labelOf(body));
66
+ const token = signRunToken(
67
+ { email: taken.email, agentId: lease.agentId, runtimeId: lease.runtimeId },
68
+ secret,
69
+ { ttlSec: sessionTtlFromEnv() },
70
+ );
71
+ log.info(
72
+ { agent: lease.agentId, runtime: lease.runtimeId, label: lease.label },
73
+ 'run: agent claimed by a local runtime',
74
+ );
75
+ sendJson(req, res, 200, {
76
+ token,
77
+ agent: lease.agentId,
78
+ label: lease.label,
79
+ });
80
+ }
81
+
82
+ async function handleConfig(
83
+ req: IncomingMessage,
84
+ res: ServerResponse,
85
+ deps: RunApiDeps,
86
+ ): Promise<void> {
87
+ const who = runIdentity(req);
88
+ if (who === null) {
89
+ sendJson(req, res, 401, { error: 'unauthorized' });
90
+ return;
91
+ }
92
+ await deps.fenceRuntime(who.runtimeId, who.agentId);
93
+ await deps.touchRuntime(who.runtimeId);
94
+ sendJson(req, res, 200, { agent: await deps.loadAgent(who.agentId) });
95
+ }
96
+
97
+ export function handleRunApiRequest(
98
+ req: IncomingMessage,
99
+ res: ServerResponse,
100
+ deps: RunApiDeps,
101
+ ): boolean {
102
+ const path = (req.url ?? '').split('?')[0] ?? '';
103
+ const method = ALLOWED[path];
104
+ if (method === undefined) {
105
+ if (!path.startsWith('/api/run/') && path !== '/api/run') return false;
106
+ sendJson(req, res, 404, { error: 'no such route' });
107
+ return true;
108
+ }
109
+ if (req.method === 'OPTIONS') {
110
+ res.writeHead(204, cors(req)).end();
111
+ return true;
112
+ }
113
+ if (req.method !== method) {
114
+ sendJson(req, res, 405, { error: 'method not allowed' });
115
+ return true;
116
+ }
117
+ const work =
118
+ path === CLAIM_PATH
119
+ ? handleClaim(req, res, deps)
120
+ : handleConfig(req, res, deps);
121
+ work.catch((err: unknown) => {
122
+ apiFailure(req, res, err, 'run-api');
123
+ });
124
+ return true;
125
+ }
@@ -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);
@@ -0,0 +1,105 @@
1
+ import { errMsg, log } from './log.js';
2
+ import type { LoadedAgent, StationSource } from '../db/materialize.js';
3
+
4
+ const POLL_MS = 30_000;
5
+ const BACKOFF_MS = 5 * 60_000;
6
+ const FETCH_TIMEOUT_MS = 15_000;
7
+
8
+ export class RuntimeRevoked extends Error {}
9
+
10
+ const LOOPBACK = new Set(['127.0.0.1', '::1', 'localhost']);
11
+
12
+ export function carriesSecretsSafely(base: string): boolean {
13
+ let url: URL;
14
+ try {
15
+ url = new URL(base);
16
+ } catch {
17
+ return false;
18
+ }
19
+ if (url.protocol === 'https:') return true;
20
+ return url.protocol === 'http:' && LOOPBACK.has(url.hostname);
21
+ }
22
+
23
+ export interface RuntimeConfig {
24
+ url: string;
25
+ token: string;
26
+ }
27
+
28
+ export function runtimeConfigFromEnv(): RuntimeConfig | null {
29
+ const token = process.env.METRO_RUN_TOKEN?.trim() ?? '';
30
+ if (token === '') return null;
31
+ const raw = (process.env.METRO_URL?.trim() ?? '').replace(/\/+$/, '');
32
+ const url = raw === '' ? 'https://mcp.metro.box' : raw;
33
+ if (!carriesSecretsSafely(url))
34
+ throw new Error(
35
+ `refusing to fetch station credentials from ${url} in the clear — ` +
36
+ 'use https, or a loopback address',
37
+ );
38
+ return { url, token };
39
+ }
40
+
41
+ export function httpSource(cfg: RuntimeConfig): StationSource {
42
+ return async () => {
43
+ const res = await fetch(`${cfg.url}/api/run/config`, {
44
+ headers: { authorization: `Bearer ${cfg.token}` },
45
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
46
+ redirect: 'manual',
47
+ });
48
+ if (res.status === 401 || res.status === 403 || res.status === 409)
49
+ throw new RuntimeRevoked(
50
+ `metro no longer recognises this machine as the holder (${String(res.status)})`,
51
+ );
52
+ if (!res.ok)
53
+ throw new Error(`metro answered ${String(res.status)} for the run config`);
54
+ const body = (await res.json()) as { agent?: LoadedAgent };
55
+ if (!body.agent) throw new Error('metro returned no agent');
56
+ return [body.agent];
57
+ };
58
+ }
59
+
60
+ export interface PollerDeps {
61
+ sync: () => Promise<void>;
62
+ stopAll: () => Promise<void>;
63
+ everyMs?: number;
64
+ backoffMs?: number;
65
+ }
66
+
67
+ export function startRuntimePoller(deps: PollerDeps): () => void {
68
+ const everyMs = deps.everyMs ?? POLL_MS;
69
+ const backoffMs = deps.backoffMs ?? BACKOFF_MS;
70
+ let stopped = false;
71
+ let timer: ReturnType<typeof setTimeout> | null = null;
72
+
73
+ const arm = (ms: number): void => {
74
+ if (stopped) return;
75
+ timer = setTimeout(() => {
76
+ tick().catch((err: unknown) => {
77
+ log.error({ err: errMsg(err) }, 'runtime: poller failed');
78
+ });
79
+ }, ms);
80
+ timer.unref?.();
81
+ };
82
+
83
+ async function tick(): Promise<void> {
84
+ if (stopped) return;
85
+ try {
86
+ await deps.sync();
87
+ arm(everyMs);
88
+ } catch (err) {
89
+ if (err instanceof RuntimeRevoked) {
90
+ stopped = true;
91
+ log.error({ reason: err.message }, 'runtime: revoked — stopping stations');
92
+ await deps.stopAll();
93
+ return;
94
+ }
95
+ log.warn({ err: errMsg(err) }, 'runtime: could not reach metro; retrying');
96
+ arm(backoffMs);
97
+ }
98
+ }
99
+
100
+ arm(everyMs);
101
+ return () => {
102
+ stopped = true;
103
+ if (timer !== null) clearTimeout(timer);
104
+ };
105
+ }
@@ -0,0 +1,57 @@
1
+ import {
2
+ chmodSync,
3
+ existsSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ renameSync,
7
+ statSync,
8
+ writeFileSync,
9
+ } from 'node:fs';
10
+ import { dirname } from 'node:path';
11
+ import { errMsg, log } from './log.js';
12
+
13
+ export function ensureSecureDir(dir: string): void {
14
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
15
+ try {
16
+ chmodSync(dir, 0o700);
17
+ } catch {
18
+ }
19
+ }
20
+
21
+ export function chmodIfExists(path: string, mode = 0o600): void {
22
+ if (!existsSync(path)) return;
23
+ try {
24
+ if ((statSync(path).mode & 0o777) !== mode) chmodSync(path, mode);
25
+ } catch {
26
+ }
27
+ }
28
+
29
+ export function writeSecure(path: string, data: string): void {
30
+ ensureSecureDir(dirname(path));
31
+ const tmp = `${path}.tmp-${process.pid}`;
32
+ writeFileSync(tmp, data, { mode: 0o600 });
33
+ try {
34
+ chmodSync(tmp, 0o600);
35
+ } catch {
36
+ }
37
+ renameSync(tmp, path);
38
+ chmodIfExists(path, 0o600);
39
+ }
40
+
41
+ export function readJson<T>(
42
+ path: string,
43
+ fallback: T,
44
+ opts?: { warn?: string },
45
+ ): T {
46
+ if (!existsSync(path)) return fallback;
47
+ try {
48
+ return JSON.parse(readFileSync(path, 'utf8')) as T;
49
+ } catch (err) {
50
+ if (opts?.warn) log.warn({ err: errMsg(err), path }, opts.warn);
51
+ return fallback;
52
+ }
53
+ }
54
+
55
+ export function writeJson(path: string, value: unknown): void {
56
+ writeFileSync(path, JSON.stringify(value, null, 2));
57
+ }
@@ -0,0 +1,27 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { apiSession, cors, sendJson } from './api-http.js';
3
+
4
+ const PATH = '/api/session';
5
+
6
+ export function handleSessionApiRequest(
7
+ req: IncomingMessage,
8
+ res: ServerResponse,
9
+ ): boolean {
10
+ const path = (req.url ?? '').split('?')[0] ?? '';
11
+ if (path !== PATH) return false;
12
+ if (req.method === 'OPTIONS') {
13
+ res.writeHead(204, cors(req)).end();
14
+ return true;
15
+ }
16
+ if (req.method !== 'GET') {
17
+ sendJson(req, res, 405, { error: 'method not allowed' });
18
+ return true;
19
+ }
20
+ const session = apiSession(req);
21
+ if (!session) {
22
+ sendJson(req, res, 401, { error: 'unauthorized' });
23
+ return true;
24
+ }
25
+ sendJson(req, res, 200, { email: session.email });
26
+ return true;
27
+ }
@@ -0,0 +1,41 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { handleSessionApiRequest } from './session-api.js';
3
+ import { handleAgentApiRequest, type AgentApiDeps } from './agent-api.js';
4
+ import {
5
+ handleConnectorApiRequest,
6
+ type ConnectorApiDeps,
7
+ } from './connector-api.js';
8
+ import { handleCollectionApiRequest } from './collection-api.js';
9
+ import { handleCliPairRequest } from './cli-pair-api.js';
10
+ import { handleRunApiRequest, type RunApiDeps } from './run-api.js';
11
+ import {
12
+ handleProjectApiRequest,
13
+ type ProjectApiDeps,
14
+ } from './project-api.js';
15
+
16
+ export interface SessionApis {
17
+ agentApi?: AgentApiDeps;
18
+ connectorApi?: ConnectorApiDeps;
19
+ projectApi?: ProjectApiDeps;
20
+ runApi?: RunApiDeps;
21
+ }
22
+
23
+ export function handleSessionApis(
24
+ req: IncomingMessage,
25
+ res: ServerResponse,
26
+ apis: SessionApis,
27
+ ): boolean {
28
+ const { agentApi, connectorApi, projectApi, runApi } = apis;
29
+ const routes: (() => boolean)[] = [() => handleSessionApiRequest(req, res)];
30
+ if (runApi) routes.push(() => handleRunApiRequest(req, res, runApi));
31
+ if (projectApi)
32
+ routes.push(() => handleProjectApiRequest(req, res, projectApi));
33
+ if (connectorApi)
34
+ routes.push(
35
+ () => handleCliPairRequest(req, res, connectorApi),
36
+ () => handleCollectionApiRequest(req, res, connectorApi),
37
+ () => handleConnectorApiRequest(req, res, connectorApi),
38
+ );
39
+ if (agentApi) routes.push(() => handleAgentApiRequest(req, res, agentApi));
40
+ return routes.some((run) => run());
41
+ }