@stage-labs/metro 0.1.0-beta.4 → 0.1.0-beta.41

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 (249) 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 +106 -0
  6. package/dist/claude.js +15 -0
  7. package/dist/cli.js +181 -207
  8. package/dist/control.js +173 -0
  9. package/dist/plugin.js +82 -0
  10. package/dist/prompt.js +61 -0
  11. package/dist/runtime.js +117 -0
  12. package/dist/serve.js +77 -0
  13. package/dist/store.js +93 -0
  14. package/dist/tail.js +120 -0
  15. package/dist/update.js +33 -0
  16. package/dist/version.js +97 -0
  17. package/package.json +34 -21
  18. package/runtime/node_modules/@metro-labs/discord-bot/package.json +28 -0
  19. package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +119 -0
  20. package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +388 -0
  21. package/runtime/node_modules/@metro-labs/discord-bot/src/api-base.ts +1 -0
  22. package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +29 -0
  23. package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +210 -0
  24. package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +50 -0
  25. package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +115 -0
  26. package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +127 -0
  27. package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +215 -0
  28. package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +43 -0
  29. package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +19 -0
  30. package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +82 -0
  31. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-speak.ts +195 -0
  32. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-transcribe.ts +292 -0
  33. package/runtime/node_modules/@metro-labs/discord-bot/src/voice.ts +197 -0
  34. package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +5 -0
  35. package/runtime/node_modules/@metro-labs/mcp/package.json +67 -0
  36. package/runtime/node_modules/@metro-labs/mcp/src/channels/dedupe.ts +24 -0
  37. package/runtime/node_modules/@metro-labs/mcp/src/channels/inbound.ts +367 -0
  38. package/runtime/node_modules/@metro-labs/mcp/src/channels/media-note.ts +85 -0
  39. package/runtime/node_modules/@metro-labs/mcp/src/channels/pending.ts +75 -0
  40. package/runtime/node_modules/@metro-labs/mcp/src/channels/relay.ts +129 -0
  41. package/runtime/node_modules/@metro-labs/mcp/src/channels/webhook-note.ts +51 -0
  42. package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +311 -0
  43. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +334 -0
  44. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-connector-api.ts +150 -0
  45. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-import.ts +76 -0
  46. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-pair.ts +20 -0
  47. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-error.ts +8 -0
  48. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +153 -0
  49. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +99 -0
  50. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-owner.ts +32 -0
  51. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +133 -0
  52. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +278 -0
  53. package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +383 -0
  54. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +143 -0
  55. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connect-hint.ts +33 -0
  56. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +315 -0
  57. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-json.ts +21 -0
  58. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth-routes.ts +157 -0
  59. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +127 -0
  60. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +100 -0
  61. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +57 -0
  62. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +333 -0
  63. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cors.ts +29 -0
  64. package/runtime/node_modules/@metro-labs/mcp/src/daemon/crash-guard.ts +49 -0
  65. package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +117 -0
  66. package/runtime/node_modules/@metro-labs/mcp/src/daemon/hosted-connectors.ts +118 -0
  67. package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +369 -0
  68. package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +86 -0
  69. package/runtime/node_modules/@metro-labs/mcp/src/daemon/import-api.ts +67 -0
  70. package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-connectors-api.ts +56 -0
  71. package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +187 -0
  72. package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-secret.ts +21 -0
  73. package/runtime/node_modules/@metro-labs/mcp/src/daemon/log.ts +42 -0
  74. package/runtime/node_modules/@metro-labs/mcp/src/daemon/mode-api.ts +31 -0
  75. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +162 -0
  76. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +120 -0
  77. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +65 -0
  78. package/runtime/node_modules/@metro-labs/mcp/src/daemon/pair-codes.ts +58 -0
  79. package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +145 -0
  80. package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +214 -0
  81. package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +224 -0
  82. package/runtime/node_modules/@metro-labs/mcp/src/daemon/relay.ts +303 -0
  83. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +139 -0
  84. package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-source.ts +105 -0
  85. package/runtime/node_modules/@metro-labs/mcp/src/daemon/secure-fs.ts +57 -0
  86. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +27 -0
  87. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +63 -0
  88. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-config.ts +27 -0
  89. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +218 -0
  90. package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-auth.ts +82 -0
  91. package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-nonces.ts +43 -0
  92. package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-routes.ts +89 -0
  93. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor-io.ts +97 -0
  94. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor.ts +320 -0
  95. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-call.ts +22 -0
  96. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-error.ts +53 -0
  97. package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +257 -0
  98. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +295 -0
  99. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-store.ts +181 -0
  100. package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +91 -0
  101. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +214 -0
  102. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-connectors.ts +143 -0
  103. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-map.ts +84 -0
  104. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-scope.ts +64 -0
  105. package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +37 -0
  106. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +147 -0
  107. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +126 -0
  108. package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +326 -0
  109. package/runtime/node_modules/@metro-labs/mcp/src/db/file-admin.ts +319 -0
  110. package/runtime/node_modules/@metro-labs/mcp/src/db/file-source.ts +152 -0
  111. package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +21 -0
  112. package/runtime/node_modules/@metro-labs/mcp/src/db/key-map.ts +45 -0
  113. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +373 -0
  114. package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +309 -0
  115. package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +24 -0
  116. package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +105 -0
  117. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +118 -0
  118. package/runtime/node_modules/@metro-labs/mcp/src/db/token-carry.ts +45 -0
  119. package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +72 -0
  120. package/runtime/node_modules/@metro-labs/mcp/src/db/whatsapp-login.ts +18 -0
  121. package/runtime/node_modules/@metro-labs/mcp/src/mcp/accounts.ts +141 -0
  122. package/runtime/node_modules/@metro-labs/mcp/src/mcp/call-tools.ts +203 -0
  123. package/runtime/node_modules/@metro-labs/mcp/src/mcp/channel-owner.ts +29 -0
  124. package/runtime/node_modules/@metro-labs/mcp/src/mcp/ctx.ts +62 -0
  125. package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +91 -0
  126. package/runtime/node_modules/@metro-labs/mcp/src/mcp/frame-scope.ts +21 -0
  127. package/runtime/node_modules/@metro-labs/mcp/src/mcp/group-tools.ts +129 -0
  128. package/runtime/node_modules/@metro-labs/mcp/src/mcp/index.ts +192 -0
  129. package/runtime/node_modules/@metro-labs/mcp/src/mcp/member-tools.ts +72 -0
  130. package/runtime/node_modules/@metro-labs/mcp/src/mcp/permission-relay.ts +66 -0
  131. package/runtime/node_modules/@metro-labs/mcp/src/mcp/raw-get-stream.ts +207 -0
  132. package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +73 -0
  133. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-registry.ts +172 -0
  134. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +43 -0
  135. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session.ts +211 -0
  136. package/runtime/node_modules/@metro-labs/mcp/src/mcp/str.ts +5 -0
  137. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-dispatch.ts +212 -0
  138. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +397 -0
  139. package/runtime/node_modules/@metro-labs/mcp/src/mcp/upload-tool.ts +66 -0
  140. package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +240 -0
  141. package/runtime/node_modules/@metro-labs/mcp/src/server.ts +2 -0
  142. package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +80 -0
  143. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-inline.ts +100 -0
  144. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +161 -0
  145. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +200 -0
  146. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-xmtp.ts +111 -0
  147. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +182 -0
  148. package/runtime/node_modules/@metro-labs/mcp/src/stations/attachments.ts +196 -0
  149. package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +82 -0
  150. package/runtime/node_modules/@metro-labs/mcp/src/stations/messaging-normalize.ts +88 -0
  151. package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +35 -0
  152. package/runtime/node_modules/@metro-labs/mcp/src/stations/station-runtime.ts +64 -0
  153. package/runtime/node_modules/@metro-labs/mcp/src/stations/types.ts +107 -0
  154. package/runtime/node_modules/@metro-labs/telegram/package.json +28 -0
  155. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +81 -0
  156. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +254 -0
  157. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +87 -0
  158. package/runtime/node_modules/@metro-labs/telegram/src/client.ts +77 -0
  159. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +141 -0
  160. package/runtime/node_modules/@metro-labs/telegram/src/group.ts +261 -0
  161. package/runtime/node_modules/@metro-labs/telegram/src/history.ts +52 -0
  162. package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +78 -0
  163. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +46 -0
  164. package/runtime/node_modules/@metro-labs/telegram/src/login.ts +166 -0
  165. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +80 -0
  166. package/runtime/node_modules/@metro-labs/telegram/src/members.ts +75 -0
  167. package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +22 -0
  168. package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +167 -0
  169. package/runtime/node_modules/@metro-labs/telegram/src/station.ts +24 -0
  170. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +7 -0
  171. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -0
  172. package/runtime/node_modules/@metro-labs/telegram-bot/package.json +25 -0
  173. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +120 -0
  174. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +295 -0
  175. package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
  176. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +172 -0
  177. package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +103 -0
  178. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
  179. package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
  180. package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +17 -0
  181. package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +48 -0
  182. package/runtime/node_modules/@metro-labs/telegram-bot/src/verify.ts +38 -0
  183. package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +3 -0
  184. package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
  185. package/runtime/node_modules/@metro-labs/webhook/src/station.ts +40 -0
  186. package/runtime/node_modules/@metro-labs/whatsapp/package.json +29 -0
  187. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +67 -0
  188. package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
  189. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +178 -0
  190. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
  191. package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
  192. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +318 -0
  193. package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
  194. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +132 -0
  195. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
  196. package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
  197. package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
  198. package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
  199. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
  200. package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
  201. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
  202. package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
  203. package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +141 -0
  204. package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +17 -0
  205. package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
  206. package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
  207. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
  208. package/runtime/node_modules/@metro-labs/xmtp/package.json +33 -0
  209. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +113 -0
  210. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
  211. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
  212. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
  213. package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
  214. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +375 -0
  215. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
  216. package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
  217. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
  218. package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
  219. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +333 -0
  220. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +78 -0
  221. package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
  222. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
  223. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +33 -0
  224. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
  225. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
  226. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
  227. package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
  228. package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +13 -0
  229. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
  230. package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
  231. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
  232. package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +84 -0
  233. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +39 -0
  234. package/runtime/runtime.json +3 -0
  235. package/runtime/trains/.keep +0 -0
  236. package/LICENSE +0 -21
  237. package/README.md +0 -117
  238. package/dist/agents/claude.js +0 -207
  239. package/dist/agents/codex.js +0 -207
  240. package/dist/agents/types.js +0 -2
  241. package/dist/channels/discord.js +0 -104
  242. package/dist/channels/telegram.js +0 -189
  243. package/dist/helpers/scope-cache.js +0 -65
  244. package/dist/helpers/streaming.js +0 -209
  245. package/dist/helpers/telegram-format.js +0 -39
  246. package/dist/helpers/turn.js +0 -40
  247. package/dist/log.js +0 -10
  248. package/dist/orchestrator.js +0 -208
  249. package/dist/paths.js +0 -68
@@ -0,0 +1,145 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import {
3
+ closeSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ openSync,
7
+ readFileSync,
8
+ unlinkSync,
9
+ writeSync,
10
+ } from 'node:fs';
11
+ import { homedir } from 'node:os';
12
+ import { join } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { log } from './log.js';
15
+
16
+ export const STATE_DIR =
17
+ process.env.METRO_STATE_DIR ?? join(homedir(), '.cache', 'metro');
18
+ mkdirSync(STATE_DIR, { recursive: true });
19
+
20
+ export const configDir = (): string =>
21
+ process.env.METRO_CONFIG_DIR ??
22
+ join(process.env.XDG_CONFIG_HOME ?? join(homedir(), '.config'), 'metro');
23
+ export const CONFIG_DIR = configDir();
24
+ export const CONFIG_ENV_FILE = join(CONFIG_DIR, '.env');
25
+
26
+ export const TRAINS_ENV_FILE = join(homedir(), '.metro', '.env');
27
+
28
+ export function trainsDir(): string {
29
+ const env = process.env.METRO_TRAINS_DIR?.trim();
30
+ if (env !== undefined && env !== '') return env;
31
+ return fileURLToPath(new URL('../../trains', import.meta.url));
32
+ }
33
+
34
+ export const isLocalMode = (): boolean =>
35
+ process.env.METRO_MODE?.trim() === 'local';
36
+
37
+ export const envSources = (): readonly string[] => [
38
+ join(process.cwd(), '.env'),
39
+ TRAINS_ENV_FILE,
40
+ CONFIG_ENV_FILE,
41
+ ];
42
+
43
+ const LINE_RE = /^\s*([A-Za-z_]\w*)\s*=\s*(.*?)\s*$/;
44
+ const QUOTED_RE = /^(['"])(.*)\1$/;
45
+
46
+ export function readDotenv(path: string): Record<string, string> {
47
+ if (!existsSync(path)) return {};
48
+ const out: Record<string, string> = {};
49
+ for (const line of readFileSync(path, 'utf8').split('\n')) {
50
+ const m = LINE_RE.exec(line);
51
+ if (m?.[1] !== undefined && m[2] !== undefined)
52
+ out[m[1]] = m[2].replace(QUOTED_RE, '$2');
53
+ }
54
+ return out;
55
+ }
56
+
57
+ export function loadMetroEnv(): void {
58
+ for (const path of envSources()) {
59
+ for (const [k, v] of Object.entries(readDotenv(path))) {
60
+ process.env[k] ??= v;
61
+ }
62
+ }
63
+ }
64
+
65
+ function readLockPid(lockFile: string): number {
66
+ try {
67
+ return Number(readFileSync(lockFile, 'utf8').trim());
68
+ } catch {
69
+ return NaN;
70
+ }
71
+ }
72
+
73
+ function namesMetro(text: string): boolean {
74
+ return text.includes('server.ts') || text.includes('metro');
75
+ }
76
+
77
+ function holderIsMetro(pid: number): boolean {
78
+ try {
79
+ return namesMetro(readFileSync(`/proc/${pid}/cmdline`, 'utf8'));
80
+ } catch {
81
+ return holderCommand(pid);
82
+ }
83
+ }
84
+
85
+ function holderCommand(pid: number): boolean {
86
+ try {
87
+ return namesMetro(
88
+ execFileSync('ps', ['-o', 'command=', '-p', String(pid)], {
89
+ encoding: 'utf8',
90
+ stdio: ['ignore', 'pipe', 'ignore'],
91
+ }),
92
+ );
93
+ } catch {
94
+ return false;
95
+ }
96
+ }
97
+
98
+ export function lockHeldByLiveDaemon(pid: number): boolean {
99
+ if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid) return false;
100
+ try {
101
+ process.kill(pid, 0);
102
+ } catch {
103
+ return false;
104
+ }
105
+ return holderIsMetro(pid);
106
+ }
107
+
108
+ function writeOwnLock(lockFile: string): void {
109
+ const fd = openSync(lockFile, 'wx');
110
+ writeSync(fd, String(process.pid));
111
+ closeSync(fd);
112
+ process.on('exit', () => {
113
+ try {
114
+ if (readFileSync(lockFile, 'utf8').trim() === String(process.pid))
115
+ unlinkSync(lockFile);
116
+ } catch {
117
+ }
118
+ });
119
+ }
120
+
121
+ export function acquireLock(lockFile: string): void {
122
+ for (let attempt = 0; attempt < 5; attempt++) {
123
+ try {
124
+ writeOwnLock(lockFile);
125
+ return;
126
+ } catch (err) {
127
+ if ((err as NodeJS.ErrnoException).code !== 'EEXIST') throw err;
128
+ const pid = readLockPid(lockFile);
129
+ if (lockHeldByLiveDaemon(pid)) {
130
+ log.info(
131
+ { pid },
132
+ 'a healthy `metro` daemon is already running; exiting (no second dispatcher)',
133
+ );
134
+ process.exit(0);
135
+ }
136
+ try {
137
+ unlinkSync(lockFile);
138
+ } catch {
139
+ }
140
+ }
141
+ }
142
+ throw new Error(
143
+ `metro: could not acquire dispatcher lock (${lockFile}) after retries`,
144
+ );
145
+ }
@@ -0,0 +1,214 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { errMsg, log } from './log.js';
3
+ import {
4
+ apiFailure,
5
+ apiSession,
6
+ bodyField,
7
+ cors,
8
+ readJsonBody,
9
+ sendJson,
10
+ type ApiSession,
11
+ } from './api-http.js';
12
+ import { parseId } from '../db/ids.js';
13
+ import type { Member, Project } from '../db/projects.js';
14
+
15
+ const PREFIX = '/api/projects';
16
+
17
+ export interface ProjectApiDeps {
18
+ listProjects: (subject: string) => Promise<Project[]>;
19
+ createProject: (subject: string, name: unknown) => Promise<Project>;
20
+ renameProject: (
21
+ subject: string,
22
+ id: string,
23
+ name: unknown,
24
+ ) => Promise<Project>;
25
+ deleteProject: (
26
+ subject: string,
27
+ id: string,
28
+ ) => Promise<{ id: string; name: string }>;
29
+ listMembers: (subject: string, id: string) => Promise<Member[]>;
30
+ addMember: (
31
+ subject: string,
32
+ id: string,
33
+ invited: unknown,
34
+ role: unknown,
35
+ ) => Promise<Member[]>;
36
+ setMemberRole: (
37
+ subject: string,
38
+ id: string,
39
+ memberId: string,
40
+ role: unknown,
41
+ ) => Promise<Member[]>;
42
+ removeMember: (
43
+ subject: string,
44
+ id: string,
45
+ memberId: string,
46
+ ) => Promise<Member[]>;
47
+ }
48
+
49
+ type Routable =
50
+ | { kind: 'index' }
51
+ | { kind: 'project'; id: string }
52
+ | { kind: 'rename'; id: string }
53
+ | { kind: 'members'; id: string }
54
+ | { kind: 'member'; id: string; memberId: string };
55
+
56
+ type Target = Routable | { kind: 'unknown' } | null;
57
+
58
+ const ALLOWED: Record<Routable['kind'], string[]> = {
59
+ index: ['GET', 'POST'],
60
+ project: ['DELETE'],
61
+ rename: ['POST'],
62
+ members: ['GET', 'POST'],
63
+ member: ['POST', 'DELETE'],
64
+ };
65
+
66
+ function memberTarget(id: string, rest: string[]): Target {
67
+ if (rest.length === 1) return { kind: 'members', id };
68
+ if (rest.length > 2) return { kind: 'unknown' };
69
+ const memberId = parseId(rest[1] ?? '');
70
+ return memberId === null
71
+ ? { kind: 'unknown' }
72
+ : { kind: 'member', id, memberId };
73
+ }
74
+
75
+ function subTarget(id: string, rest: string[]): Target {
76
+ if (rest.length === 0) return { kind: 'project', id };
77
+ const head = rest[0];
78
+ if (head === 'members') return memberTarget(id, rest);
79
+ if (rest.length > 1) return { kind: 'unknown' };
80
+ if (head === 'rename') return { kind: 'rename', id };
81
+ return { kind: 'unknown' };
82
+ }
83
+
84
+ function target(path: string): Target {
85
+ if (path === PREFIX || path === `${PREFIX}/`) return { kind: 'index' };
86
+ if (!path.startsWith(`${PREFIX}/`)) return null;
87
+ const segments = path.slice(PREFIX.length + 1).split('/').filter(Boolean);
88
+ const head = segments[0];
89
+ if (head === undefined) return { kind: 'index' };
90
+ const id = parseId(head);
91
+ return id === null ? { kind: 'unknown' } : subTarget(id, segments.slice(1));
92
+ }
93
+
94
+ async function handleIndex(
95
+ req: IncomingMessage,
96
+ res: ServerResponse,
97
+ deps: ProjectApiDeps,
98
+ session: ApiSession,
99
+ ): Promise<void> {
100
+ if (req.method === 'GET') {
101
+ sendJson(req, res, 200, { projects: await deps.listProjects(session.subject) });
102
+ return;
103
+ }
104
+ const made = await deps.createProject(
105
+ session.subject,
106
+ bodyField(await readJsonBody(req), 'name'),
107
+ );
108
+ log.info({ id: made.id, name: made.name }, 'project-api: created project');
109
+ sendJson(req, res, 201, made);
110
+ }
111
+
112
+ async function handleMembers(
113
+ req: IncomingMessage,
114
+ res: ServerResponse,
115
+ deps: ProjectApiDeps,
116
+ session: ApiSession,
117
+ id: string,
118
+ ): Promise<void> {
119
+ if (req.method === 'GET') {
120
+ sendJson(req, res, 200, { members: await deps.listMembers(session.subject, id) });
121
+ return;
122
+ }
123
+ const body = await readJsonBody(req);
124
+ const members = await deps.addMember(
125
+ session.subject,
126
+ id,
127
+ bodyField(body, 'address'),
128
+ bodyField(body, 'role'),
129
+ );
130
+ sendJson(req, res, 200, { members });
131
+ }
132
+
133
+ async function handleMember(
134
+ req: IncomingMessage,
135
+ res: ServerResponse,
136
+ deps: ProjectApiDeps,
137
+ session: ApiSession,
138
+ tgt: { id: string; memberId: string },
139
+ ): Promise<void> {
140
+ if (req.method === 'DELETE') {
141
+ const members = await deps.removeMember(session.subject, tgt.id, tgt.memberId);
142
+ sendJson(req, res, 200, { members });
143
+ return;
144
+ }
145
+ const members = await deps.setMemberRole(
146
+ session.subject,
147
+ tgt.id,
148
+ tgt.memberId,
149
+ bodyField(await readJsonBody(req), 'role'),
150
+ );
151
+ sendJson(req, res, 200, { members });
152
+ }
153
+
154
+ async function route(
155
+ req: IncomingMessage,
156
+ res: ServerResponse,
157
+ deps: ProjectApiDeps,
158
+ session: ApiSession,
159
+ tgt: Routable,
160
+ ): Promise<void> {
161
+ try {
162
+ if (tgt.kind === 'index') await handleIndex(req, res, deps, session);
163
+ else if (tgt.kind === 'members')
164
+ await handleMembers(req, res, deps, session, tgt.id);
165
+ else if (tgt.kind === 'member')
166
+ await handleMember(req, res, deps, session, tgt);
167
+ else if (tgt.kind === 'rename')
168
+ sendJson(
169
+ req,
170
+ res,
171
+ 200,
172
+ await deps.renameProject(
173
+ session.subject,
174
+ tgt.id,
175
+ bodyField(await readJsonBody(req), 'name'),
176
+ ),
177
+ );
178
+ else sendJson(req, res, 200, await deps.deleteProject(session.subject, tgt.id));
179
+ } catch (err) {
180
+ apiFailure(req, res, err, 'project-api');
181
+ }
182
+ }
183
+
184
+ export function handleProjectApiRequest(
185
+ req: IncomingMessage,
186
+ res: ServerResponse,
187
+ deps: ProjectApiDeps,
188
+ ): boolean {
189
+ const tgt = target((req.url ?? '').split('?')[0] ?? '');
190
+ if (tgt === null) return false;
191
+ if (req.method === 'OPTIONS') {
192
+ res.writeHead(204, cors(req)).end();
193
+ return true;
194
+ }
195
+ if (tgt.kind === 'unknown') {
196
+ sendJson(req, res, 404, { error: 'no such project' });
197
+ return true;
198
+ }
199
+ if (!ALLOWED[tgt.kind].includes(req.method ?? '')) {
200
+ sendJson(req, res, 405, { error: 'method not allowed' });
201
+ return true;
202
+ }
203
+ const session = apiSession(req);
204
+ if (!session) {
205
+ sendJson(req, res, 401, { error: 'unauthorized' });
206
+ return true;
207
+ }
208
+ route(req, res, deps, session, tgt).catch((err: unknown) => {
209
+ log.warn({ err: errMsg(err) }, 'project-api: unhandled error');
210
+ if (!res.headersSent)
211
+ sendJson(req, res, 500, { error: 'project api failed' });
212
+ });
213
+ return true;
214
+ }
@@ -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
+ }