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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/dist/api.js +131 -0
  2. package/dist/bedrock-eventstream.js +120 -0
  3. package/dist/bedrock-proxy.js +260 -0
  4. package/dist/bedrock-upstream.js +72 -0
  5. package/dist/bedrock.js +104 -0
  6. package/dist/claude.js +15 -0
  7. package/dist/cli.js +157 -253
  8. package/dist/control.js +158 -0
  9. package/dist/plugin.js +82 -0
  10. package/dist/prompt.js +61 -0
  11. package/dist/runtime.js +94 -0
  12. package/dist/store.js +70 -0
  13. package/dist/tail.js +120 -0
  14. package/dist/update.js +33 -0
  15. package/dist/version.js +97 -0
  16. package/package.json +34 -21
  17. package/runtime/node_modules/@metro-labs/discord-bot/package.json +28 -0
  18. package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +119 -0
  19. package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +388 -0
  20. package/runtime/node_modules/@metro-labs/discord-bot/src/api-base.ts +1 -0
  21. package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +29 -0
  22. package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +210 -0
  23. package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +50 -0
  24. package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +115 -0
  25. package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +127 -0
  26. package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +215 -0
  27. package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +43 -0
  28. package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +19 -0
  29. package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +82 -0
  30. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-speak.ts +195 -0
  31. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-transcribe.ts +292 -0
  32. package/runtime/node_modules/@metro-labs/discord-bot/src/voice.ts +197 -0
  33. package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +5 -0
  34. package/runtime/node_modules/@metro-labs/mcp/package.json +66 -0
  35. package/runtime/node_modules/@metro-labs/mcp/src/channels/dedupe.ts +24 -0
  36. package/runtime/node_modules/@metro-labs/mcp/src/channels/inbound.ts +367 -0
  37. package/runtime/node_modules/@metro-labs/mcp/src/channels/media-note.ts +85 -0
  38. package/runtime/node_modules/@metro-labs/mcp/src/channels/pending.ts +75 -0
  39. package/runtime/node_modules/@metro-labs/mcp/src/channels/relay.ts +129 -0
  40. package/runtime/node_modules/@metro-labs/mcp/src/channels/webhook-note.ts +51 -0
  41. package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +311 -0
  42. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +337 -0
  43. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-error.ts +8 -0
  44. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +129 -0
  45. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +99 -0
  46. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-owner.ts +32 -0
  47. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +129 -0
  48. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +278 -0
  49. package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +321 -0
  50. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +123 -0
  51. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair.ts +20 -0
  52. package/runtime/node_modules/@metro-labs/mcp/src/daemon/collection-api.ts +202 -0
  53. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +339 -0
  54. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-json.ts +21 -0
  55. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth-routes.ts +157 -0
  56. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +127 -0
  57. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +100 -0
  58. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +57 -0
  59. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +333 -0
  60. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cors.ts +29 -0
  61. package/runtime/node_modules/@metro-labs/mcp/src/daemon/crash-guard.ts +49 -0
  62. package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +117 -0
  63. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-auth.ts +164 -0
  64. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-oauth.ts +208 -0
  65. package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +359 -0
  66. package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +79 -0
  67. package/runtime/node_modules/@metro-labs/mcp/src/daemon/log.ts +42 -0
  68. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +162 -0
  69. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +120 -0
  70. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +65 -0
  71. package/runtime/node_modules/@metro-labs/mcp/src/daemon/pair-codes.ts +58 -0
  72. package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +134 -0
  73. package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +214 -0
  74. package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +224 -0
  75. package/runtime/node_modules/@metro-labs/mcp/src/daemon/relay.ts +302 -0
  76. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +130 -0
  77. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-pair.ts +18 -0
  78. package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-source.ts +105 -0
  79. package/runtime/node_modules/@metro-labs/mcp/src/daemon/secure-fs.ts +57 -0
  80. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +27 -0
  81. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +43 -0
  82. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +218 -0
  83. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor-io.ts +97 -0
  84. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor.ts +323 -0
  85. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-call.ts +22 -0
  86. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-error.ts +53 -0
  87. package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +169 -0
  88. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +295 -0
  89. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-store.ts +181 -0
  90. package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +91 -0
  91. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +215 -0
  92. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-map.ts +71 -0
  93. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-scope.ts +64 -0
  94. package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +37 -0
  95. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-collections.ts +218 -0
  96. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +147 -0
  97. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +126 -0
  98. package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +308 -0
  99. package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +19 -0
  100. package/runtime/node_modules/@metro-labs/mcp/src/db/key-map.ts +45 -0
  101. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +382 -0
  102. package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +300 -0
  103. package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +31 -0
  104. package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +105 -0
  105. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +131 -0
  106. package/runtime/node_modules/@metro-labs/mcp/src/db/token-carry.ts +45 -0
  107. package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +57 -0
  108. package/runtime/node_modules/@metro-labs/mcp/src/db/whatsapp-login.ts +18 -0
  109. package/runtime/node_modules/@metro-labs/mcp/src/mcp/accounts.ts +145 -0
  110. package/runtime/node_modules/@metro-labs/mcp/src/mcp/call-tools.ts +203 -0
  111. package/runtime/node_modules/@metro-labs/mcp/src/mcp/channel-owner.ts +29 -0
  112. package/runtime/node_modules/@metro-labs/mcp/src/mcp/ctx.ts +62 -0
  113. package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +91 -0
  114. package/runtime/node_modules/@metro-labs/mcp/src/mcp/frame-scope.ts +21 -0
  115. package/runtime/node_modules/@metro-labs/mcp/src/mcp/group-tools.ts +129 -0
  116. package/runtime/node_modules/@metro-labs/mcp/src/mcp/index.ts +192 -0
  117. package/runtime/node_modules/@metro-labs/mcp/src/mcp/member-tools.ts +72 -0
  118. package/runtime/node_modules/@metro-labs/mcp/src/mcp/permission-relay.ts +66 -0
  119. package/runtime/node_modules/@metro-labs/mcp/src/mcp/raw-get-stream.ts +207 -0
  120. package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +73 -0
  121. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-registry.ts +172 -0
  122. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +43 -0
  123. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session.ts +211 -0
  124. package/runtime/node_modules/@metro-labs/mcp/src/mcp/str.ts +5 -0
  125. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-dispatch.ts +212 -0
  126. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +397 -0
  127. package/runtime/node_modules/@metro-labs/mcp/src/mcp/upload-tool.ts +66 -0
  128. package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +240 -0
  129. package/runtime/node_modules/@metro-labs/mcp/src/server.ts +2 -0
  130. package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +80 -0
  131. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-inline.ts +100 -0
  132. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +161 -0
  133. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +200 -0
  134. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-xmtp.ts +111 -0
  135. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +182 -0
  136. package/runtime/node_modules/@metro-labs/mcp/src/stations/attachments.ts +196 -0
  137. package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +82 -0
  138. package/runtime/node_modules/@metro-labs/mcp/src/stations/messaging-normalize.ts +88 -0
  139. package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +35 -0
  140. package/runtime/node_modules/@metro-labs/mcp/src/stations/station-runtime.ts +64 -0
  141. package/runtime/node_modules/@metro-labs/mcp/src/stations/types.ts +107 -0
  142. package/runtime/node_modules/@metro-labs/telegram/package.json +28 -0
  143. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +81 -0
  144. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +254 -0
  145. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +87 -0
  146. package/runtime/node_modules/@metro-labs/telegram/src/client.ts +77 -0
  147. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +141 -0
  148. package/runtime/node_modules/@metro-labs/telegram/src/group.ts +261 -0
  149. package/runtime/node_modules/@metro-labs/telegram/src/history.ts +52 -0
  150. package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +78 -0
  151. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +46 -0
  152. package/runtime/node_modules/@metro-labs/telegram/src/login.ts +166 -0
  153. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +80 -0
  154. package/runtime/node_modules/@metro-labs/telegram/src/members.ts +75 -0
  155. package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +22 -0
  156. package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +167 -0
  157. package/runtime/node_modules/@metro-labs/telegram/src/station.ts +24 -0
  158. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +7 -0
  159. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -0
  160. package/runtime/node_modules/@metro-labs/telegram-bot/package.json +25 -0
  161. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +120 -0
  162. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +295 -0
  163. package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
  164. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +172 -0
  165. package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +103 -0
  166. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
  167. package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
  168. package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +17 -0
  169. package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +48 -0
  170. package/runtime/node_modules/@metro-labs/telegram-bot/src/verify.ts +38 -0
  171. package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +3 -0
  172. package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
  173. package/runtime/node_modules/@metro-labs/webhook/src/station.ts +40 -0
  174. package/runtime/node_modules/@metro-labs/whatsapp/package.json +29 -0
  175. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +67 -0
  176. package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
  177. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +178 -0
  178. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
  179. package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
  180. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +318 -0
  181. package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
  182. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +132 -0
  183. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
  184. package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
  185. package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
  186. package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
  187. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
  188. package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
  189. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
  190. package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
  191. package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +141 -0
  192. package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +17 -0
  193. package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
  194. package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
  195. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
  196. package/runtime/node_modules/@metro-labs/xmtp/package.json +33 -0
  197. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +113 -0
  198. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
  199. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
  200. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
  201. package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
  202. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +375 -0
  203. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
  204. package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
  205. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
  206. package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
  207. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +333 -0
  208. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +78 -0
  209. package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
  210. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
  211. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +33 -0
  212. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
  213. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
  214. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
  215. package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
  216. package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +13 -0
  217. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
  218. package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
  219. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
  220. package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +84 -0
  221. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +39 -0
  222. package/runtime/runtime.json +3 -0
  223. package/runtime/trains/.keep +0 -0
  224. package/LICENSE +0 -21
  225. package/README.md +0 -114
  226. package/dist/agents/claude.js +0 -229
  227. package/dist/agents/codex.js +0 -282
  228. package/dist/agents/types.js +0 -3
  229. package/dist/channels/discord.js +0 -142
  230. package/dist/channels/telegram.js +0 -209
  231. package/dist/lib/dotenv.js +0 -31
  232. package/dist/lib/scope-cache.js +0 -85
  233. package/dist/lib/streaming.js +0 -207
  234. package/dist/log.js +0 -12
  235. package/dist/orchestrator.js +0 -399
  236. package/dist/paths.js +0 -33
package/dist/claude.js ADDED
@@ -0,0 +1,15 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ const CHANNEL_FLAGS = ['--dangerously-load-development-channels', 'server:metro'];
3
+ export const claudeArgs = (extra) => [
4
+ ...CHANNEL_FLAGS,
5
+ ...extra,
6
+ ];
7
+ export function launchClaude(extra) {
8
+ const leaveToChild = () => undefined;
9
+ process.on('SIGINT', leaveToChild);
10
+ process.on('SIGTERM', leaveToChild);
11
+ const res = spawnSync('claude', claudeArgs(extra), { stdio: 'inherit' });
12
+ if (res.error !== undefined)
13
+ throw new Error('the `claude` command is not on PATH — install Claude Code first');
14
+ return Promise.resolve(res.status ?? 1);
15
+ }
package/dist/cli.js CHANGED
@@ -1,266 +1,170 @@
1
1
  #!/usr/bin/env node
2
- import { spawn } from 'node:child_process';
3
- import { existsSync, readFileSync } from 'node:fs';
4
- import { join } from 'node:path';
5
- import pkg from '../package.json' with { type: 'json' };
6
- import * as discord from './channels/discord.js';
7
- import * as telegram from './channels/telegram.js';
8
- import { readDotenv, writeDotenv } from './lib/dotenv.js';
9
- import { errMsg } from './log.js';
10
- import { CONFIG_ENV_FILE, configuredPlatforms, loadMetroEnv, STATE_DIR } from './paths.js';
11
- const USAGE = `metro — Telegram + Discord bridge for your Claude Code / Codex agent
2
+ import { hostname } from 'node:os';
3
+ import { claimCode, claimRuntime, mcpServers, NotSignedIn, whoisAuthorized, } from './api.js';
4
+ import { askSecret } from './prompt.js';
5
+ import { clearToken, credentialsPath, metroUrl, metroWebUrl, writeToken, } from './store.js';
6
+ import { detach, lockedBy, probe, runningPid, stopAll, tail } from './control.js';
7
+ import { tailEvents } from './tail.js';
8
+ import { assertAgentId, daemonPlan, localUrl, readRunToken, runDaemon, runtimeDir, writeRunToken, } from './runtime.js';
9
+ import { launchClaude } from './claude.js';
10
+ import { bedrock } from './bedrock.js';
11
+ import { installPlugin, syncPluginServers } from './plugin.js';
12
+ import { update } from './update.js';
13
+ import { currentVersion } from './version.js';
14
+ const USAGE = `metro — the command line for your MCP connectors
12
15
 
13
- Usage:
14
- metro Run the orchestrator daemon.
15
- Each chat conversation gets its own
16
- codex session, with streaming responses.
17
- metro setup Show config status (tokens).
18
- metro setup telegram <token> Save TELEGRAM_BOT_TOKEN (validated via getMe).
19
- metro setup discord <token> Save DISCORD_BOT_TOKEN (validated via getMe).
20
- metro setup clear [telegram|discord|all] Remove tokens.
21
- metro doctor Health check: tokens + gateway reachability.
22
- metro update Upgrade in place (npm/bun/pnpm auto-detected).
23
- metro --version, -v Print version.
24
- metro --help, -h This help.
16
+ metro start <agent-id> [--detach]
17
+ run that agent's stations on this machine
18
+ metro stop [agent-id]
19
+ stop every metro daemon on this machine, however it was started
20
+ metro status <agent-id>
21
+ is it running, and is it healthy
22
+ metro logs <agent-id> [-f]
23
+ show the detached daemon's log
24
+ metro tail <agent-id>
25
+ follow this machine's inbound events, one JSON line each
26
+ metro login authorize a connector collection with a code from the web UI
27
+ metro logout forget this machine's sign-in
28
+ metro whoami print the account and collection this machine may read
29
+ metro mcp print the mcpServers block for the authorized collection
30
+ metro plugin set up the Claude Code plugin (connector servers + /metro:login)
31
+ metro claude [args...]
32
+ open Claude Code with the metro channel; every argument is passed through
33
+ metro bedrock [args...]
34
+ the same, with inference on Amazon Bedrock through a local proxy so the
35
+ channel still works (needs AWS_BEARER_TOKEN_BEDROCK and AWS_REGION)
36
+ metro update update to the newest published version
37
+ metro version print this CLI's version
25
38
 
26
- Exit codes:
27
- 0 success · 1 usage · 2 config (no tokens — run \`metro setup\`) · 3 upstream
39
+ Start Claude Code with all of them, without writing them to disk:
40
+
41
+ claude --mcp-config <(metro mcp)
42
+
43
+ METRO_URL the metro to talk to (default https://mcp.metro.box)
44
+ METRO_UI_URL where the web UI lives (default https://metro.box)
45
+ METRO_TOKEN use this connector sign-in instead of the stored one
46
+ METRO_RUN_TOKEN use this runtime authorization instead of the stored one
47
+ METRO_BEDROCK_MODEL
48
+ send every request to this Bedrock model id (default: derive from the
49
+ model Claude Code asks for, e.g. eu.anthropic.claude-sonnet-4-6)
50
+ METRO_RUNTIME_DIR run the daemon from this directory instead of the bundled one
28
51
  `;
29
- function exitErr(msg, code) { return Object.assign(new Error(msg), { code }); }
30
- function parseArgs(argv) {
31
- const positional = [];
32
- const flags = {};
33
- for (let i = 0; i < argv.length; i++) {
34
- const a = argv[i];
35
- if (!a.startsWith('--')) {
36
- positional.push(a);
37
- continue;
38
- }
39
- const eq = a.indexOf('=');
40
- if (eq !== -1) {
41
- flags[a.slice(2, eq)] = a.slice(eq + 1);
42
- continue;
43
- }
44
- const key = a.slice(2);
45
- const next = argv[i + 1];
46
- if (next !== undefined && !next.startsWith('--')) {
47
- flags[key] = next;
48
- i++;
49
- }
50
- else
51
- flags[key] = true;
52
- }
53
- return { positional, flags };
52
+ function hostLabel() {
53
+ const name = hostname().trim();
54
+ return name === '' ? 'unnamed machine' : name;
54
55
  }
55
- const isJson = (f) => f.json === true;
56
- const emitResult = (f, human, structured) => {
57
- process.stdout.write(isJson(f) ? JSON.stringify(structured) + '\n' : human + '\n');
58
- };
59
- function maskToken(t) {
60
- if (!t)
61
- return '';
62
- if (t.length <= 8)
63
- return '••••';
64
- return `${t.slice(0, 6)}…${t.slice(-2)}`;
56
+ async function authorizeRuntime(agentId) {
57
+ process.stderr.write(`Authorize this machine at ${metroWebUrl()}/#/authorize/${agentId}\n`);
58
+ const code = (await askSecret('Paste the code (input is hidden): ')).trim();
59
+ if (code === '')
60
+ throw new Error('no code given');
61
+ const claimed = await claimRuntime(code, hostLabel());
62
+ writeRunToken(claimed.agent, claimed.token);
63
+ process.stderr.write(`Authorized as '${claimed.label}'.\n`);
64
+ return claimed.token;
65
65
  }
66
- const TOKEN_KEYS = { telegram: 'TELEGRAM_BOT_TOKEN', discord: 'DISCORD_BOT_TOKEN' };
67
- async function cmdSetup(positional, flags) {
68
- const [sub, value] = positional;
69
- if (!sub)
70
- return cmdSetupStatus(flags);
71
- if (sub === 'telegram' || sub === 'discord') {
72
- if (!value)
73
- throw new Error(`metro setup ${sub} <token> — token is required`);
74
- const trimmed = value.trim();
75
- let identity;
76
- if (!flags['no-validate']) {
77
- process.env[TOKEN_KEYS[sub]] = trimmed;
78
- try {
79
- identity = sub === 'telegram' ? `@${(await telegram.getMe()).username}` : (await discord.getMe()).username;
80
- }
81
- catch (err) {
82
- delete process.env[TOKEN_KEYS[sub]];
83
- throw exitErr(`token rejected by ${sub}: ${errMsg(err)} (use --no-validate to save anyway)`, 3);
84
- }
85
- }
86
- const env = readDotenv(CONFIG_ENV_FILE);
87
- env[TOKEN_KEYS[sub]] = trimmed;
88
- writeDotenv(CONFIG_ENV_FILE, env);
89
- const human = identity
90
- ? `saved ${TOKEN_KEYS[sub]} (verified as ${identity}) to ${CONFIG_ENV_FILE} (chmod 0600)`
91
- : `saved ${TOKEN_KEYS[sub]} to ${CONFIG_ENV_FILE} (chmod 0600)`;
92
- emitResult(flags, human, { ok: true, saved: TOKEN_KEYS[sub], verified_as: identity ?? null });
93
- return;
94
- }
95
- if (sub === 'clear') {
96
- const target = value ?? 'all';
97
- const env = readDotenv(CONFIG_ENV_FILE);
98
- if (target === 'all') {
99
- delete env.TELEGRAM_BOT_TOKEN;
100
- delete env.DISCORD_BOT_TOKEN;
101
- }
102
- else if (target === 'telegram' || target === 'discord')
103
- delete env[TOKEN_KEYS[target]];
104
- else
105
- throw new Error(`metro setup clear <telegram|discord|all> — got '${target}'`);
106
- writeDotenv(CONFIG_ENV_FILE, env);
107
- emitResult(flags, `cleared ${target === 'all' ? 'all metro tokens' : TOKEN_KEYS[target]}`, { ok: true, cleared: target });
108
- return;
109
- }
110
- throw new Error(`unknown setup subcommand '${sub}' (try: telegram, discord, clear)`);
66
+ async function start(argv) {
67
+ const agentId = assertAgentId(argv[0]);
68
+ if (runningPid(agentId) !== null || lockedBy() !== null)
69
+ throw new Error('a metro daemon is already running on this machine. ' +
70
+ 'Stop it first: metro stop');
71
+ const dir = runtimeDir();
72
+ const token = readRunToken(agentId) ?? (await authorizeRuntime(agentId));
73
+ const detached = argv.includes('--detach');
74
+ const plan = daemonPlan({ agentId, token, dir });
75
+ if (!detached)
76
+ return runDaemon(agentId, plan);
77
+ const pid = detach({ agentId, ...plan });
78
+ process.stderr.write(`metro is running for ${agentId} (pid ${String(pid)})\n` +
79
+ ` logs: metro logs ${agentId} -f\n` +
80
+ ` stop: metro stop ${agentId}\n`);
81
+ return 0;
111
82
  }
112
- async function cmdSetupStatus(flags) {
113
- loadMetroEnv();
114
- const tg = process.env.TELEGRAM_BOT_TOKEN ?? '';
115
- const dc = process.env.DISCORD_BOT_TOKEN ?? '';
116
- if (isJson(flags)) {
117
- process.stdout.write(JSON.stringify({
118
- version: pkg.version,
119
- config_env_file: CONFIG_ENV_FILE,
120
- tokens: {
121
- telegram: { set: !!tg, masked: maskToken(tg) },
122
- discord: { set: !!dc, masked: maskToken(dc) },
123
- },
124
- }) + '\n');
125
- return;
126
- }
127
- process.stdout.write(`metro ${pkg.version}\n\n` +
128
- `config: ${CONFIG_ENV_FILE}${existsSync(CONFIG_ENV_FILE) ? '' : ' (not yet written)'}\n\n` +
129
- ` TELEGRAM_BOT_TOKEN ${tg ? `set (${maskToken(tg)})` : 'not set'}\n` +
130
- ` DISCORD_BOT_TOKEN ${dc ? `set (${maskToken(dc)})` : 'not set'}\n\n`);
131
- if (!tg && !dc) {
132
- process.stdout.write('Get started:\n' +
133
- ' 1. metro setup telegram <token> # https://t.me/BotFather\n' +
134
- ' metro setup discord <token> # https://discord.com/developers/applications\n' +
135
- ' 2. metro doctor # verify\n' +
136
- ' 3. metro # run the orchestrator\n');
137
- }
138
- else {
139
- process.stdout.write('Run `metro` to start the orchestrator, or `metro doctor` to verify.\n');
140
- }
83
+ async function stopDaemon(argv) {
84
+ const agentId = argv[0] === undefined ? undefined : assertAgentId(argv[0]);
85
+ const stopped = await stopAll(agentId);
86
+ if (stopped.length === 0) {
87
+ process.stderr.write('no metro daemon is running on this machine\n');
88
+ return 1;
89
+ }
90
+ for (const d of stopped)
91
+ process.stderr.write(`Stopped metro (pid ${String(d.pid)}, via ${d.via})\n`);
92
+ return 0;
141
93
  }
142
- async function cmdDoctor(flags) {
143
- loadMetroEnv();
144
- const checks = [];
145
- const cfg = configuredPlatforms();
146
- checks.push({
147
- name: 'tokens',
148
- ok: cfg.telegram || cfg.discord,
149
- detail: cfg.telegram || cfg.discord
150
- ? `loaded from ${existsSync(CONFIG_ENV_FILE) ? CONFIG_ENV_FILE : 'process env'}`
151
- : 'no platform configured — run `metro setup telegram|discord <token>`',
152
- });
153
- for (const [platform, getMe] of [['telegram', telegram.getMe], ['discord', discord.getMe]]) {
154
- if (!cfg[platform]) {
155
- checks.push({ name: platform, ok: null, detail: 'not configured' });
156
- continue;
157
- }
158
- try {
159
- const me = await getMe();
160
- checks.push({ name: platform, ok: true, detail: `getMe → ${platform === 'telegram' ? '@' : ''}${me.username}` });
161
- }
162
- catch (err) {
163
- checks.push({ name: platform, ok: false, detail: errMsg(err) });
164
- }
165
- }
166
- const lockFile = join(STATE_DIR, '.tail-lock');
167
- if (!existsSync(lockFile)) {
168
- checks.push({ name: 'orchestrator', ok: null, detail: 'not running' });
169
- }
170
- else {
171
- try {
172
- const pid = Number(readFileSync(lockFile, 'utf8').trim());
173
- if (!Number.isInteger(pid) || pid <= 0)
174
- throw new Error('invalid pid');
175
- process.kill(pid, 0);
176
- checks.push({ name: 'orchestrator', ok: true, detail: `running (pid ${pid})` });
177
- }
178
- catch {
179
- checks.push({ name: 'orchestrator', ok: null, detail: 'stale lockfile (will auto-reclaim on next start)' });
180
- }
181
- }
182
- if (isJson(flags)) {
183
- process.stdout.write(JSON.stringify({ checks }) + '\n');
184
- }
185
- else {
186
- process.stdout.write('metro doctor\n\n');
187
- for (const c of checks) {
188
- const icon = c.ok === true ? '✓' : c.ok === false ? '✗' : '–';
189
- process.stdout.write(` ${icon} ${c.name.padEnd(15)} ${c.detail}\n`);
190
- }
191
- process.stdout.write('\n');
192
- }
193
- if (checks.some(c => c.ok === false))
194
- throw exitErr('one or more checks failed', 3);
94
+ async function status(argv) {
95
+ const agentId = assertAgentId(argv[0]);
96
+ const pid = runningPid(agentId);
97
+ if (pid === null) {
98
+ process.stdout.write(`${agentId}: not running\n`);
99
+ return 1;
100
+ }
101
+ const health = await probe(localUrl());
102
+ process.stdout.write(`${agentId}: running (pid ${String(pid)}) · ` +
103
+ (health === null
104
+ ? `not answering on ${localUrl()}\n`
105
+ : `healthy, up ${String(health.uptime)}s\n`));
106
+ return health === null ? 1 : 0;
195
107
  }
196
- async function cmdUpdate(flags) {
197
- const tag = pkg.version.includes('-') ? 'beta' : 'latest';
198
- const res = await fetch('https://registry.npmjs.org/@stage-labs/metro', { signal: AbortSignal.timeout(15_000) });
199
- if (!res.ok)
200
- throw new Error(`npm registry: ${res.status}`);
201
- const data = (await res.json());
202
- const latest = data['dist-tags']?.[tag];
203
- if (!latest)
204
- throw new Error(`no '${tag}' dist-tag for @stage-labs/metro`);
205
- if (latest === pkg.version) {
206
- emitResult(flags, `already on ${pkg.version} (latest ${tag})`, { ok: true, current: pkg.version, latest, upgraded: false });
207
- return;
208
- }
209
- const argv1 = process.argv[1] ?? '';
210
- const spec = `@stage-labs/metro@${tag}`;
211
- const argv = argv1.includes('/.bun/') || argv1.includes('\\bun\\') ? ['bun', 'add', '-g', spec]
212
- : argv1.includes('/pnpm/') || argv1.includes('\\pnpm\\') ? ['pnpm', 'add', '-g', spec]
213
- : ['npm', 'install', '-g', spec];
214
- if (isJson(flags)) {
215
- process.stdout.write(JSON.stringify({ ok: true, current: pkg.version, latest, command: argv.join(' ') }) + '\n');
216
- }
217
- else {
218
- process.stdout.write(`metro ${pkg.version} → ${latest}\n$ ${argv.join(' ')}\n`);
219
- }
220
- await new Promise((resolve, reject) => {
221
- const child = spawn(argv[0], argv.slice(1), { stdio: isJson(flags) ? 'ignore' : 'inherit' });
222
- child.on('exit', code => (code === 0 ? resolve() : reject(new Error(`${argv[0]} exited ${code}`))));
223
- child.on('error', reject);
224
- });
108
+ function logs(argv) {
109
+ const agentId = assertAgentId(argv[0]);
110
+ return tail(agentId, argv.includes('-f') || argv.includes('--follow'));
111
+ }
112
+ async function login() {
113
+ process.stderr.write(`Choose a connector collection at ${metroWebUrl()}/#/authorize\n`);
114
+ const { token, email, collection } = await claimCode(await askSecret('Paste the code here (input is hidden): '));
115
+ writeToken(token);
116
+ process.stderr.write(`Authorized '${collection}' for ${email}. Stored in ${credentialsPath()}\n`);
117
+ if (syncPluginServers())
118
+ process.stderr.write('Claude Code plugin refreshed — new sessions have the connectors; ' +
119
+ 'run /reload-plugins in any session already open.\n');
120
+ }
121
+ async function whoami() {
122
+ const { email, collection } = await whoisAuthorized();
123
+ process.stdout.write(`${email} · collection '${collection}' on ${metroUrl()}\n`);
225
124
  }
125
+ const HELP = new Set([undefined, 'help', '--help', '-h']);
226
126
  const COMMANDS = {
227
- setup: cmdSetup,
228
- doctor: (_, f) => cmdDoctor(f),
229
- update: (_, f) => cmdUpdate(f),
127
+ login: async () => {
128
+ await login();
129
+ return 0;
130
+ },
131
+ logout: async () => {
132
+ clearToken();
133
+ process.stderr.write('Signed out.\n');
134
+ return Promise.resolve(0);
135
+ },
136
+ whoami: async () => {
137
+ await whoami();
138
+ return 0;
139
+ },
140
+ start: () => start(process.argv.slice(3)),
141
+ stop: () => stopDaemon(process.argv.slice(3)),
142
+ status: () => status(process.argv.slice(3)),
143
+ logs: () => logs(process.argv.slice(3)),
144
+ tail: () => tailEvents(process.argv.slice(3)),
145
+ mcp: async () => {
146
+ process.stdout.write(`${await mcpServers()}\n`);
147
+ return 0;
148
+ },
149
+ plugin: installPlugin,
150
+ claude: () => launchClaude(process.argv.slice(3)),
151
+ bedrock: () => bedrock(process.argv.slice(3)),
152
+ update,
153
+ version: async () => {
154
+ process.stdout.write(`${currentVersion()}\n`);
155
+ return Promise.resolve(0);
156
+ },
230
157
  };
231
- async function main() {
232
- const cmd = process.argv[2];
233
- if (cmd === '--version' || cmd === '-v') {
234
- process.stdout.write(`${pkg.version}\n`);
235
- return;
236
- }
237
- if (cmd === '--help' || cmd === '-h') {
238
- process.stdout.write(USAGE);
239
- return;
240
- }
241
- // Bare `metro` runs the orchestrator (the daemon's the default).
242
- if (!cmd) {
243
- await import('./orchestrator.js');
244
- return;
245
- }
246
- const handler = COMMANDS[cmd];
247
- if (!handler) {
248
- process.stderr.write(`unknown command '${cmd}'\n\n${USAGE}`);
249
- process.exit(1);
250
- }
251
- const { positional, flags } = parseArgs(process.argv.slice(3));
252
- try {
253
- await handler(positional, flags);
254
- }
255
- catch (err) {
256
- const code = err.code;
257
- if (isJson(flags)) {
258
- process.stdout.write(JSON.stringify({ ok: false, error: errMsg(err), code: code ?? 1 }) + '\n');
259
- }
260
- else {
261
- process.stderr.write(`error: ${errMsg(err)}\n`);
262
- }
263
- process.exit(typeof code === 'number' ? code : 1);
264
- }
158
+ async function run(command) {
159
+ const handler = command === undefined ? undefined : COMMANDS[command];
160
+ if (handler !== undefined)
161
+ return handler();
162
+ process.stderr.write(USAGE);
163
+ return Promise.resolve(HELP.has(command) ? 0 : 1);
265
164
  }
266
- await main();
165
+ const code = await run(process.argv[2]).catch((err) => {
166
+ const message = err instanceof Error ? err.message : String(err);
167
+ process.stderr.write(`metro: ${message}\n`);
168
+ return err instanceof NotSignedIn ? 2 : 1;
169
+ });
170
+ process.exit(code);
@@ -0,0 +1,158 @@
1
+ import { execFileSync, spawn } from 'node:child_process';
2
+ import { existsSync, mkdirSync, openSync, readdirSync, readFileSync, rmSync, writeFileSync, } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ const STATE = () => join(homedir(), '.metro');
6
+ const pidPath = (agentId) => join(STATE(), `run-${agentId}.pid`);
7
+ export const logPath = (agentId) => join(STATE(), 'logs', `${agentId}.log`);
8
+ function ensureStateDirs() {
9
+ mkdirSync(STATE(), { recursive: true, mode: 0o700 });
10
+ mkdirSync(join(STATE(), 'logs'), { recursive: true, mode: 0o700 });
11
+ }
12
+ function readPid(agentId) {
13
+ try {
14
+ const pid = Number(readFileSync(pidPath(agentId), 'utf8').trim());
15
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
21
+ function isAlive(pid) {
22
+ try {
23
+ process.kill(pid, 0);
24
+ return true;
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ export function runningPid(agentId) {
31
+ const pid = readPid(agentId);
32
+ if (pid === null)
33
+ return null;
34
+ if (isAlive(pid))
35
+ return pid;
36
+ rmSync(pidPath(agentId), { force: true });
37
+ return null;
38
+ }
39
+ function writePid(agentId, pid) {
40
+ writeFileSync(pidPath(agentId), String(pid));
41
+ }
42
+ function clearPid(agentId) {
43
+ rmSync(pidPath(agentId), { force: true });
44
+ }
45
+ export function detach(opts) {
46
+ ensureStateDirs();
47
+ const out = openSync(logPath(opts.agentId), 'a');
48
+ const child = spawn(opts.command, opts.args, {
49
+ cwd: opts.cwd,
50
+ env: opts.env,
51
+ detached: true,
52
+ stdio: ['ignore', out, out],
53
+ });
54
+ child.unref();
55
+ const pid = child.pid ?? 0;
56
+ if (pid === 0)
57
+ throw new Error('could not start the metro daemon');
58
+ writePid(opts.agentId, pid);
59
+ return pid;
60
+ }
61
+ async function stopPid(pid, waitMs) {
62
+ process.kill(pid, 'SIGTERM');
63
+ const until = Date.now() + waitMs;
64
+ while (Date.now() < until) {
65
+ if (!isAlive(pid))
66
+ return;
67
+ await new Promise((r) => setTimeout(r, 100));
68
+ }
69
+ process.kill(pid, 'SIGKILL');
70
+ }
71
+ const lockPath = () => process.env.METRO_STATE_DIR?.trim()
72
+ ? join(process.env.METRO_STATE_DIR.trim(), '.tail-lock')
73
+ : join(homedir(), '.cache', 'metro', '.tail-lock');
74
+ function commandOf(pid) {
75
+ try {
76
+ return execFileSync('ps', ['-o', 'command=', '-p', String(pid)], {
77
+ encoding: 'utf8',
78
+ stdio: ['ignore', 'pipe', 'ignore'],
79
+ });
80
+ }
81
+ catch {
82
+ return '';
83
+ }
84
+ }
85
+ const looksLikeMetro = (command) => command.includes('server.ts') || command.includes('metro');
86
+ export function lockedBy() {
87
+ let pid;
88
+ try {
89
+ pid = Number(readFileSync(lockPath(), 'utf8').trim());
90
+ }
91
+ catch {
92
+ return null;
93
+ }
94
+ if (!Number.isInteger(pid) || pid <= 0 || !isAlive(pid))
95
+ return null;
96
+ return looksLikeMetro(commandOf(pid)) ? pid : null;
97
+ }
98
+ function trackedAgents(agentId) {
99
+ const tracked = new Set();
100
+ if (agentId !== undefined)
101
+ tracked.add(agentId);
102
+ try {
103
+ for (const name of readdirSync(STATE())) {
104
+ const found = /^run-(.+)\.pid$/.exec(name);
105
+ if (found?.[1] !== undefined)
106
+ tracked.add(found[1]);
107
+ }
108
+ }
109
+ catch {
110
+ return tracked;
111
+ }
112
+ return tracked;
113
+ }
114
+ export async function stopAll(agentId, waitMs = 10_000) {
115
+ const stopped = [];
116
+ const seen = new Set();
117
+ for (const id of trackedAgents(agentId)) {
118
+ const pid = runningPid(id);
119
+ if (pid === null || seen.has(pid))
120
+ continue;
121
+ seen.add(pid);
122
+ await stopPid(pid, waitMs);
123
+ clearPid(id);
124
+ stopped.push({ pid, via: id });
125
+ }
126
+ const holder = lockedBy();
127
+ if (holder !== null && !seen.has(holder)) {
128
+ await stopPid(holder, waitMs);
129
+ rmSync(lockPath(), { force: true });
130
+ stopped.push({ pid: holder, via: 'the machine lock' });
131
+ }
132
+ return stopped;
133
+ }
134
+ export function tail(agentId, follow) {
135
+ const path = logPath(agentId);
136
+ if (!existsSync(path))
137
+ throw new Error(`no log for ${agentId} yet — it is only written when started with --detach`);
138
+ const args = follow ? ['-n', '200', '-f', path] : ['-n', '200', path];
139
+ const child = spawn('tail', args, { stdio: ['ignore', 'inherit', 'inherit'] });
140
+ return new Promise((resolve) => {
141
+ child.on('exit', (code) => {
142
+ resolve(code ?? 0);
143
+ });
144
+ });
145
+ }
146
+ export async function probe(url) {
147
+ try {
148
+ const res = await fetch(`${url}/health`, {
149
+ signal: AbortSignal.timeout(3_000),
150
+ });
151
+ if (!res.ok)
152
+ return null;
153
+ return (await res.json());
154
+ }
155
+ catch {
156
+ return null;
157
+ }
158
+ }