@stage-labs/metro 0.1.0-beta.19 → 0.1.0-beta.20

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 (210) hide show
  1. package/dist/api.js +9 -0
  2. package/dist/cli.js +75 -4
  3. package/dist/control.js +102 -0
  4. package/dist/runtime.js +94 -0
  5. package/package.json +27 -5
  6. package/runtime/node_modules/@metro-labs/discord/package.json +28 -0
  7. package/runtime/node_modules/@metro-labs/discord/src/accounts.ts +119 -0
  8. package/runtime/node_modules/@metro-labs/discord/src/actions.ts +388 -0
  9. package/runtime/node_modules/@metro-labs/discord/src/api-base.ts +1 -0
  10. package/runtime/node_modules/@metro-labs/discord/src/attachments.ts +29 -0
  11. package/runtime/node_modules/@metro-labs/discord/src/format.ts +210 -0
  12. package/runtime/node_modules/@metro-labs/discord/src/group-actions.ts +50 -0
  13. package/runtime/node_modules/@metro-labs/discord/src/group.ts +115 -0
  14. package/runtime/node_modules/@metro-labs/discord/src/index.ts +127 -0
  15. package/runtime/node_modules/@metro-labs/discord/src/members.ts +215 -0
  16. package/runtime/node_modules/@metro-labs/discord/src/send-files.ts +43 -0
  17. package/runtime/node_modules/@metro-labs/discord/src/station.ts +19 -0
  18. package/runtime/node_modules/@metro-labs/discord/src/verify.ts +82 -0
  19. package/runtime/node_modules/@metro-labs/discord/src/voice-speak.ts +195 -0
  20. package/runtime/node_modules/@metro-labs/discord/src/voice-transcribe.ts +292 -0
  21. package/runtime/node_modules/@metro-labs/discord/src/voice.ts +197 -0
  22. package/runtime/node_modules/@metro-labs/discord/src/wire.ts +5 -0
  23. package/runtime/node_modules/@metro-labs/mcp/package.json +66 -0
  24. package/runtime/node_modules/@metro-labs/mcp/src/channels/dedupe.ts +24 -0
  25. package/runtime/node_modules/@metro-labs/mcp/src/channels/inbound.ts +367 -0
  26. package/runtime/node_modules/@metro-labs/mcp/src/channels/media-note.ts +85 -0
  27. package/runtime/node_modules/@metro-labs/mcp/src/channels/pending.ts +75 -0
  28. package/runtime/node_modules/@metro-labs/mcp/src/channels/relay.ts +129 -0
  29. package/runtime/node_modules/@metro-labs/mcp/src/channels/webhook-note.ts +51 -0
  30. package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +311 -0
  31. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +335 -0
  32. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-error.ts +8 -0
  33. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +129 -0
  34. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +99 -0
  35. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-owner.ts +32 -0
  36. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +124 -0
  37. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +278 -0
  38. package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +322 -0
  39. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +111 -0
  40. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair.ts +20 -0
  41. package/runtime/node_modules/@metro-labs/mcp/src/daemon/collection-api.ts +202 -0
  42. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +338 -0
  43. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-json.ts +31 -0
  44. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth-routes.ts +157 -0
  45. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +127 -0
  46. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +100 -0
  47. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +57 -0
  48. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +333 -0
  49. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cors.ts +29 -0
  50. package/runtime/node_modules/@metro-labs/mcp/src/daemon/crash-guard.ts +49 -0
  51. package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +117 -0
  52. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-auth.ts +164 -0
  53. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-oauth.ts +208 -0
  54. package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +357 -0
  55. package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +79 -0
  56. package/runtime/node_modules/@metro-labs/mcp/src/daemon/log.ts +42 -0
  57. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +162 -0
  58. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +120 -0
  59. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +65 -0
  60. package/runtime/node_modules/@metro-labs/mcp/src/daemon/pair-codes.ts +58 -0
  61. package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +134 -0
  62. package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +214 -0
  63. package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +224 -0
  64. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +125 -0
  65. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-pair.ts +18 -0
  66. package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-source.ts +105 -0
  67. package/runtime/node_modules/@metro-labs/mcp/src/daemon/secure-fs.ts +57 -0
  68. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +27 -0
  69. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +41 -0
  70. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +218 -0
  71. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor-io.ts +97 -0
  72. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor.ts +323 -0
  73. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-call.ts +22 -0
  74. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-error.ts +53 -0
  75. package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +169 -0
  76. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +295 -0
  77. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-store.ts +181 -0
  78. package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +91 -0
  79. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +215 -0
  80. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-map.ts +71 -0
  81. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-scope.ts +64 -0
  82. package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +37 -0
  83. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-collections.ts +218 -0
  84. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +147 -0
  85. package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +329 -0
  86. package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +19 -0
  87. package/runtime/node_modules/@metro-labs/mcp/src/db/key-map.ts +45 -0
  88. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +340 -0
  89. package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +300 -0
  90. package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +28 -0
  91. package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +105 -0
  92. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +131 -0
  93. package/runtime/node_modules/@metro-labs/mcp/src/db/token-carry.ts +45 -0
  94. package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +57 -0
  95. package/runtime/node_modules/@metro-labs/mcp/src/db/whatsapp-login.ts +18 -0
  96. package/runtime/node_modules/@metro-labs/mcp/src/mcp/accounts.ts +115 -0
  97. package/runtime/node_modules/@metro-labs/mcp/src/mcp/call-tools.ts +203 -0
  98. package/runtime/node_modules/@metro-labs/mcp/src/mcp/channel-owner.ts +29 -0
  99. package/runtime/node_modules/@metro-labs/mcp/src/mcp/ctx.ts +62 -0
  100. package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +91 -0
  101. package/runtime/node_modules/@metro-labs/mcp/src/mcp/frame-scope.ts +21 -0
  102. package/runtime/node_modules/@metro-labs/mcp/src/mcp/group-tools.ts +129 -0
  103. package/runtime/node_modules/@metro-labs/mcp/src/mcp/index.ts +192 -0
  104. package/runtime/node_modules/@metro-labs/mcp/src/mcp/member-tools.ts +72 -0
  105. package/runtime/node_modules/@metro-labs/mcp/src/mcp/permission-relay.ts +66 -0
  106. package/runtime/node_modules/@metro-labs/mcp/src/mcp/raw-get-stream.ts +207 -0
  107. package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +73 -0
  108. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-registry.ts +172 -0
  109. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +43 -0
  110. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session.ts +211 -0
  111. package/runtime/node_modules/@metro-labs/mcp/src/mcp/str.ts +5 -0
  112. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-dispatch.ts +212 -0
  113. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +397 -0
  114. package/runtime/node_modules/@metro-labs/mcp/src/mcp/upload-tool.ts +66 -0
  115. package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +240 -0
  116. package/runtime/node_modules/@metro-labs/mcp/src/server.ts +2 -0
  117. package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +80 -0
  118. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-inline.ts +100 -0
  119. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +161 -0
  120. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +200 -0
  121. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-xmtp.ts +111 -0
  122. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +182 -0
  123. package/runtime/node_modules/@metro-labs/mcp/src/stations/attachments.ts +196 -0
  124. package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +82 -0
  125. package/runtime/node_modules/@metro-labs/mcp/src/stations/messaging-normalize.ts +88 -0
  126. package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +35 -0
  127. package/runtime/node_modules/@metro-labs/mcp/src/stations/station-runtime.ts +64 -0
  128. package/runtime/node_modules/@metro-labs/mcp/src/stations/types.ts +107 -0
  129. package/runtime/node_modules/@metro-labs/telegram/package.json +25 -0
  130. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +120 -0
  131. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +295 -0
  132. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +128 -0
  133. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +172 -0
  134. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +103 -0
  135. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +166 -0
  136. package/runtime/node_modules/@metro-labs/telegram/src/members.ts +47 -0
  137. package/runtime/node_modules/@metro-labs/telegram/src/station.ts +17 -0
  138. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +48 -0
  139. package/runtime/node_modules/@metro-labs/telegram/src/verify.ts +38 -0
  140. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +3 -0
  141. package/runtime/node_modules/@metro-labs/telegram-user/package.json +28 -0
  142. package/runtime/node_modules/@metro-labs/telegram-user/src/accounts.ts +81 -0
  143. package/runtime/node_modules/@metro-labs/telegram-user/src/actions.ts +254 -0
  144. package/runtime/node_modules/@metro-labs/telegram-user/src/attachments.ts +87 -0
  145. package/runtime/node_modules/@metro-labs/telegram-user/src/client.ts +77 -0
  146. package/runtime/node_modules/@metro-labs/telegram-user/src/format.ts +141 -0
  147. package/runtime/node_modules/@metro-labs/telegram-user/src/group.ts +261 -0
  148. package/runtime/node_modules/@metro-labs/telegram-user/src/history.ts +52 -0
  149. package/runtime/node_modules/@metro-labs/telegram-user/src/inbound.ts +78 -0
  150. package/runtime/node_modules/@metro-labs/telegram-user/src/index.ts +46 -0
  151. package/runtime/node_modules/@metro-labs/telegram-user/src/login.ts +166 -0
  152. package/runtime/node_modules/@metro-labs/telegram-user/src/media-actions.ts +80 -0
  153. package/runtime/node_modules/@metro-labs/telegram-user/src/members.ts +75 -0
  154. package/runtime/node_modules/@metro-labs/telegram-user/src/normalize.ts +22 -0
  155. package/runtime/node_modules/@metro-labs/telegram-user/src/reactions.ts +167 -0
  156. package/runtime/node_modules/@metro-labs/telegram-user/src/station.ts +24 -0
  157. package/runtime/node_modules/@metro-labs/telegram-user/src/types.ts +7 -0
  158. package/runtime/node_modules/@metro-labs/telegram-user/src/wire.ts +1 -0
  159. package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
  160. package/runtime/node_modules/@metro-labs/webhook/src/station.ts +40 -0
  161. package/runtime/node_modules/@metro-labs/whatsapp/package.json +29 -0
  162. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +67 -0
  163. package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
  164. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +178 -0
  165. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
  166. package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
  167. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +318 -0
  168. package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
  169. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +132 -0
  170. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
  171. package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
  172. package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
  173. package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
  174. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
  175. package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
  176. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
  177. package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
  178. package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +141 -0
  179. package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +17 -0
  180. package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
  181. package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
  182. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
  183. package/runtime/node_modules/@metro-labs/xmtp/package.json +33 -0
  184. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +113 -0
  185. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
  186. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
  187. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
  188. package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
  189. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +375 -0
  190. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
  191. package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
  192. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
  193. package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
  194. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +333 -0
  195. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +78 -0
  196. package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
  197. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
  198. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +33 -0
  199. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
  200. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
  201. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
  202. package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
  203. package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +13 -0
  204. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
  205. package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
  206. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
  207. package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +84 -0
  208. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +39 -0
  209. package/runtime/runtime.json +3 -0
  210. package/runtime/trains/.keep +0 -0
package/dist/api.js CHANGED
@@ -78,6 +78,15 @@ export async function claimCode(code) {
78
78
  collection: body.collection,
79
79
  };
80
80
  }
81
+ export async function claimRuntime(code, label) {
82
+ const body = await post('/api/run/claim', { code, label });
83
+ if (!isRecord(body) ||
84
+ typeof body.token !== 'string' ||
85
+ typeof body.agent !== 'string' ||
86
+ typeof body.label !== 'string')
87
+ throw new Error('metro returned an unexpected response');
88
+ return { token: body.token, agent: body.agent, label: body.label };
89
+ }
81
90
  export async function mcpServers() {
82
91
  const body = await get('/api/cli/mcp');
83
92
  if (!isRecord(body) || typeof body.json !== 'string')
package/dist/cli.js CHANGED
@@ -1,11 +1,22 @@
1
1
  #!/usr/bin/env node
2
- import { claimCode, mcpServers, NotSignedIn, whoisAuthorized, } from './api.js';
2
+ import { hostname } from 'node:os';
3
+ import { claimCode, claimRuntime, mcpServers, NotSignedIn, whoisAuthorized, } from './api.js';
3
4
  import { askLine } from './prompt.js';
4
5
  import { clearToken, credentialsPath, metroUrl, metroWebUrl, writeToken, } from './store.js';
6
+ import { detach, probe, runningPid, stop, tail } from './control.js';
7
+ import { assertAgentId, daemonPlan, localUrl, readRunToken, runDaemon, runtimeDir, writeRunToken, } from './runtime.js';
5
8
  import { update } from './update.js';
6
9
  import { currentVersion } from './version.js';
7
10
  const USAGE = `metro — the command line for your MCP connectors
8
11
 
12
+ metro start <agent-id> [--detach]
13
+ run that agent's stations on this machine
14
+ metro stop <agent-id>
15
+ stop the daemon started with --detach
16
+ metro status <agent-id>
17
+ is it running, and is it healthy
18
+ metro logs <agent-id> [-f]
19
+ show the detached daemon's log
9
20
  metro login authorize a connector collection with a code from the web UI
10
21
  metro logout forget this machine's sign-in
11
22
  metro whoami print the account and collection this machine may read
@@ -17,10 +28,66 @@ Start Claude Code with all of them, without writing them to disk:
17
28
 
18
29
  claude --mcp-config <(metro mcp)
19
30
 
20
- METRO_URL the metro to talk to (default https://mcp.metro.box)
21
- METRO_TOKEN use this session instead of the stored one
22
- METRO_UI_URL where the web UI lives (default https://metro.box)
31
+ METRO_URL the metro to talk to (default https://mcp.metro.box)
32
+ METRO_UI_URL where the web UI lives (default https://metro.box)
33
+ METRO_TOKEN use this connector sign-in instead of the stored one
34
+ METRO_RUN_TOKEN use this runtime authorization instead of the stored one
35
+ METRO_RUNTIME_DIR run the daemon from this directory instead of the bundled one
23
36
  `;
37
+ function hostLabel() {
38
+ const name = hostname().trim();
39
+ return name === '' ? 'unnamed machine' : name;
40
+ }
41
+ async function authorizeRuntime(agentId) {
42
+ process.stderr.write(`Authorize this machine at ${metroWebUrl()}/#/agent/${agentId}\n`);
43
+ const code = (await askLine('Paste the code: ')).trim();
44
+ if (code === '')
45
+ throw new Error('no code given');
46
+ const claimed = await claimRuntime(code, hostLabel());
47
+ writeRunToken(claimed.agent, claimed.token);
48
+ process.stderr.write(`Authorized as '${claimed.label}'.\n`);
49
+ return claimed.token;
50
+ }
51
+ async function start(argv) {
52
+ const agentId = assertAgentId(argv[0]);
53
+ if (runningPid(agentId) !== null)
54
+ throw new Error(`metro is already running for ${agentId} — stop it first`);
55
+ const dir = runtimeDir();
56
+ const token = readRunToken(agentId) ?? (await authorizeRuntime(agentId));
57
+ const detached = argv.includes('--detach');
58
+ const plan = daemonPlan({ agentId, token, dir });
59
+ if (!detached)
60
+ return runDaemon(agentId, plan);
61
+ const pid = detach({ agentId, ...plan });
62
+ process.stderr.write(`metro is running for ${agentId} (pid ${String(pid)})\n` +
63
+ ` logs: metro logs ${agentId} -f\n` +
64
+ ` stop: metro stop ${agentId}\n`);
65
+ return 0;
66
+ }
67
+ async function stopDaemon(argv) {
68
+ const agentId = assertAgentId(argv[0]);
69
+ const stopped = await stop(agentId);
70
+ process.stderr.write(stopped ? `Stopped metro for ${agentId}\n` : `metro is not running for ${agentId}\n`);
71
+ return stopped ? 0 : 1;
72
+ }
73
+ async function status(argv) {
74
+ const agentId = assertAgentId(argv[0]);
75
+ const pid = runningPid(agentId);
76
+ if (pid === null) {
77
+ process.stdout.write(`${agentId}: not running\n`);
78
+ return 1;
79
+ }
80
+ const health = await probe(localUrl());
81
+ process.stdout.write(`${agentId}: running (pid ${String(pid)}) · ` +
82
+ (health === null
83
+ ? `not answering on ${localUrl()}\n`
84
+ : `healthy, up ${String(health.uptime)}s\n`));
85
+ return health === null ? 1 : 0;
86
+ }
87
+ function logs(argv) {
88
+ const agentId = assertAgentId(argv[0]);
89
+ return tail(agentId, argv.includes('-f') || argv.includes('--follow'));
90
+ }
24
91
  async function login() {
25
92
  process.stderr.write(`Choose a connector collection at ${metroWebUrl()}/#/authorize\n`);
26
93
  const { token, email, collection } = await claimCode(await askLine('Paste the code here: '));
@@ -46,6 +113,10 @@ const COMMANDS = {
46
113
  await whoami();
47
114
  return 0;
48
115
  },
116
+ start: () => start(process.argv.slice(3)),
117
+ stop: () => stopDaemon(process.argv.slice(3)),
118
+ status: () => status(process.argv.slice(3)),
119
+ logs: () => logs(process.argv.slice(3)),
49
120
  mcp: async () => {
50
121
  process.stdout.write(`${await mcpServers()}\n`);
51
122
  return 0;
@@ -0,0 +1,102 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync, mkdirSync, openSync, 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
+ export async function stop(agentId, waitMs = 10_000) {
62
+ const pid = runningPid(agentId);
63
+ if (pid === null)
64
+ return false;
65
+ process.kill(pid, 'SIGTERM');
66
+ const until = Date.now() + waitMs;
67
+ while (Date.now() < until) {
68
+ if (!isAlive(pid)) {
69
+ clearPid(agentId);
70
+ return true;
71
+ }
72
+ await new Promise((r) => setTimeout(r, 100));
73
+ }
74
+ process.kill(pid, 'SIGKILL');
75
+ clearPid(agentId);
76
+ return true;
77
+ }
78
+ export function tail(agentId, follow) {
79
+ const path = logPath(agentId);
80
+ if (!existsSync(path))
81
+ throw new Error(`no log for ${agentId} yet — it is only written when started with --detach`);
82
+ const args = follow ? ['-n', '200', '-f', path] : ['-n', '200', path];
83
+ const child = spawn('tail', args, { stdio: ['ignore', 'inherit', 'inherit'] });
84
+ return new Promise((resolve) => {
85
+ child.on('exit', (code) => {
86
+ resolve(code ?? 0);
87
+ });
88
+ });
89
+ }
90
+ export async function probe(url) {
91
+ try {
92
+ const res = await fetch(`${url}/health`, {
93
+ signal: AbortSignal.timeout(3_000),
94
+ });
95
+ if (!res.ok)
96
+ return null;
97
+ return (await res.json());
98
+ }
99
+ catch {
100
+ return null;
101
+ }
102
+ }
@@ -0,0 +1,94 @@
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ import { mkdirSync, readFileSync, writeFileSync, chmodSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { existsSync } from 'node:fs';
6
+ import { homedir } from 'node:os';
7
+ import { metroUrl } from './store.js';
8
+ const AGENT_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{10}$/;
9
+ const SERVER_ENTRY = join('node_modules', '@metro-labs', 'mcp', 'src', 'server.ts');
10
+ export class MissingRuntime extends Error {
11
+ }
12
+ export function runtimeDir() {
13
+ const explicit = process.env.METRO_RUNTIME_DIR?.trim();
14
+ const dir = explicit !== undefined && explicit !== ''
15
+ ? explicit
16
+ : join(dirname(dirname(fileURLToPath(import.meta.url))), 'runtime');
17
+ if (!existsSync(join(dir, SERVER_ENTRY)))
18
+ throw new MissingRuntime(`no metro daemon at ${dir}. Reinstall with: npm i -g @stage-labs/metro@beta`);
19
+ return dir;
20
+ }
21
+ export class MissingBun extends Error {
22
+ }
23
+ export function assertAgentId(id) {
24
+ if (id === undefined || !AGENT_RE.test(id))
25
+ throw new Error(`'${id ?? ''}' is not an agent id — copy it from the agent's page in the web UI`);
26
+ return id;
27
+ }
28
+ export function findBun() {
29
+ const found = spawnSync('bun', ['--version'], { stdio: 'ignore' });
30
+ if (found.error !== undefined || found.status !== 0)
31
+ throw new MissingBun('metro start needs Bun, which is not on PATH.\n' +
32
+ 'Install it with: curl -fsSL https://bun.sh/install | bash');
33
+ return 'bun';
34
+ }
35
+ function tokenPath(agentId) {
36
+ const xdg = process.env.XDG_CONFIG_HOME?.trim();
37
+ const base = xdg === undefined || xdg === '' ? join(homedir(), '.config') : xdg;
38
+ return join(base, 'metro', `runtime-${agentId}.json`);
39
+ }
40
+ export function readRunToken(agentId) {
41
+ const fromEnv = process.env.METRO_RUN_TOKEN?.trim();
42
+ if (fromEnv !== undefined && fromEnv !== '')
43
+ return fromEnv;
44
+ try {
45
+ const parsed = JSON.parse(readFileSync(tokenPath(agentId), 'utf8'));
46
+ if (typeof parsed.token !== 'string' || parsed.token === '')
47
+ return null;
48
+ return parsed.url === metroUrl() ? parsed.token : null;
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ export function writeRunToken(agentId, token) {
55
+ const path = tokenPath(agentId);
56
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
57
+ writeFileSync(path, JSON.stringify({ token, url: metroUrl() }, null, 2));
58
+ chmodSync(path, 0o600);
59
+ }
60
+ export const localPort = () => Number(process.env.METRO_WEBHOOK_PORT) || 8420;
61
+ export const localUrl = () => `http://127.0.0.1:${String(localPort())}`;
62
+ export function daemonPlan(opts) {
63
+ return {
64
+ command: findBun(),
65
+ args: [SERVER_ENTRY],
66
+ cwd: opts.dir,
67
+ env: {
68
+ ...process.env,
69
+ METRO_RUN_TOKEN: opts.token,
70
+ METRO_AGENT: opts.agentId,
71
+ METRO_URL: metroUrl(),
72
+ METRO_HTTP_HOST: process.env.METRO_HTTP_HOST ?? '127.0.0.1',
73
+ METRO_TRAINS_DIR: join(opts.dir, 'trains'),
74
+ },
75
+ };
76
+ }
77
+ export function runDaemon(agentId, plan) {
78
+ process.stderr.write(`Starting metro for ${agentId}\n`);
79
+ const child = spawn(plan.command, plan.args, {
80
+ cwd: plan.cwd,
81
+ env: plan.env,
82
+ stdio: ['ignore', 'inherit', 'inherit'],
83
+ });
84
+ const relay = (sig) => () => {
85
+ child.kill(sig);
86
+ };
87
+ process.on('SIGINT', relay('SIGINT'));
88
+ process.on('SIGTERM', relay('SIGTERM'));
89
+ return new Promise((resolve) => {
90
+ child.on('exit', (code) => {
91
+ resolve(code ?? 0);
92
+ });
93
+ });
94
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage-labs/metro",
3
- "version": "0.1.0-beta.19",
3
+ "version": "0.1.0-beta.20",
4
4
  "description": "The metro command line. Sign in once per machine, then hand your MCP connector list to Claude Code without the credentials touching disk, argv or shell history.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -17,16 +17,17 @@
17
17
  "metro": "dist/cli.js"
18
18
  },
19
19
  "files": [
20
- "dist"
20
+ "dist",
21
+ "runtime"
21
22
  ],
22
23
  "publishConfig": {
23
24
  "access": "public",
24
25
  "tag": "beta"
25
26
  },
26
27
  "scripts": {
27
- "build": "tsc",
28
- "prepublishOnly": "tsc",
29
- "test": "tsc --noEmit && bun test test/"
28
+ "build": "tsc && node scripts/stage-runtime.mjs",
29
+ "prepublishOnly": "tsc && node scripts/stage-runtime.mjs",
30
+ "test": "tsc --noEmit && node scripts/stage-runtime.mjs && bun test test/"
30
31
  },
31
32
  "devDependencies": {
32
33
  "@stage-labs/config": "0.1.0-beta.2",
@@ -34,5 +35,26 @@
34
35
  "knip": "^6.15.0",
35
36
  "madge": "^8.0.0",
36
37
  "typescript": "^5"
38
+ },
39
+ "dependencies": {
40
+ "@discordjs/voice": "^0.19.2",
41
+ "@modelcontextprotocol/sdk": "^1.12.0",
42
+ "@mtcute/bun": "0.30.1",
43
+ "@xmtp/content-type-primitives": "^3.0.1",
44
+ "@xmtp/content-type-reaction": "^2.0.2",
45
+ "@xmtp/content-type-remote-attachment": "^2.0.4",
46
+ "@xmtp/content-type-reply": "^2.0.3",
47
+ "@xmtp/content-type-wallet-send-calls": "^2.0.0",
48
+ "@xmtp/node-bindings": "1.9.1",
49
+ "@xmtp/node-sdk": "^6.0.0",
50
+ "baileys": "7.0.0-rc14",
51
+ "discord.js": "^14.26.4",
52
+ "drizzle-orm": "^0.44.0",
53
+ "pino": "^9.5.0",
54
+ "pino-pretty": "^13.1.3",
55
+ "postgres": "^3.4.5",
56
+ "prism-media": "^1.3.5",
57
+ "viem": "^2.52.2",
58
+ "zod": "^3.23.8"
37
59
  }
38
60
  }
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@metro-labs/discord",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/station.ts",
8
+ "./train": "./src/index.ts",
9
+ "./verify": "./src/verify.ts"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "test": "tsc --noEmit && bun test test/"
14
+ },
15
+ "dependencies": {
16
+ "@metro-labs/mcp": "workspace:*",
17
+ "@discordjs/voice": "^0.19.2",
18
+ "discord.js": "^14.26.4",
19
+ "prism-media": "^1.3.5"
20
+ },
21
+ "devDependencies": {
22
+ "@stage-labs/config": "0.1.0-beta.2",
23
+ "eslint": "^10.3.0",
24
+ "knip": "^6.15.0",
25
+ "madge": "^8.0.0",
26
+ "typescript": "^5"
27
+ }
28
+ }
@@ -0,0 +1,119 @@
1
+ import { Client } from 'discord.js';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import {
5
+ makeAccountStore,
6
+ resolveAccountId,
7
+ } from '@metro-labs/mcp/stations/account-store';
8
+ import { Line } from '@metro-labs/mcp/lines';
9
+ import { emit } from './wire.js';
10
+ import { API } from './api-base.js';
11
+
12
+ const ACCOUNTS_FILE =
13
+ process.env.DISCORD_ACCOUNTS_FILE ??
14
+ join(homedir(), '.metro', 'discord-accounts.json');
15
+
16
+ export interface AccountConfig {
17
+ id: string;
18
+ token: string;
19
+ owner?: string;
20
+ }
21
+
22
+ export const { loadAccounts } = makeAccountStore<AccountConfig>({
23
+ prefix: 'discord',
24
+ file: ACCOUNTS_FILE,
25
+ allowlistEnv: ['DISCORD_ONLY_ACCOUNTS', 'DISCORD_ACCOUNTS'],
26
+ validate(raw, die) {
27
+ const seen = new Set<string>();
28
+ for (const a of raw) {
29
+ if (!a.id) die('account missing id');
30
+ if (!a.token || typeof a.token !== 'string')
31
+ die(`account '${a.id}' missing token`);
32
+ if (seen.has(a.id)) die(`duplicate account id '${a.id}'`);
33
+ seen.add(a.id);
34
+ }
35
+ },
36
+ });
37
+
38
+ export interface Account {
39
+ cfg: AccountConfig;
40
+ client: Client;
41
+ }
42
+ export const accounts = new Map<string, Account>();
43
+
44
+ function restBody(
45
+ body: unknown,
46
+ isForm: boolean,
47
+ ): RequestInit['body'] | undefined {
48
+ if (body === undefined) return undefined;
49
+ return isForm ? (body as RequestInit['body']) : JSON.stringify(body);
50
+ }
51
+
52
+ async function restResult<T>(res: Response): Promise<T> {
53
+ if (res.status === 204) return undefined as T;
54
+ const ctype = res.headers.get('content-type') ?? '';
55
+ if (ctype.includes('application/json')) return res.json() as Promise<T>;
56
+ return res.arrayBuffer().then((b) => Buffer.from(b) as unknown as T);
57
+ }
58
+
59
+ export async function rest<T = unknown>(
60
+ accountId: string,
61
+ method: string,
62
+ path: string,
63
+ body?: unknown,
64
+ isForm = false,
65
+ ): Promise<T> {
66
+ const acct = accounts.get(accountId);
67
+ if (!acct) throw new Error(`unknown account '${accountId}'`);
68
+ const headers: Record<string, string> = {
69
+ Authorization: `Bot ${acct.cfg.token}`,
70
+ 'User-Agent': 'metro-discord-train (https://github.com/bonustrack/stage)',
71
+ };
72
+ if (body !== undefined && !isForm)
73
+ headers['Content-Type'] = 'application/json';
74
+ emit({ op: 'log', text: `discord[${accountId}] api ${method} ${path}` });
75
+ const res = await fetch(`${API}${path}`, {
76
+ method,
77
+ headers,
78
+ body: restBody(body, isForm),
79
+ signal: AbortSignal.timeout(30_000),
80
+ });
81
+ emit({
82
+ op: 'log',
83
+ text: `discord[${accountId}] api ${method} ${path} -> ${res.status}`,
84
+ });
85
+ if (!res.ok) {
86
+ const text = await res.text().catch(() => '');
87
+ throw new Error(`discord ${method} ${path}: ${res.status} ${text}`);
88
+ }
89
+ return restResult<T>(res);
90
+ }
91
+
92
+ export function lineOf(accountId: string, channelId: string): string {
93
+ return `metro://discord/${accountId}/${channelId}`;
94
+ }
95
+
96
+ export function parseLine(
97
+ line: string,
98
+ ): { accountId: string; channelId: string } | null {
99
+ const p = Line.parseDiscord(line);
100
+ return p ? { accountId: p.accountId, channelId: p.resource } : null;
101
+ }
102
+
103
+ export function accountFor(args: { account?: string; line?: string }): string {
104
+ return resolveAccountId(accounts, args, (line) => parseLine(line)?.accountId);
105
+ }
106
+
107
+ export function routeOf(
108
+ line: string,
109
+ account?: string,
110
+ ): { accountId: string; channelId: string } {
111
+ const parsed = parseLine(line);
112
+ if (!parsed) throw new Error(`bad discord line: ${line}`);
113
+ const accountId = account ?? parsed.accountId;
114
+ if (!accounts.has(accountId))
115
+ throw new Error(`unknown account '${accountId}' in line ${line}`);
116
+ return { accountId, channelId: parsed.channelId };
117
+ }
118
+
119
+ export const encodeEmoji = (e: string): string => encodeURIComponent(e);