@stage-labs/metro 0.1.0-beta.2 → 0.1.0-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/dist/api.js +103 -0
  2. package/dist/cli.js +134 -565
  3. package/dist/control.js +102 -0
  4. package/dist/prompt.js +10 -0
  5. package/dist/runtime.js +94 -0
  6. package/dist/store.js +70 -0
  7. package/dist/update.js +33 -0
  8. package/dist/version.js +97 -0
  9. package/package.json +34 -22
  10. package/runtime/node_modules/@metro-labs/discord/package.json +28 -0
  11. package/runtime/node_modules/@metro-labs/discord/src/accounts.ts +119 -0
  12. package/runtime/node_modules/@metro-labs/discord/src/actions.ts +388 -0
  13. package/runtime/node_modules/@metro-labs/discord/src/api-base.ts +1 -0
  14. package/runtime/node_modules/@metro-labs/discord/src/attachments.ts +29 -0
  15. package/runtime/node_modules/@metro-labs/discord/src/format.ts +210 -0
  16. package/runtime/node_modules/@metro-labs/discord/src/group-actions.ts +50 -0
  17. package/runtime/node_modules/@metro-labs/discord/src/group.ts +115 -0
  18. package/runtime/node_modules/@metro-labs/discord/src/index.ts +127 -0
  19. package/runtime/node_modules/@metro-labs/discord/src/members.ts +215 -0
  20. package/runtime/node_modules/@metro-labs/discord/src/send-files.ts +43 -0
  21. package/runtime/node_modules/@metro-labs/discord/src/station.ts +19 -0
  22. package/runtime/node_modules/@metro-labs/discord/src/verify.ts +82 -0
  23. package/runtime/node_modules/@metro-labs/discord/src/voice-speak.ts +195 -0
  24. package/runtime/node_modules/@metro-labs/discord/src/voice-transcribe.ts +292 -0
  25. package/runtime/node_modules/@metro-labs/discord/src/voice.ts +197 -0
  26. package/runtime/node_modules/@metro-labs/discord/src/wire.ts +5 -0
  27. package/runtime/node_modules/@metro-labs/mcp/package.json +66 -0
  28. package/runtime/node_modules/@metro-labs/mcp/src/channels/dedupe.ts +24 -0
  29. package/runtime/node_modules/@metro-labs/mcp/src/channels/inbound.ts +367 -0
  30. package/runtime/node_modules/@metro-labs/mcp/src/channels/media-note.ts +85 -0
  31. package/runtime/node_modules/@metro-labs/mcp/src/channels/pending.ts +75 -0
  32. package/runtime/node_modules/@metro-labs/mcp/src/channels/relay.ts +129 -0
  33. package/runtime/node_modules/@metro-labs/mcp/src/channels/webhook-note.ts +51 -0
  34. package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +311 -0
  35. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +335 -0
  36. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-error.ts +8 -0
  37. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +129 -0
  38. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +99 -0
  39. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-owner.ts +32 -0
  40. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +124 -0
  41. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +278 -0
  42. package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +322 -0
  43. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +111 -0
  44. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair.ts +20 -0
  45. package/runtime/node_modules/@metro-labs/mcp/src/daemon/collection-api.ts +202 -0
  46. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +338 -0
  47. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-json.ts +31 -0
  48. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth-routes.ts +157 -0
  49. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +127 -0
  50. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +100 -0
  51. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +57 -0
  52. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +333 -0
  53. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cors.ts +29 -0
  54. package/runtime/node_modules/@metro-labs/mcp/src/daemon/crash-guard.ts +49 -0
  55. package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +117 -0
  56. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-auth.ts +164 -0
  57. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-oauth.ts +208 -0
  58. package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +357 -0
  59. package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +79 -0
  60. package/runtime/node_modules/@metro-labs/mcp/src/daemon/log.ts +42 -0
  61. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +162 -0
  62. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +120 -0
  63. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +65 -0
  64. package/runtime/node_modules/@metro-labs/mcp/src/daemon/pair-codes.ts +58 -0
  65. package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +134 -0
  66. package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +214 -0
  67. package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +224 -0
  68. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +125 -0
  69. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-pair.ts +18 -0
  70. package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-source.ts +105 -0
  71. package/runtime/node_modules/@metro-labs/mcp/src/daemon/secure-fs.ts +57 -0
  72. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +27 -0
  73. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +41 -0
  74. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +218 -0
  75. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor-io.ts +97 -0
  76. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor.ts +323 -0
  77. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-call.ts +22 -0
  78. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-error.ts +53 -0
  79. package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +169 -0
  80. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +295 -0
  81. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-store.ts +181 -0
  82. package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +91 -0
  83. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +215 -0
  84. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-map.ts +71 -0
  85. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-scope.ts +64 -0
  86. package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +37 -0
  87. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-collections.ts +218 -0
  88. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +147 -0
  89. package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +329 -0
  90. package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +19 -0
  91. package/runtime/node_modules/@metro-labs/mcp/src/db/key-map.ts +45 -0
  92. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +364 -0
  93. package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +300 -0
  94. package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +31 -0
  95. package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +105 -0
  96. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +131 -0
  97. package/runtime/node_modules/@metro-labs/mcp/src/db/token-carry.ts +45 -0
  98. package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +57 -0
  99. package/runtime/node_modules/@metro-labs/mcp/src/db/whatsapp-login.ts +18 -0
  100. package/runtime/node_modules/@metro-labs/mcp/src/mcp/accounts.ts +145 -0
  101. package/runtime/node_modules/@metro-labs/mcp/src/mcp/call-tools.ts +203 -0
  102. package/runtime/node_modules/@metro-labs/mcp/src/mcp/channel-owner.ts +29 -0
  103. package/runtime/node_modules/@metro-labs/mcp/src/mcp/ctx.ts +62 -0
  104. package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +91 -0
  105. package/runtime/node_modules/@metro-labs/mcp/src/mcp/frame-scope.ts +21 -0
  106. package/runtime/node_modules/@metro-labs/mcp/src/mcp/group-tools.ts +129 -0
  107. package/runtime/node_modules/@metro-labs/mcp/src/mcp/index.ts +192 -0
  108. package/runtime/node_modules/@metro-labs/mcp/src/mcp/member-tools.ts +72 -0
  109. package/runtime/node_modules/@metro-labs/mcp/src/mcp/permission-relay.ts +66 -0
  110. package/runtime/node_modules/@metro-labs/mcp/src/mcp/raw-get-stream.ts +207 -0
  111. package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +73 -0
  112. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-registry.ts +172 -0
  113. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +43 -0
  114. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session.ts +211 -0
  115. package/runtime/node_modules/@metro-labs/mcp/src/mcp/str.ts +5 -0
  116. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-dispatch.ts +212 -0
  117. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +397 -0
  118. package/runtime/node_modules/@metro-labs/mcp/src/mcp/upload-tool.ts +66 -0
  119. package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +240 -0
  120. package/runtime/node_modules/@metro-labs/mcp/src/server.ts +2 -0
  121. package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +80 -0
  122. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-inline.ts +100 -0
  123. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +161 -0
  124. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +200 -0
  125. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-xmtp.ts +111 -0
  126. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +182 -0
  127. package/runtime/node_modules/@metro-labs/mcp/src/stations/attachments.ts +196 -0
  128. package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +82 -0
  129. package/runtime/node_modules/@metro-labs/mcp/src/stations/messaging-normalize.ts +88 -0
  130. package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +35 -0
  131. package/runtime/node_modules/@metro-labs/mcp/src/stations/station-runtime.ts +64 -0
  132. package/runtime/node_modules/@metro-labs/mcp/src/stations/types.ts +107 -0
  133. package/runtime/node_modules/@metro-labs/telegram/package.json +25 -0
  134. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +120 -0
  135. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +295 -0
  136. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +128 -0
  137. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +172 -0
  138. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +103 -0
  139. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +166 -0
  140. package/runtime/node_modules/@metro-labs/telegram/src/members.ts +47 -0
  141. package/runtime/node_modules/@metro-labs/telegram/src/station.ts +17 -0
  142. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +48 -0
  143. package/runtime/node_modules/@metro-labs/telegram/src/verify.ts +38 -0
  144. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +3 -0
  145. package/runtime/node_modules/@metro-labs/telegram-user/package.json +28 -0
  146. package/runtime/node_modules/@metro-labs/telegram-user/src/accounts.ts +81 -0
  147. package/runtime/node_modules/@metro-labs/telegram-user/src/actions.ts +254 -0
  148. package/runtime/node_modules/@metro-labs/telegram-user/src/attachments.ts +87 -0
  149. package/runtime/node_modules/@metro-labs/telegram-user/src/client.ts +77 -0
  150. package/runtime/node_modules/@metro-labs/telegram-user/src/format.ts +141 -0
  151. package/runtime/node_modules/@metro-labs/telegram-user/src/group.ts +261 -0
  152. package/runtime/node_modules/@metro-labs/telegram-user/src/history.ts +52 -0
  153. package/runtime/node_modules/@metro-labs/telegram-user/src/inbound.ts +78 -0
  154. package/runtime/node_modules/@metro-labs/telegram-user/src/index.ts +46 -0
  155. package/runtime/node_modules/@metro-labs/telegram-user/src/login.ts +166 -0
  156. package/runtime/node_modules/@metro-labs/telegram-user/src/media-actions.ts +80 -0
  157. package/runtime/node_modules/@metro-labs/telegram-user/src/members.ts +75 -0
  158. package/runtime/node_modules/@metro-labs/telegram-user/src/normalize.ts +22 -0
  159. package/runtime/node_modules/@metro-labs/telegram-user/src/reactions.ts +167 -0
  160. package/runtime/node_modules/@metro-labs/telegram-user/src/station.ts +24 -0
  161. package/runtime/node_modules/@metro-labs/telegram-user/src/types.ts +7 -0
  162. package/runtime/node_modules/@metro-labs/telegram-user/src/wire.ts +1 -0
  163. package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
  164. package/runtime/node_modules/@metro-labs/webhook/src/station.ts +40 -0
  165. package/runtime/node_modules/@metro-labs/whatsapp/package.json +29 -0
  166. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +67 -0
  167. package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
  168. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +178 -0
  169. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
  170. package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
  171. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +318 -0
  172. package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
  173. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +132 -0
  174. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
  175. package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
  176. package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
  177. package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
  178. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
  179. package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
  180. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
  181. package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
  182. package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +141 -0
  183. package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +17 -0
  184. package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
  185. package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
  186. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
  187. package/runtime/node_modules/@metro-labs/xmtp/package.json +33 -0
  188. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +113 -0
  189. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
  190. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
  191. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
  192. package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
  193. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +375 -0
  194. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
  195. package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
  196. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
  197. package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
  198. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +333 -0
  199. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +78 -0
  200. package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
  201. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
  202. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +33 -0
  203. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
  204. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
  205. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
  206. package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
  207. package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +13 -0
  208. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
  209. package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
  210. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
  211. package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +84 -0
  212. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +39 -0
  213. package/runtime/runtime.json +3 -0
  214. package/runtime/trains/.keep +0 -0
  215. package/LICENSE +0 -21
  216. package/README.md +0 -78
  217. package/dist/channels/discord.js +0 -148
  218. package/dist/channels/telegram.js +0 -137
  219. package/dist/lib/address.js +0 -21
  220. package/dist/lib/codex-rc.js +0 -274
  221. package/dist/lib/dotenv.js +0 -31
  222. package/dist/log.js +0 -12
  223. package/dist/paths.js +0 -45
  224. package/dist/tail.js +0 -161
  225. package/skills/metro/SKILL.md +0 -122
@@ -0,0 +1,322 @@
1
+ import type { ProjectApiDeps } from './project-api.js';
2
+ import { join } from 'node:path';
3
+ import { type Server } from 'node:http';
4
+ import { selfLine, userSelf } from './events.js';
5
+ import { setTrainCallBackend } from './train-call.js';
6
+ import { errMsg, log, logFatalSync } from './log.js';
7
+ import { acquireLock, loadMetroEnv, STATE_DIR } from './paths.js';
8
+ import { installCrashGuard, markDaemonReady } from './crash-guard.js';
9
+ import {
10
+ loadTunnelConfig,
11
+ Tunnel,
12
+ warnOnLegacyWebhooks,
13
+ webhookPort,
14
+ } from './tunnel.js';
15
+ import { TrainSupervisor, TRAINS_DIR } from './supervisor.js';
16
+ import {
17
+ makeEmit,
18
+ startWebhookServer,
19
+ trainEventToMetroEvent,
20
+ } from './http.js';
21
+ import { type RunApiDeps } from './run-api.js';
22
+ import {
23
+ claimRuntime,
24
+ fenceRuntime,
25
+ runtimeLabels,
26
+ touchRuntime,
27
+ } from '../db/runtimes.js';
28
+ import {
29
+ mintRuntimeCodeForEmail,
30
+ releaseRuntimeForEmail,
31
+ } from '../db/runtime-admin.js';
32
+ import { loadAgentForRuntime, localAgentKey } from '../db/materialize.js';
33
+ import {
34
+ agentLiveness,
35
+ closeAgentSession,
36
+ createMetroMcp,
37
+ } from '../mcp/index.js';
38
+ import { metroCall } from '../mcp/ctx.js';
39
+ import { gatherAccountsForAgents } from '../mcp/accounts.js';
40
+ import {
41
+ accountStationCapabilities,
42
+ stationByName,
43
+ } from '../stations/registry.js';
44
+ import { prepareAccount } from '../stations/attach.js';
45
+ import {
46
+ materializeFrom,
47
+ materializeFromDb,
48
+ reloadAccountsFromDb,
49
+ reloadFrom,
50
+ } from '../db/materialize.js';
51
+ import {
52
+ httpSource,
53
+ runtimeConfigFromEnv,
54
+ startRuntimePoller,
55
+ } from './runtime-source.js';
56
+ import { createAgentForEmail, deleteAgentForEmail, listAgentsForEmail, ownedAgentOrThrow, resetAgentKeyForEmail, type ResetAgentKey,} from '../db/agent-admin.js';
57
+ import {
58
+ attachAccountToAgent,
59
+ detachAccountFromAgent,
60
+ } from '../db/account-attach.js';
61
+ import {
62
+ createConnectorForEmail,
63
+ createPendingConnectorForEmail,
64
+ deleteConnectorForEmail,
65
+ disconnectConnectorForEmail,
66
+ getConnectorForEmail,
67
+ listConnectorsForEmail,
68
+ listFreshConnectorsByIds,
69
+ renameConnectorForEmail,
70
+ reconnectConnectorForEmail,
71
+ verifyConnectorForEmail,
72
+ } from '../db/connectors.js';
73
+ import {
74
+ addMemberForEmail,
75
+ createProjectForEmail,
76
+ deleteProjectForEmail,
77
+ listMembersForEmail,
78
+ listProjectsForEmail,
79
+ removeMemberForEmail,
80
+ renameProjectForEmail,
81
+ setMemberRoleForEmail,
82
+ } from '../db/projects.js';
83
+ import {
84
+ addToCollectionForEmail,
85
+ createCollectionForEmail,
86
+ deleteCollectionForEmail,
87
+ getCollectionForEmail,
88
+ listCollectionsForEmail,
89
+ removeFromCollectionForEmail,
90
+ renameCollectionForEmail,
91
+ } from '../db/connector-collections.js';
92
+ import type { StationName } from '../db/schema.js';
93
+ import { AttachSessions } from './attach-session.js';
94
+ import { startUploadReaper } from './upload-store.js';
95
+ import type { AgentApiDeps } from './agent-api.js';
96
+ import type { ConnectorApiDeps } from './connector-api.js';
97
+
98
+ installCrashGuard();
99
+ loadMetroEnv();
100
+ acquireLock(join(STATE_DIR, '.tail-lock'));
101
+
102
+ const self = userSelf();
103
+ log.info({ self, line: selfLine() }, 'user identity');
104
+
105
+ process.stdout.on('error', (err) => {
106
+ if ((err as NodeJS.ErrnoException).code !== 'EPIPE')
107
+ log.warn({ err: errMsg(err) }, 'stdout error');
108
+ });
109
+
110
+ const supervisor = new TrainSupervisor();
111
+ const emit = makeEmit();
112
+
113
+ supervisor.onTrainEvent((env, train) => {
114
+ const entry = trainEventToMetroEvent(env, train);
115
+ if (entry) emit(entry);
116
+ });
117
+
118
+ let webhookServer: Server | null = null;
119
+ const tunnelCfg = loadTunnelConfig();
120
+ const tunnel = tunnelCfg ? new Tunnel(tunnelCfg, webhookPort()) : null;
121
+
122
+ setTrainCallBackend((train, action, args) =>
123
+ supervisor.call(train, action, args),
124
+ );
125
+
126
+ const runtime = runtimeConfigFromEnv();
127
+ const localSource = runtime === null ? null : httpSource(runtime);
128
+
129
+ function announceLocalEndpoint(): void {
130
+ const key = localAgentKey();
131
+ if (key === null) {
132
+ log.warn('this agent has no key — reset it in the web UI to connect an agent');
133
+ return;
134
+ }
135
+ const url = `http://127.0.0.1:${String(webhookPort())}/mcp?token=${key}`;
136
+ process.stderr.write(
137
+ `\nConnect an agent on this machine:\n\n claude mcp add --transport http metro "${url}"\n\n`,
138
+ );
139
+ }
140
+
141
+ async function applyStations(removed: StationName[]): Promise<void> {
142
+ for (const station of removed) await supervisor.stopTrain(station);
143
+ }
144
+
145
+ async function syncLocal(): Promise<void> {
146
+ if (localSource === null) return;
147
+ const before = new Set(supervisor.running());
148
+ const { active, removed } = await reloadFrom(localSource);
149
+ await applyStations(removed);
150
+ for (const station of active)
151
+ if (before.has(station)) supervisor.requestReload(station);
152
+ }
153
+
154
+ async function stopLocalStations(): Promise<void> {
155
+ for (const station of supervisor.running()) await supervisor.stopTrain(station);
156
+ }
157
+
158
+ async function syncStations(station: StationName): Promise<void> {
159
+ const { removed } = await reloadAccountsFromDb();
160
+ if (stationByName(station)?.hasTrain === false) return;
161
+ if (removed.includes(station)) await supervisor.stopTrain(station);
162
+ else supervisor.requestReload(station);
163
+ }
164
+
165
+ const attachSessions = new AttachSessions({
166
+ authorize: async (owner) => {
167
+ await ownedAgentOrThrow(owner.email, owner.agentId);
168
+ },
169
+ complete: async (owner, station, config) => {
170
+ const ref = await attachAccountToAgent(
171
+ owner.email,
172
+ owner.agentId,
173
+ station,
174
+ config,
175
+ );
176
+ const activated = await syncStations(station).then(
177
+ () => true,
178
+ (err: unknown) => {
179
+ log.warn(
180
+ { station, err: errMsg(err) },
181
+ 'attach-session: station reload failed, the change lands at the next boot',
182
+ );
183
+ return false;
184
+ },
185
+ );
186
+ return { accountId: ref.accountId, activated };
187
+ },
188
+ });
189
+
190
+ async function resetAgentKey(
191
+ email: string,
192
+ id: string,
193
+ ): Promise<ResetAgentKey> {
194
+ const reset = await resetAgentKeyForEmail(email, id);
195
+ const closed = await closeAgentSession(id);
196
+ log.info(
197
+ { agent: reset.name, id: reset.id, sessionClosed: closed },
198
+ 'agent-api: key rotated, live session dropped',
199
+ );
200
+ return reset;
201
+ }
202
+
203
+ const agentApi: AgentApiDeps = {
204
+ attachSessions,
205
+ listAgents: listAgentsForEmail,
206
+ createAgent: createAgentForEmail,
207
+ deleteAgent: deleteAgentForEmail,
208
+ resetKey: resetAgentKey,
209
+ gatherAccounts: gatherAccountsForAgents,
210
+ capabilities: accountStationCapabilities,
211
+ liveness: agentLiveness,
212
+ mintRuntimeCode: mintRuntimeCodeForEmail,
213
+ releaseRuntime: releaseRuntimeForEmail,
214
+ runtimes: runtimeLabels,
215
+ prepareAccount,
216
+ attachAccount: attachAccountToAgent,
217
+ detachAccount: detachAccountFromAgent,
218
+ syncStations,
219
+ };
220
+
221
+ const runApi: RunApiDeps = {
222
+ claimRuntime,
223
+ fenceRuntime,
224
+ touchRuntime,
225
+ loadAgent: loadAgentForRuntime,
226
+ };
227
+
228
+ const projectApi: ProjectApiDeps = {
229
+ listProjects: listProjectsForEmail,
230
+ createProject: createProjectForEmail,
231
+ renameProject: renameProjectForEmail,
232
+ deleteProject: deleteProjectForEmail,
233
+ listMembers: listMembersForEmail,
234
+ addMember: addMemberForEmail,
235
+ setMemberRole: setMemberRoleForEmail,
236
+ removeMember: removeMemberForEmail,
237
+ };
238
+
239
+ const connectorApi: ConnectorApiDeps = {
240
+ listConnectors: listConnectorsForEmail,
241
+ freshConnectorsByIds: listFreshConnectorsByIds,
242
+ listCollections: listCollectionsForEmail,
243
+ getCollection: getCollectionForEmail,
244
+ createCollection: createCollectionForEmail,
245
+ renameCollection: renameCollectionForEmail,
246
+ deleteCollection: deleteCollectionForEmail,
247
+ addToCollection: addToCollectionForEmail,
248
+ removeFromCollection: removeFromCollectionForEmail,
249
+ renameConnector: renameConnectorForEmail,
250
+ createConnector: createConnectorForEmail,
251
+ createPendingConnector: createPendingConnectorForEmail,
252
+ reconnectConnector: reconnectConnectorForEmail,
253
+ getConnector: getConnectorForEmail,
254
+ verifyConnector: verifyConnectorForEmail,
255
+ disconnectConnector: disconnectConnectorForEmail,
256
+ deleteConnector: deleteConnectorForEmail,
257
+ };
258
+
259
+ async function main(): Promise<void> {
260
+ if (localSource === null) await materializeFromDb();
261
+ else await materializeFrom(localSource);
262
+ warnOnLegacyWebhooks();
263
+ supervisor.start();
264
+ const metroMcp = await createMetroMcp();
265
+ webhookServer = await startWebhookServer(
266
+ emit,
267
+ { agentApi, connectorApi, projectApi, runApi },
268
+ metroMcp.httpHandler,
269
+ metroCall,
270
+ );
271
+ metroMcp.startInbound();
272
+ startUploadReaper();
273
+ if (localSource !== null) {
274
+ startRuntimePoller({ sync: syncLocal, stopAll: stopLocalStations });
275
+ announceLocalEndpoint();
276
+ }
277
+ tunnel?.start();
278
+ log.info(
279
+ { tunnel: !!tunnel, trainsDir: TRAINS_DIR, mcp: '/' },
280
+ 'dispatcher ready',
281
+ );
282
+ markDaemonReady();
283
+ }
284
+
285
+ const SHUTDOWN_TIMEOUT_MS = 3_000;
286
+ let shuttingDown = false;
287
+ async function shutdown(): Promise<void> {
288
+ if (shuttingDown) return;
289
+ shuttingDown = true;
290
+ log.info('dispatcher shutting down');
291
+ tunnel?.stop();
292
+ if (webhookServer) {
293
+ const server = webhookServer;
294
+ await Promise.race([
295
+ new Promise<void>((r) => {
296
+ server.close(() => {
297
+ r();
298
+ });
299
+ }),
300
+ new Promise<void>((r) => {
301
+ setTimeout(r, SHUTDOWN_TIMEOUT_MS).unref();
302
+ }),
303
+ ]);
304
+ }
305
+ await attachSessions.stop();
306
+ await supervisor.stop();
307
+ process.exit(0);
308
+ }
309
+ const onShutdown = (): void => {
310
+ shutdown().catch((err: unknown) => {
311
+ log.error({ err: errMsg(err) }, 'dispatcher: shutdown failed');
312
+ process.exit(1);
313
+ });
314
+ };
315
+ if (process.env.METRO_STDIN_SHUTDOWN === '1')
316
+ process.stdin.on('end', onShutdown).on('close', onShutdown);
317
+ for (const sig of ['SIGINT', 'SIGTERM'] as const) process.on(sig, onShutdown);
318
+
319
+ await main().catch((err: unknown) => {
320
+ logFatalSync({ err: errMsg(err) }, 'dispatcher failed to start');
321
+ process.exit(1);
322
+ });
@@ -0,0 +1,111 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { errMsg, log } from './log.js';
3
+ import {
4
+ bodyField,
5
+ cliIdentity,
6
+ cors,
7
+ readJsonBody,
8
+ sendJson,
9
+ } from './api-http.js';
10
+ import { CLI_CODE_RE, takeCliCode } from './cli-pair.js';
11
+ import { mcpServersJson } from './connector-json.js';
12
+ import { sessionTtlFromEnv } from './google-oauth.js';
13
+ import { signCliToken } from './session.js';
14
+ import type { ConnectorApiDeps } from './connector-api.js';
15
+
16
+ const CLAIM_PATH = '/api/cli/claim';
17
+ const MCP_PATH = '/api/cli/mcp';
18
+ const SESSION_PATH = '/api/cli/session';
19
+
20
+ const ALLOWED: Record<string, string> = {
21
+ [CLAIM_PATH]: 'POST',
22
+ [MCP_PATH]: 'GET',
23
+ [SESSION_PATH]: 'GET',
24
+ };
25
+
26
+ function secretOrNull(): string | null {
27
+ const secret = process.env.METRO_SESSION_SECRET?.trim() ?? '';
28
+ return secret === '' ? null : secret;
29
+ }
30
+
31
+ async function handleClaim(
32
+ req: IncomingMessage,
33
+ res: ServerResponse,
34
+ deps: ConnectorApiDeps,
35
+ ): Promise<void> {
36
+ const secret = secretOrNull();
37
+ if (secret === null) {
38
+ sendJson(req, res, 503, { error: 'sign-in is not configured' });
39
+ return;
40
+ }
41
+ const code = bodyField(await readJsonBody(req), 'code');
42
+ const taken =
43
+ typeof code === 'string' && CLI_CODE_RE.test(code)
44
+ ? takeCliCode(code)
45
+ : undefined;
46
+ if (taken === undefined) {
47
+ sendJson(req, res, 400, {
48
+ error: 'that code has expired or was already used',
49
+ });
50
+ return;
51
+ }
52
+ const collection = await deps.getCollection(taken.email, taken.collectionId);
53
+ const token = signCliToken(
54
+ { email: taken.email, collectionId: collection.id },
55
+ secret,
56
+ { ttlSec: sessionTtlFromEnv() },
57
+ );
58
+ log.info({ email: taken.email, collection: collection.id }, 'cli: code claimed');
59
+ sendJson(req, res, 200, { token, email: taken.email, collection: collection.name });
60
+ }
61
+
62
+ async function handleRead(
63
+ req: IncomingMessage,
64
+ res: ServerResponse,
65
+ deps: ConnectorApiDeps,
66
+ path: string,
67
+ ): Promise<void> {
68
+ const who = cliIdentity(req);
69
+ if (who === null) {
70
+ sendJson(req, res, 401, { error: 'unauthorized' });
71
+ return;
72
+ }
73
+ const collection = await deps.getCollection(who.email, who.collectionId);
74
+ if (path === SESSION_PATH) {
75
+ sendJson(req, res, 200, { email: who.email, collection: collection.name });
76
+ return;
77
+ }
78
+ const rows = await deps.freshConnectorsByIds(collection.connectorIds);
79
+ sendJson(req, res, 200, { json: mcpServersJson(rows), collection: collection.name });
80
+ }
81
+
82
+ export function handleCliPairRequest(
83
+ req: IncomingMessage,
84
+ res: ServerResponse,
85
+ deps: ConnectorApiDeps,
86
+ ): boolean {
87
+ const path = (req.url ?? '').split('?')[0] ?? '';
88
+ const method = ALLOWED[path];
89
+ if (method === undefined) {
90
+ if (!path.startsWith('/api/cli/') && path !== '/api/cli') return false;
91
+ sendJson(req, res, 404, { error: 'no such route' });
92
+ return true;
93
+ }
94
+ if (req.method === 'OPTIONS') {
95
+ res.writeHead(204, cors(req)).end();
96
+ return true;
97
+ }
98
+ if (req.method !== method) {
99
+ sendJson(req, res, 405, { error: 'method not allowed' });
100
+ return true;
101
+ }
102
+ const work =
103
+ path === CLAIM_PATH
104
+ ? handleClaim(req, res, deps)
105
+ : handleRead(req, res, deps, path);
106
+ work.catch((err: unknown) => {
107
+ log.warn({ err: errMsg(err) }, 'cli: unhandled error');
108
+ if (!res.headersSent) sendJson(req, res, 500, { error: 'cli api failed' });
109
+ });
110
+ return true;
111
+ }
@@ -0,0 +1,20 @@
1
+ import { makeCodeStore } from './pair-codes.js';
2
+
3
+ export interface CliCode {
4
+ email: string;
5
+ collectionId: string;
6
+ }
7
+
8
+ const store = makeCodeStore<CliCode>('mc');
9
+
10
+ export const CLI_CODE_RE = store.pattern;
11
+
12
+ export const mintCliCode = (
13
+ entry: CliCode,
14
+ now?: number,
15
+ ): { code: string; expiresAt: number } => store.mint(entry, now);
16
+
17
+ export const takeCliCode = (code: string, now?: number): CliCode | undefined =>
18
+ store.take(code, now);
19
+
20
+ export const cliCodeCount = (): number => store.count();
@@ -0,0 +1,202 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { errMsg, log } from './log.js';
3
+ import {
4
+ apiFailure,
5
+ apiSession,
6
+ projectParam,
7
+ bodyField,
8
+ cors,
9
+ readJsonBody,
10
+ sendJson,
11
+ type ApiSession,
12
+ } from './api-http.js';
13
+ import { parseId } from '../db/ids.js';
14
+ import { mintCliCode } from './cli-pair.js';
15
+ import type { ConnectorApiDeps } from './connector-api.js';
16
+
17
+ const PREFIX = '/api/collections';
18
+
19
+ type Routable =
20
+ | { kind: 'index' }
21
+ | { kind: 'collection'; id: string }
22
+ | { kind: 'rename'; id: string }
23
+ | { kind: 'code'; id: string }
24
+ | { kind: 'items'; id: string }
25
+ | { kind: 'item'; id: string; connectorId: string };
26
+
27
+ type Target = Routable | { kind: 'unknown' } | null;
28
+
29
+ const ALLOWED: Record<Routable['kind'], string[]> = {
30
+ index: ['GET', 'POST'],
31
+ collection: ['GET', 'DELETE'],
32
+ rename: ['POST'],
33
+ code: ['POST'],
34
+ items: ['POST'],
35
+ item: ['DELETE'],
36
+ };
37
+
38
+ function itemTarget(id: string, rest: string[]): Target {
39
+ if (rest.length === 1) return { kind: 'items', id };
40
+ if (rest.length > 2) return { kind: 'unknown' };
41
+ const connectorId = parseId(rest[1] ?? '');
42
+ return connectorId === null
43
+ ? { kind: 'unknown' }
44
+ : { kind: 'item', id, connectorId };
45
+ }
46
+
47
+ function subTarget(id: string, rest: string[]): Target {
48
+ if (rest.length === 0) return { kind: 'collection', id };
49
+ const head = rest[0];
50
+ if (head === 'items') return itemTarget(id, rest);
51
+ if (rest.length > 1) return { kind: 'unknown' };
52
+ if (head === 'rename') return { kind: 'rename', id };
53
+ if (head === 'code') return { kind: 'code', id };
54
+ return { kind: 'unknown' };
55
+ }
56
+
57
+ function target(path: string): Target {
58
+ if (path === PREFIX || path === `${PREFIX}/`) return { kind: 'index' };
59
+ if (!path.startsWith(`${PREFIX}/`)) return null;
60
+ const segments = path.slice(PREFIX.length + 1).split('/').filter(Boolean);
61
+ const head = segments[0];
62
+ if (head === undefined) return { kind: 'index' };
63
+ const id = parseId(head);
64
+ return id === null ? { kind: 'unknown' } : subTarget(id, segments.slice(1));
65
+ }
66
+
67
+ async function nameFrom(req: IncomingMessage): Promise<string | null> {
68
+ const name = bodyField(await readJsonBody(req), 'name');
69
+ return typeof name === 'string' ? name : null;
70
+ }
71
+
72
+ async function handleIndex(
73
+ req: IncomingMessage,
74
+ res: ServerResponse,
75
+ deps: ConnectorApiDeps,
76
+ session: ApiSession,
77
+ ): Promise<void> {
78
+ const project = projectParam(req);
79
+ if (project === null) {
80
+ sendJson(req, res, 400, { error: 'a project is required' });
81
+ return;
82
+ }
83
+ if (req.method === 'GET') {
84
+ sendJson(req, res, 200, {
85
+ collections: await deps.listCollections(session.email, project),
86
+ });
87
+ return;
88
+ }
89
+ const name = await nameFrom(req);
90
+ if (name === null) {
91
+ sendJson(req, res, 400, { error: 'name is required' });
92
+ return;
93
+ }
94
+ const list = await deps.createCollection(session.email, project, name);
95
+ log.info({ id: list.id, name: list.name }, 'collection-api: created collection');
96
+ sendJson(req, res, 201, list);
97
+ }
98
+
99
+ async function handleRename(
100
+ req: IncomingMessage,
101
+ res: ServerResponse,
102
+ deps: ConnectorApiDeps,
103
+ session: ApiSession,
104
+ id: string,
105
+ ): Promise<void> {
106
+ const name = await nameFrom(req);
107
+ if (name === null) {
108
+ sendJson(req, res, 400, { error: 'name is required' });
109
+ return;
110
+ }
111
+ sendJson(req, res, 200, await deps.renameCollection(session.email, id, name));
112
+ }
113
+
114
+ async function handleItems(
115
+ req: IncomingMessage,
116
+ res: ServerResponse,
117
+ deps: ConnectorApiDeps,
118
+ session: ApiSession,
119
+ id: string,
120
+ ): Promise<void> {
121
+ const connectorId = bodyField(await readJsonBody(req), 'connectorId');
122
+ if (typeof connectorId !== 'string' || parseId(connectorId) === null) {
123
+ sendJson(req, res, 400, { error: 'connectorId is required' });
124
+ return;
125
+ }
126
+ sendJson(req, res, 200, await deps.addToCollection(session.email, id, connectorId));
127
+ }
128
+
129
+ async function handleCode(
130
+ req: IncomingMessage,
131
+ res: ServerResponse,
132
+ deps: ConnectorApiDeps,
133
+ session: ApiSession,
134
+ id: string,
135
+ ): Promise<void> {
136
+ const collection = await deps.getCollection(session.email, id);
137
+ const minted = mintCliCode({ email: session.email, collectionId: collection.id });
138
+ log.info({ id: collection.id, email: session.email }, 'collection-api: minted cli code');
139
+ sendJson(req, res, 200, { ...minted, collection: collection.name });
140
+ }
141
+
142
+ async function route(
143
+ req: IncomingMessage,
144
+ res: ServerResponse,
145
+ deps: ConnectorApiDeps,
146
+ session: ApiSession,
147
+ tgt: Routable,
148
+ ): Promise<void> {
149
+ try {
150
+ if (tgt.kind === 'index')
151
+ await handleIndex(req, res, deps, session);
152
+ else if (tgt.kind === 'rename')
153
+ await handleRename(req, res, deps, session, tgt.id);
154
+ else if (tgt.kind === 'code')
155
+ await handleCode(req, res, deps, session, tgt.id);
156
+ else if (tgt.kind === 'items')
157
+ await handleItems(req, res, deps, session, tgt.id);
158
+ else if (tgt.kind === 'item')
159
+ sendJson(
160
+ req,
161
+ res,
162
+ 200,
163
+ await deps.removeFromCollection(session.email, tgt.id, tgt.connectorId),
164
+ );
165
+ else if (req.method === 'DELETE')
166
+ sendJson(req, res, 200, await deps.deleteCollection(session.email, tgt.id));
167
+ else sendJson(req, res, 200, await deps.getCollection(session.email, tgt.id));
168
+ } catch (err) {
169
+ apiFailure(req, res, err, 'collection-api');
170
+ }
171
+ }
172
+
173
+ export function handleCollectionApiRequest(
174
+ req: IncomingMessage,
175
+ res: ServerResponse,
176
+ deps: ConnectorApiDeps,
177
+ ): boolean {
178
+ const tgt = target((req.url ?? '').split('?')[0] ?? '');
179
+ if (tgt === null) return false;
180
+ if (req.method === 'OPTIONS') {
181
+ res.writeHead(204, cors(req)).end();
182
+ return true;
183
+ }
184
+ if (tgt.kind === 'unknown') {
185
+ sendJson(req, res, 404, { error: 'no such collection' });
186
+ return true;
187
+ }
188
+ if (!ALLOWED[tgt.kind].includes(req.method ?? '')) {
189
+ sendJson(req, res, 405, { error: 'method not allowed' });
190
+ return true;
191
+ }
192
+ const session = apiSession(req);
193
+ if (!session) {
194
+ sendJson(req, res, 401, { error: 'unauthorized' });
195
+ return true;
196
+ }
197
+ route(req, res, deps, session, tgt).catch((err: unknown) => {
198
+ log.warn({ err: errMsg(err) }, 'list-api: unhandled error');
199
+ if (!res.headersSent) sendJson(req, res, 500, { error: 'list api failed' });
200
+ });
201
+ return true;
202
+ }