@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,218 @@
1
+ import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
2
+ import { ID_RE } from '../db/ids.js';
3
+
4
+ export class SessionError extends Error {}
5
+
6
+ const b64url = (buf: Buffer): string =>
7
+ buf
8
+ .toString('base64')
9
+ .replace(/\+/g, '-')
10
+ .replace(/\//g, '_')
11
+ .replace(/=+$/, '');
12
+
13
+ const b64urlJson = (v: unknown): string =>
14
+ b64url(Buffer.from(JSON.stringify(v)));
15
+
16
+ const decodeJson = (seg: string): unknown =>
17
+ JSON.parse(
18
+ Buffer.from(seg.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString(
19
+ 'utf8',
20
+ ),
21
+ );
22
+
23
+ const HEADER = b64urlJson({ alg: 'HS256', typ: 'JWT' });
24
+
25
+ function sign(payload: Record<string, unknown>, secret: string): string {
26
+ if (secret === '') throw new SessionError('missing signing secret');
27
+ const body = b64urlJson(payload);
28
+ const data = `${HEADER}.${body}`;
29
+ const sig = b64url(createHmac('sha256', secret).update(data).digest());
30
+ return `${data}.${sig}`;
31
+ }
32
+
33
+ function verify(token: string, secret: string): Record<string, unknown> {
34
+ if (secret === '') throw new SessionError('missing signing secret');
35
+ const parts = token.split('.');
36
+ if (parts.length !== 3) throw new SessionError('malformed token');
37
+ const data = `${parts[0]}.${parts[1]}`;
38
+ const expected = b64url(createHmac('sha256', secret).update(data).digest());
39
+ const a = Buffer.from(parts[2] ?? '');
40
+ const b = Buffer.from(expected);
41
+ if (a.length !== b.length || !timingSafeEqual(a, b))
42
+ throw new SessionError('bad signature');
43
+ let payload: unknown;
44
+ try {
45
+ payload = decodeJson(parts[1] ?? '');
46
+ } catch {
47
+ throw new SessionError('malformed payload');
48
+ }
49
+ if (typeof payload !== 'object' || payload === null)
50
+ throw new SessionError('malformed payload');
51
+ return payload as Record<string, unknown>;
52
+ }
53
+
54
+ const nowSec = (now?: number): number => Math.floor((now ?? Date.now()) / 1000);
55
+
56
+ export function newNonce(): string {
57
+ return b64url(randomBytes(16));
58
+ }
59
+
60
+ interface StateClaims {
61
+ return_to: string;
62
+ nonce: string;
63
+ }
64
+
65
+ export function signState(
66
+ claims: StateClaims,
67
+ secret: string,
68
+ opts: { ttlSec?: number; now?: number } = {},
69
+ ): string {
70
+ const iat = nowSec(opts.now);
71
+ return sign(
72
+ {
73
+ typ: 'state',
74
+ return_to: claims.return_to,
75
+ nonce: claims.nonce,
76
+ iat,
77
+ exp: iat + (opts.ttlSec ?? 600),
78
+ },
79
+ secret,
80
+ );
81
+ }
82
+
83
+ export function verifyState(
84
+ token: string,
85
+ secret: string,
86
+ now?: number,
87
+ ): StateClaims {
88
+ const p = verify(token, secret);
89
+ if (p.typ !== 'state') throw new SessionError('wrong token type');
90
+ if (typeof p.exp !== 'number' || p.exp < nowSec(now))
91
+ throw new SessionError('state expired');
92
+ if (typeof p.return_to !== 'string' || typeof p.nonce !== 'string')
93
+ throw new SessionError('malformed state');
94
+ return { return_to: p.return_to, nonce: p.nonce };
95
+ }
96
+
97
+ interface SessionClaims {
98
+ email: string;
99
+ agentIds: string[];
100
+ }
101
+
102
+ export interface CliClaims {
103
+ email: string;
104
+ collectionId: string;
105
+ }
106
+
107
+ export interface RunClaims {
108
+ email: string;
109
+ agentId: string;
110
+ runtimeId: string;
111
+ }
112
+
113
+ export function signSession(
114
+ claims: SessionClaims,
115
+ secret: string,
116
+ opts: { ttlSec?: number; now?: number } = {},
117
+ ): string {
118
+ const iat = nowSec(opts.now);
119
+ return sign(
120
+ {
121
+ typ: 'session',
122
+ sub: claims.email,
123
+ agent_ids: claims.agentIds,
124
+ iat,
125
+ exp: iat + (opts.ttlSec ?? 30 * 24 * 3600),
126
+ },
127
+ secret,
128
+ );
129
+ }
130
+
131
+ export function verifySession(
132
+ token: string,
133
+ secret: string,
134
+ now?: number,
135
+ ): SessionClaims {
136
+ const p = verify(token, secret);
137
+ if (p.typ !== 'session') throw new SessionError('wrong token type');
138
+ if (typeof p.exp !== 'number' || p.exp < nowSec(now))
139
+ throw new SessionError('session expired');
140
+ const ids = p.agent_ids;
141
+ if (
142
+ typeof p.sub !== 'string' ||
143
+ !Array.isArray(ids) ||
144
+ ids.some((a) => typeof a !== 'string' || !ID_RE.test(a))
145
+ )
146
+ throw new SessionError('malformed session');
147
+ return { email: p.sub, agentIds: ids as string[] };
148
+ }
149
+
150
+ export function signCliToken(
151
+ claims: CliClaims,
152
+ secret: string,
153
+ opts: { ttlSec?: number; now?: number } = {},
154
+ ): string {
155
+ const iat = nowSec(opts.now);
156
+ return sign(
157
+ {
158
+ typ: 'cli',
159
+ sub: claims.email,
160
+ collection: claims.collectionId,
161
+ iat,
162
+ exp: iat + (opts.ttlSec ?? 30 * 24 * 3600),
163
+ },
164
+ secret,
165
+ );
166
+ }
167
+
168
+ export function verifyCliToken(
169
+ token: string,
170
+ secret: string,
171
+ now?: number,
172
+ ): CliClaims {
173
+ const p = verify(token, secret);
174
+ if (p.typ !== 'cli') throw new SessionError('wrong token type');
175
+ if (typeof p.exp !== 'number' || p.exp < nowSec(now))
176
+ throw new SessionError('cli token expired');
177
+ if (typeof p.sub !== 'string' || typeof p.collection !== 'string')
178
+ throw new SessionError('malformed cli token');
179
+ return { email: p.sub, collectionId: p.collection };
180
+ }
181
+
182
+ export function signRunToken(
183
+ claims: RunClaims,
184
+ secret: string,
185
+ opts: { ttlSec?: number; now?: number } = {},
186
+ ): string {
187
+ const iat = nowSec(opts.now);
188
+ return sign(
189
+ {
190
+ typ: 'run',
191
+ sub: claims.email,
192
+ agent: claims.agentId,
193
+ rt: claims.runtimeId,
194
+ iat,
195
+ exp: iat + (opts.ttlSec ?? 365 * 24 * 3600),
196
+ },
197
+ secret,
198
+ );
199
+ }
200
+
201
+ export function verifyRunToken(
202
+ token: string,
203
+ secret: string,
204
+ now?: number,
205
+ ): RunClaims {
206
+ const p = verify(token, secret);
207
+ if (p.typ !== 'run') throw new SessionError('wrong token type');
208
+ if (typeof p.exp !== 'number' || p.exp < nowSec(now))
209
+ throw new SessionError('run token expired');
210
+ if (
211
+ typeof p.sub !== 'string' ||
212
+ typeof p.agent !== 'string' ||
213
+ typeof p.rt !== 'string' ||
214
+ !ID_RE.test(p.agent)
215
+ )
216
+ throw new SessionError('malformed run token');
217
+ return { email: p.sub, agentId: p.agent, runtimeId: p.rt };
218
+ }
@@ -0,0 +1,97 @@
1
+ import { watch, type FSWatcher } from 'node:fs';
2
+ import { join, parse as parsePath } from 'node:path';
3
+ import { errMsg, log } from './log.js';
4
+ import type { Pending } from './protocol.js';
5
+
6
+ export const RESTART_BACKOFFS_MS = [1_000, 5_000, 30_000] as const;
7
+ export const MAX_CONSECUTIVE_FAILS = 5;
8
+ export const HOT_RELOAD_DEBOUNCE_MS = 300;
9
+ export const TRAIN_EXT = /\.(ts|js|mjs)$/;
10
+
11
+ export interface TrainState {
12
+ name: string;
13
+ path: string;
14
+ proc: ReturnType<typeof Bun.spawn> | null;
15
+ pending: Map<string, Pending>;
16
+ buf: string;
17
+ errBuf: string;
18
+ failCount: number;
19
+ restartTimer: ReturnType<typeof setTimeout> | null;
20
+ startedAt: string | null;
21
+ stopped: boolean;
22
+ }
23
+
24
+ export function startWatcher(
25
+ dir: string,
26
+ timers: Map<string, ReturnType<typeof setTimeout>>,
27
+ onChange: (name: string, path: string) => void,
28
+ ): FSWatcher | null {
29
+ try {
30
+ const w = watch(dir, (_event, filename) => {
31
+ if (!filename) return;
32
+ const base = filename;
33
+ if (!TRAIN_EXT.test(base) || base.startsWith('_') || base.startsWith('.'))
34
+ return;
35
+ const name = parsePath(base).name;
36
+ const existing = timers.get(name);
37
+ if (existing) clearTimeout(existing);
38
+ timers.set(
39
+ name,
40
+ setTimeout(() => {
41
+ timers.delete(name);
42
+ onChange(name, join(dir, base));
43
+ }, HOT_RELOAD_DEBOUNCE_MS),
44
+ );
45
+ });
46
+ w.on('error', (err) => {
47
+ log.warn({ err: errMsg(err) }, 'train hot-reload: watcher error');
48
+ });
49
+ log.info({ dir }, 'train hot-reload: watching');
50
+ return w;
51
+ } catch (err) {
52
+ log.warn(
53
+ { err: errMsg(err) },
54
+ 'train hot-reload: watch unavailable (on-demand restart only)',
55
+ );
56
+ return null;
57
+ }
58
+ }
59
+
60
+ export function killGracefully(
61
+ proc: ReturnType<typeof Bun.spawn> | null,
62
+ ): Promise<unknown> | null {
63
+ if (proc?.exitCode !== null) return null;
64
+ try {
65
+ proc.kill('SIGTERM');
66
+ } catch {
67
+ }
68
+ const grace = setTimeout(() => {
69
+ try {
70
+ proc.kill('SIGKILL');
71
+ } catch {
72
+ }
73
+ }, 2_000);
74
+ return proc.exited.finally(() => {
75
+ clearTimeout(grace);
76
+ });
77
+ }
78
+
79
+ export async function pumpStream(
80
+ stream: unknown,
81
+ name: string,
82
+ kind: 'stdout' | 'stderr',
83
+ onChunk: (s: string) => void,
84
+ ): Promise<void> {
85
+ if (!stream || typeof stream === 'number') return;
86
+ const reader = (stream as ReadableStream<Uint8Array>).getReader();
87
+ const dec = new TextDecoder();
88
+ try {
89
+ while (true) {
90
+ const { value, done } = await reader.read();
91
+ if (done) break;
92
+ onChunk(dec.decode(value, { stream: true }));
93
+ }
94
+ } catch (err) {
95
+ log.debug({ name, err: errMsg(err) }, `train: ${kind} pump ended`);
96
+ }
97
+ }
@@ -0,0 +1,323 @@
1
+ import { mkdirSync, statSync, type FSWatcher } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { errMsg, log } from './log.js';
5
+ import { daemonSelf } from './events.js';
6
+ import {
7
+ drainLines,
8
+ failAllPending,
9
+ listTrainFiles,
10
+ mintCallId,
11
+ parseTrainLine,
12
+ sendCall,
13
+ type TrainCallResponse,
14
+ type TrainEvent,
15
+ } from './protocol.js';
16
+ import {
17
+ HOT_RELOAD_DEBOUNCE_MS,
18
+ MAX_CONSECUTIVE_FAILS,
19
+ RESTART_BACKOFFS_MS,
20
+ killGracefully,
21
+ pumpStream,
22
+ startWatcher,
23
+ type TrainState,
24
+ } from './supervisor-io.js';
25
+
26
+ export const TRAINS_DIR =
27
+ process.env.METRO_TRAINS_DIR ?? join(homedir(), '.metro', 'trains');
28
+
29
+ const DYLD_FALLBACK_ENV: Record<string, string> =
30
+ process.platform === 'darwin'
31
+ ? {
32
+ DYLD_FALLBACK_LIBRARY_PATH: [
33
+ '/usr/lib',
34
+ process.env.DYLD_FALLBACK_LIBRARY_PATH,
35
+ ]
36
+ .filter(Boolean)
37
+ .join(':'),
38
+ }
39
+ : {};
40
+
41
+ export class TrainSupervisor {
42
+ private trains = new Map<string, TrainState>();
43
+ private onEvent: ((event: TrainEvent, train: string) => void) | null = null;
44
+ private nextCallId = 1;
45
+ private watcher: FSWatcher | null = null;
46
+ private reloadTimers = new Map<string, ReturnType<typeof setTimeout>>();
47
+
48
+ constructor(private dir: string = TRAINS_DIR) {}
49
+
50
+ running(): string[] {
51
+ return [...this.trains.keys()];
52
+ }
53
+
54
+ onTrainEvent(handler: (event: TrainEvent, train: string) => void): void {
55
+ this.onEvent = handler;
56
+ }
57
+
58
+ start(): void {
59
+ mkdirSync(this.dir, { recursive: true });
60
+ for (const t of listTrainFiles(this.dir)) this.startTrain(t.name, t.path);
61
+ log.info(
62
+ { dir: this.dir, count: this.trains.size },
63
+ 'train supervisor: started',
64
+ );
65
+ this.watchForReloads();
66
+ }
67
+
68
+ private watchForReloads(): void {
69
+ this.watcher = startWatcher(this.dir, this.reloadTimers, (name, path) => {
70
+ this.handleSourceChange(name, path);
71
+ });
72
+ }
73
+
74
+ requestReload(name: string): void {
75
+ const path = join(this.dir, `${name}.ts`);
76
+ const existing = this.reloadTimers.get(name);
77
+ if (existing) clearTimeout(existing);
78
+ this.reloadTimers.set(
79
+ name,
80
+ setTimeout(() => {
81
+ this.reloadTimers.delete(name);
82
+ this.handleSourceChange(name, path);
83
+ }, HOT_RELOAD_DEBOUNCE_MS),
84
+ );
85
+ }
86
+
87
+ async stopTrain(name: string): Promise<void> {
88
+ const t = this.trains.get(name);
89
+ if (!t) return;
90
+ const timer = this.reloadTimers.get(name);
91
+ if (timer) {
92
+ clearTimeout(timer);
93
+ this.reloadTimers.delete(name);
94
+ }
95
+ t.stopped = true;
96
+ if (t.restartTimer) {
97
+ clearTimeout(t.restartTimer);
98
+ t.restartTimer = null;
99
+ }
100
+ failAllPending(t.pending, `train '${name}' stopped`);
101
+ const wait = killGracefully(t.proc);
102
+ if (wait) await wait;
103
+ this.trains.delete(name);
104
+ log.info({ name }, 'train: stopped, last account detached');
105
+ }
106
+
107
+ private handleSourceChange(name: string, path: string): void {
108
+ try {
109
+ if (!statSync(path).isFile()) return;
110
+ } catch {
111
+ return;
112
+ }
113
+ if (this.trains.has(name)) {
114
+ log.info({ name }, 'train hot-reload: source changed, restarting');
115
+ this.restart(name).catch((err: unknown) => {
116
+ log.warn(
117
+ { name, err: errMsg(err) },
118
+ 'train hot-reload: restart failed',
119
+ );
120
+ });
121
+ return;
122
+ }
123
+ log.info({ name }, 'train hot-reload: new train, spawning');
124
+ this.startTrain(name, path);
125
+ }
126
+
127
+ async stop(): Promise<void> {
128
+ if (this.watcher) {
129
+ try {
130
+ this.watcher.close();
131
+ } catch {
132
+ }
133
+ this.watcher = null;
134
+ }
135
+ for (const timer of this.reloadTimers.values()) clearTimeout(timer);
136
+ this.reloadTimers.clear();
137
+ const tasks: Promise<unknown>[] = [];
138
+ for (const t of this.trains.values()) {
139
+ t.stopped = true;
140
+ if (t.restartTimer) {
141
+ clearTimeout(t.restartTimer);
142
+ t.restartTimer = null;
143
+ }
144
+ failAllPending(t.pending, 'train shutting down');
145
+ const wait = killGracefully(t.proc);
146
+ if (wait) tasks.push(wait);
147
+ }
148
+ await Promise.all(tasks);
149
+ }
150
+
151
+ async restart(name: string): Promise<void> {
152
+ const t = this.trains.get(name);
153
+ if (!t) throw new Error(`no train named '${name}'`);
154
+ if (t.restartTimer) {
155
+ clearTimeout(t.restartTimer);
156
+ t.restartTimer = null;
157
+ }
158
+ failAllPending(t.pending, `train '${name}' restarting`);
159
+ t.stopped = true;
160
+ const wait = killGracefully(t.proc);
161
+ if (wait) await wait;
162
+ t.stopped = false;
163
+ t.failCount = 0;
164
+ t.proc = null;
165
+ this.spawn(t);
166
+ }
167
+
168
+ async call(
169
+ name: string,
170
+ action: string,
171
+ args: unknown,
172
+ ): Promise<TrainCallResponse> {
173
+ const t = this.trains.get(name);
174
+ if (!t)
175
+ throw new Error(
176
+ `no train named '${name}' (have: ${[...this.trains.keys()].join(', ') || '(none)'})`,
177
+ );
178
+ if (t.proc?.exitCode !== null)
179
+ throw new Error(`train '${name}' is not running`);
180
+ return sendCall(t as never, mintCallId(this.nextCallId++), action, args);
181
+ }
182
+
183
+ private startTrain(name: string, path: string): void {
184
+ if (this.trains.has(name)) {
185
+ log.warn({ name }, 'train supervisor: duplicate name, skipping');
186
+ return;
187
+ }
188
+ const state: TrainState = {
189
+ name,
190
+ path,
191
+ proc: null,
192
+ pending: new Map(),
193
+ buf: '',
194
+ errBuf: '',
195
+ failCount: 0,
196
+ restartTimer: null,
197
+ startedAt: null,
198
+ stopped: false,
199
+ };
200
+ this.trains.set(name, state);
201
+ this.spawn(state);
202
+ }
203
+
204
+ private spawn(state: TrainState): void {
205
+ if (state.stopped) return;
206
+ try {
207
+ const proc = Bun.spawn(['bun', 'run', state.path], {
208
+ stdin: 'pipe',
209
+ stdout: 'pipe',
210
+ stderr: 'pipe',
211
+ env: {
212
+ ...process.env,
213
+ ...DYLD_FALLBACK_ENV,
214
+ METRO_TRAIN_NAME: state.name,
215
+ METRO_SELF_URI: daemonSelf(),
216
+ },
217
+ });
218
+ state.proc = proc;
219
+ state.startedAt = new Date().toISOString();
220
+ state.buf = '';
221
+ state.errBuf = '';
222
+ log.info({ name: state.name, pid: proc.pid }, 'train: spawned');
223
+ const pumpFailed = (err: unknown): void => {
224
+ log.warn({ name: state.name, err: errMsg(err) }, 'train: pump failed');
225
+ };
226
+ this.pumpStdout(state).catch(pumpFailed);
227
+ this.pumpStderr(state).catch(pumpFailed);
228
+ proc.exited
229
+ .then((code) => {
230
+ this.onExit(state, code ?? 0);
231
+ })
232
+ .catch((err: unknown) => {
233
+ log.error(
234
+ { name: state.name, err: errMsg(err) },
235
+ 'train: exit handler failed',
236
+ );
237
+ });
238
+ } catch (err) {
239
+ log.warn({ name: state.name, err: errMsg(err) }, 'train: spawn failed');
240
+ this.scheduleRestart(state);
241
+ }
242
+ }
243
+
244
+ private pumpStdout(state: TrainState): Promise<void> {
245
+ return pumpStream(state.proc?.stdout, state.name, 'stdout', (chunk) => {
246
+ state.buf += chunk;
247
+ state.buf = drainLines(state.name, state.buf, (line) => {
248
+ this.handleLine(state, line);
249
+ });
250
+ });
251
+ }
252
+
253
+ private pumpStderr(state: TrainState): Promise<void> {
254
+ return pumpStream(state.proc?.stderr, state.name, 'stderr', (chunk) => {
255
+ state.errBuf += chunk;
256
+ state.errBuf = drainLines(state.name, state.errBuf, (line) => {
257
+ log.warn({ train: state.name }, line);
258
+ });
259
+ });
260
+ }
261
+
262
+ private handleLine(state: TrainState, line: string): void {
263
+ const msg = parseTrainLine(state.name, line);
264
+ if (!msg || msg.op === 'ignore') return;
265
+ if (msg.op === 'response') {
266
+ const pending = state.pending.get(msg.id);
267
+ if (!pending) {
268
+ log.debug({ name: state.name, id: msg.id }, 'train: stale response');
269
+ return;
270
+ }
271
+ state.pending.delete(msg.id);
272
+ clearTimeout(pending.timer);
273
+ pending.resolve({
274
+ result: msg.result,
275
+ error: msg.error,
276
+ errorInfo: msg.errorInfo,
277
+ });
278
+ return;
279
+ }
280
+ if (msg.op === 'log') {
281
+ log.info({ name: state.name, msg: msg.text }, 'train log');
282
+ return;
283
+ }
284
+ this.onEvent?.(msg.event, state.name);
285
+ }
286
+
287
+ private onExit(state: TrainState, code: number): void {
288
+ log.warn({ name: state.name, code }, 'train: exited');
289
+ state.proc = null;
290
+ state.startedAt = null;
291
+ failAllPending(
292
+ state.pending,
293
+ `train '${state.name}' exited (code=${code}) before responding`,
294
+ );
295
+ if (state.stopped) return;
296
+ state.failCount++;
297
+ if (state.failCount >= MAX_CONSECUTIVE_FAILS) {
298
+ log.error(
299
+ { name: state.name, fails: state.failCount },
300
+ 'train: too many consecutive failures, giving up (restart metro to retry)',
301
+ );
302
+ return;
303
+ }
304
+ this.scheduleRestart(state);
305
+ }
306
+
307
+ private scheduleRestart(state: TrainState): void {
308
+ if (state.stopped) return;
309
+ const idx = Math.min(state.failCount, RESTART_BACKOFFS_MS.length - 1);
310
+ const delay = RESTART_BACKOFFS_MS[idx];
311
+ log.info(
312
+ { name: state.name, delay, attempt: state.failCount },
313
+ 'train: restart scheduled',
314
+ );
315
+ state.restartTimer = setTimeout(() => {
316
+ state.restartTimer = null;
317
+ this.spawn(state);
318
+ setTimeout(() => {
319
+ if (state.proc?.exitCode === null) state.failCount = 0;
320
+ }, 30_000);
321
+ }, delay);
322
+ }
323
+ }
@@ -0,0 +1,22 @@
1
+ import type { TrainCallResponse } from './protocol.js';
2
+
3
+ export type TrainCallBackend = (
4
+ train: string,
5
+ action: string,
6
+ args: unknown,
7
+ ) => Promise<TrainCallResponse>;
8
+
9
+ let backend: TrainCallBackend | null = null;
10
+
11
+ export function setTrainCallBackend(fn: TrainCallBackend): void {
12
+ backend = fn;
13
+ }
14
+
15
+ export function forwardTrainCall(
16
+ train: string,
17
+ action: string,
18
+ args: unknown,
19
+ ): Promise<TrainCallResponse> {
20
+ if (!backend) throw new Error('train-call backend not wired');
21
+ return backend(train, action, args);
22
+ }
@@ -0,0 +1,53 @@
1
+ export interface TrainErrorInfo {
2
+ code: string;
3
+ message: string;
4
+ retryable?: boolean;
5
+ retryAfterMs?: number;
6
+ }
7
+
8
+ export class TrainError extends Error {
9
+ readonly code: string;
10
+ readonly retryable?: boolean;
11
+ readonly retryAfterMs?: number;
12
+ constructor(
13
+ code: string,
14
+ message: string,
15
+ opts?: { retryable?: boolean; retryAfterMs?: number },
16
+ ) {
17
+ super(message);
18
+ this.name = 'TrainError';
19
+ this.code = code;
20
+ this.retryable = opts?.retryable;
21
+ this.retryAfterMs = opts?.retryAfterMs;
22
+ }
23
+ toErrorInfo(): TrainErrorInfo {
24
+ return {
25
+ code: this.code,
26
+ message: this.message,
27
+ ...(this.retryable !== undefined ? { retryable: this.retryable } : {}),
28
+ ...(this.retryAfterMs !== undefined
29
+ ? { retryAfterMs: this.retryAfterMs }
30
+ : {}),
31
+ };
32
+ }
33
+ }
34
+
35
+ export function serializeTrainError(err: unknown): {
36
+ error: string;
37
+ errorInfo?: TrainErrorInfo;
38
+ } {
39
+ if (err instanceof TrainError)
40
+ return { error: err.message, errorInfo: err.toErrorInfo() };
41
+ return { error: err instanceof Error ? err.message : String(err) };
42
+ }
43
+
44
+ export function coerceErrorInfo(v: unknown): TrainErrorInfo | undefined {
45
+ if (!v || typeof v !== 'object') return undefined;
46
+ const o = v as Record<string, unknown>;
47
+ if (typeof o.code !== 'string' || typeof o.message !== 'string')
48
+ return undefined;
49
+ const info: TrainErrorInfo = { code: o.code, message: o.message };
50
+ if (typeof o.retryable === 'boolean') info.retryable = o.retryable;
51
+ if (typeof o.retryAfterMs === 'number') info.retryAfterMs = o.retryAfterMs;
52
+ return info;
53
+ }