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

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 (436) hide show
  1. package/dist/api.js +9 -101
  2. package/dist/claude-settings.js +40 -0
  3. package/dist/claude.js +152 -0
  4. package/dist/cli.js +52 -109
  5. package/dist/control.js +47 -75
  6. package/dist/hold.js +210 -0
  7. package/dist/local.js +43 -0
  8. package/dist/mcp-config.js +25 -0
  9. package/dist/node-name.js +71 -0
  10. package/dist/onboarding.js +44 -0
  11. package/dist/provider-flags.js +9 -0
  12. package/dist/route.js +58 -0
  13. package/dist/runtime-install.js +96 -0
  14. package/dist/runtime.js +29 -52
  15. package/dist/serve.js +157 -0
  16. package/dist/service.js +304 -0
  17. package/dist/tail.js +119 -0
  18. package/dist/update.js +13 -1
  19. package/package.json +2 -23
  20. package/runtime/marketplace/.claude-plugin/marketplace.json +12 -0
  21. package/runtime/marketplace/plugin/.claude-plugin/plugin.json +17 -0
  22. package/runtime/marketplace/plugin/.mcp.json +1 -0
  23. package/runtime/marketplace/plugin/bin/guard.mjs +71 -0
  24. package/runtime/marketplace/plugin/bin/metro-plugin.mjs +30 -0
  25. package/runtime/marketplace/plugin/bin/policy.mjs +97 -0
  26. package/runtime/marketplace/plugin/bin/session-start.mjs +31 -0
  27. package/runtime/marketplace/plugin/hooks/hooks.json +25 -0
  28. package/runtime/marketplace/plugin/orchestrator.md +26 -0
  29. package/runtime/node_modules/@metro-labs/core/package.json +47 -0
  30. package/runtime/node_modules/@metro-labs/core/src/endpoints.ts +7 -0
  31. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → core/src}/events.ts +11 -7
  32. package/runtime/node_modules/@metro-labs/{mcp/src/db → core/src}/ids.ts +5 -0
  33. package/runtime/node_modules/@metro-labs/core/src/is-record.ts +2 -0
  34. package/runtime/node_modules/@metro-labs/{mcp/src/stations → core/src}/lines.ts +24 -28
  35. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → core/src}/protocol.ts +7 -4
  36. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → core/src}/secure-fs.ts +17 -0
  37. package/runtime/node_modules/@metro-labs/core/src/station-names.ts +13 -0
  38. package/runtime/node_modules/@metro-labs/core/src/stations/account-files.ts +41 -0
  39. package/runtime/node_modules/@metro-labs/core/src/stations/account-store.ts +83 -0
  40. package/runtime/node_modules/@metro-labs/{mcp → core}/src/stations/attachments.ts +0 -1
  41. package/runtime/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +16 -0
  42. package/runtime/node_modules/@metro-labs/core/src/stations/profile.ts +58 -0
  43. package/runtime/node_modules/@metro-labs/core/src/stations/sender-profile.ts +64 -0
  44. package/runtime/node_modules/@metro-labs/{mcp → core}/src/stations/station-runtime.ts +6 -11
  45. package/runtime/node_modules/@metro-labs/core/src/stations/train-boot.ts +42 -0
  46. package/runtime/node_modules/@metro-labs/core/src/stations/train-events.ts +84 -0
  47. package/runtime/node_modules/@metro-labs/{mcp → core}/src/stations/types.ts +18 -1
  48. package/runtime/node_modules/@metro-labs/core/src/tickets.ts +37 -0
  49. package/runtime/node_modules/@metro-labs/core/src/version.ts +2 -0
  50. package/runtime/node_modules/@metro-labs/daemon/package.json +48 -0
  51. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/agent-fs.ts +134 -0
  52. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/files-api.ts +12 -0
  53. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/files.ts +89 -0
  54. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/home-fs.ts +113 -0
  55. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/job-env.ts +51 -0
  56. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/provision.ts +80 -0
  57. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedule-detail.ts +99 -0
  58. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules-api.ts +27 -0
  59. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules.ts +144 -0
  60. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/unit-files.ts +37 -0
  61. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/user.ts +89 -0
  62. package/runtime/node_modules/@metro-labs/daemon/src/agent-user/view.ts +87 -0
  63. package/runtime/node_modules/@metro-labs/daemon/src/agents/account-attach.ts +19 -0
  64. package/runtime/node_modules/@metro-labs/daemon/src/agents/account-lookups.ts +55 -0
  65. package/runtime/node_modules/@metro-labs/daemon/src/agents/account-routes.ts +76 -0
  66. package/runtime/node_modules/@metro-labs/daemon/src/agents/accounts-api.ts +357 -0
  67. package/runtime/node_modules/@metro-labs/daemon/src/agents/admin.ts +30 -0
  68. package/runtime/node_modules/@metro-labs/daemon/src/agents/allowlist.ts +59 -0
  69. package/runtime/node_modules/@metro-labs/daemon/src/agents/api.ts +131 -0
  70. package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +132 -0
  71. package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +305 -0
  72. package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +142 -0
  73. package/runtime/node_modules/@metro-labs/{mcp/src/db/key-map.ts → daemon/src/agents/keys.ts} +1 -11
  74. package/runtime/node_modules/@metro-labs/{mcp/src/db/agent-map.ts → daemon/src/agents/map.ts} +58 -1
  75. package/runtime/node_modules/@metro-labs/daemon/src/agents/policy-route.ts +27 -0
  76. package/runtime/node_modules/@metro-labs/{mcp/src/db/agent-scope.ts → daemon/src/agents/scope.ts} +2 -2
  77. package/runtime/node_modules/@metro-labs/daemon/src/agents/sender-cards.ts +94 -0
  78. package/runtime/node_modules/@metro-labs/daemon/src/agents/senders.ts +38 -0
  79. package/runtime/node_modules/@metro-labs/daemon/src/approvals/api.ts +55 -0
  80. package/runtime/node_modules/@metro-labs/daemon/src/approvals/pending.ts +150 -0
  81. package/runtime/node_modules/@metro-labs/daemon/src/approvals/preview.ts +66 -0
  82. package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +218 -0
  83. package/runtime/node_modules/@metro-labs/daemon/src/boot/connect-hint.ts +36 -0
  84. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/boot}/crash-guard.ts +1 -1
  85. package/runtime/node_modules/@metro-labs/daemon/src/boot/local-owner.ts +21 -0
  86. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/boot}/paths.ts +6 -34
  87. package/runtime/node_modules/@metro-labs/daemon/src/channels/addressed.ts +26 -0
  88. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/channels/inbound.ts +70 -45
  89. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/channels/media-note.ts +11 -7
  90. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/channels/pending.ts +4 -1
  91. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/channels/relay.ts +2 -2
  92. package/runtime/node_modules/@metro-labs/daemon/src/claude/api.ts +355 -0
  93. package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +359 -0
  94. package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +208 -0
  95. package/runtime/node_modules/@metro-labs/daemon/src/claude/memory.ts +32 -0
  96. package/runtime/node_modules/@metro-labs/daemon/src/claude/onboarding.ts +60 -0
  97. package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-install.ts +149 -0
  98. package/runtime/node_modules/@metro-labs/daemon/src/claude/session-files.ts +75 -0
  99. package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +284 -0
  100. package/runtime/node_modules/@metro-labs/daemon/src/claude/settings.ts +109 -0
  101. package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +273 -0
  102. package/runtime/node_modules/@metro-labs/daemon/src/claude/skills.ts +120 -0
  103. package/runtime/node_modules/@metro-labs/daemon/src/claude/version.ts +113 -0
  104. package/runtime/node_modules/@metro-labs/{mcp/src/daemon/connector-api.ts → daemon/src/connectors/api.ts} +79 -113
  105. package/runtime/node_modules/@metro-labs/{mcp/src/db/connector-config.ts → daemon/src/connectors/config.ts} +67 -41
  106. package/runtime/node_modules/@metro-labs/daemon/src/connectors/gates.ts +72 -0
  107. package/runtime/node_modules/@metro-labs/daemon/src/connectors/health.ts +18 -0
  108. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/connectors}/oauth-client.ts +14 -17
  109. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/connectors}/oauth-discovery.ts +33 -20
  110. package/runtime/node_modules/@metro-labs/{mcp/src/daemon/connector-oauth-routes.ts → daemon/src/connectors/oauth-routes.ts} +30 -32
  111. package/runtime/node_modules/@metro-labs/{mcp/src/daemon/connector-oauth.ts → daemon/src/connectors/oauth.ts} +61 -22
  112. package/runtime/node_modules/@metro-labs/daemon/src/connectors/plugin-sync.ts +131 -0
  113. package/runtime/node_modules/@metro-labs/daemon/src/connectors/reach.ts +27 -0
  114. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-policy.ts +90 -0
  115. package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +347 -0
  116. package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +385 -0
  117. package/runtime/node_modules/@metro-labs/daemon/src/connectors/tools.ts +81 -0
  118. package/runtime/node_modules/@metro-labs/daemon/src/connectors/url.ts +37 -0
  119. package/runtime/node_modules/@metro-labs/daemon/src/connectors/verify.ts +106 -0
  120. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/files}/attach-grant.ts +4 -4
  121. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/files}/attach-owner.ts +3 -3
  122. package/runtime/node_modules/@metro-labs/daemon/src/files/attach-reaper.ts +174 -0
  123. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/files}/attach-serve.ts +12 -13
  124. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/files}/upload-api.ts +10 -27
  125. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/files}/upload-store.ts +4 -4
  126. package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +233 -0
  127. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +208 -0
  128. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-device.ts +91 -0
  129. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-stream.ts +173 -0
  130. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-translate.ts +195 -0
  131. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-version.ts +44 -0
  132. package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +128 -0
  133. package/runtime/node_modules/@metro-labs/daemon/src/gateway/effort.ts +77 -0
  134. package/runtime/node_modules/@metro-labs/daemon/src/gateway/eventstream.ts +123 -0
  135. package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +237 -0
  136. package/runtime/node_modules/@metro-labs/daemon/src/gateway/frames.ts +118 -0
  137. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +274 -0
  138. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-auth.ts +160 -0
  139. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-client.ts +46 -0
  140. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-setup.ts +150 -0
  141. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-stream.ts +139 -0
  142. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-translate.ts +224 -0
  143. package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini.ts +208 -0
  144. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +199 -0
  145. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +278 -0
  146. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-signin.ts +193 -0
  147. package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-store.ts +70 -0
  148. package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +90 -0
  149. package/runtime/node_modules/@metro-labs/daemon/src/gateway/pkce.ts +36 -0
  150. package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +66 -0
  151. package/runtime/node_modules/@metro-labs/daemon/src/gateway/served.ts +18 -0
  152. package/runtime/node_modules/@metro-labs/daemon/src/gateway/subscription.ts +120 -0
  153. package/runtime/node_modules/@metro-labs/daemon/src/gateway/text.ts +14 -0
  154. package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage.ts +244 -0
  155. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/accounts.ts +28 -17
  156. package/runtime/node_modules/@metro-labs/daemon/src/mcp/attachment-schema.ts +91 -0
  157. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/call-tools.ts +49 -20
  158. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/ctx.ts +4 -4
  159. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/event-store.ts +15 -16
  160. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/group-tools.ts +2 -2
  161. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/index.ts +24 -79
  162. package/runtime/node_modules/@metro-labs/daemon/src/mcp/instructions.ts +19 -0
  163. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/member-tools.ts +2 -2
  164. package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-prompt.ts +87 -0
  165. package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-relay.ts +79 -0
  166. package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-gate.ts +82 -0
  167. package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-snapshot.ts +79 -0
  168. package/runtime/node_modules/@metro-labs/daemon/src/mcp/profile-lookup.ts +60 -0
  169. package/runtime/node_modules/@metro-labs/daemon/src/mcp/profile-tool.ts +96 -0
  170. package/runtime/node_modules/@metro-labs/daemon/src/mcp/read-tool.ts +87 -0
  171. package/runtime/node_modules/@metro-labs/daemon/src/mcp/request-identity.ts +46 -0
  172. package/runtime/node_modules/@metro-labs/daemon/src/mcp/session-slot.ts +48 -0
  173. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/session.ts +33 -42
  174. package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-catalog.ts +88 -0
  175. package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-def.ts +9 -0
  176. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/tool-dispatch.ts +53 -40
  177. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/tool-schemas.ts +68 -119
  178. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/upload-tool.ts +6 -11
  179. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/helper-script.ts +126 -0
  180. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/install.ts +37 -0
  181. package/runtime/node_modules/@metro-labs/daemon/src/metro-user/privilege.ts +48 -0
  182. package/runtime/node_modules/@metro-labs/daemon/src/monitor/api.ts +132 -0
  183. package/runtime/node_modules/@metro-labs/daemon/src/net/tunnel.ts +316 -0
  184. package/runtime/node_modules/@metro-labs/daemon/src/policy/policy.ts +118 -0
  185. package/runtime/node_modules/@metro-labs/daemon/src/routes/bearer.ts +35 -0
  186. package/runtime/node_modules/@metro-labs/daemon/src/routes/body.ts +24 -0
  187. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/routes}/http.ts +59 -51
  188. package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +205 -0
  189. package/runtime/node_modules/@metro-labs/daemon/src/routes/session-apis.ts +66 -0
  190. package/runtime/node_modules/@metro-labs/daemon/src/routes/session.ts +29 -0
  191. package/runtime/node_modules/@metro-labs/daemon/src/routes/threema-callback.ts +103 -0
  192. package/runtime/node_modules/@metro-labs/daemon/src/server/control.ts +39 -0
  193. package/runtime/node_modules/@metro-labs/daemon/src/server/machine.ts +58 -0
  194. package/runtime/node_modules/@metro-labs/daemon/src/server/owner.ts +26 -0
  195. package/runtime/node_modules/@metro-labs/daemon/src/server/update.ts +90 -0
  196. package/runtime/node_modules/@metro-labs/daemon/src/server.ts +2 -0
  197. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-driver.ts +42 -0
  198. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/stations/attach-inline.ts +29 -1
  199. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/stations/attach-interactive.ts +9 -36
  200. package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-outlook.ts +63 -0
  201. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/stations/attach-resolve.ts +38 -13
  202. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/stations}/attach-session.ts +32 -22
  203. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/stations/attach-xmtp.ts +8 -9
  204. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/stations/attach.ts +75 -9
  205. package/runtime/node_modules/@metro-labs/daemon/src/stations/materialize.ts +265 -0
  206. package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/stations/registry.ts +22 -6
  207. package/runtime/node_modules/@metro-labs/daemon/src/stations/runtime-deps.ts +82 -0
  208. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/stations}/supervisor-io.ts +3 -3
  209. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/stations}/supervisor.ts +6 -9
  210. package/runtime/node_modules/@metro-labs/daemon/src/stations/threema-callbacks.ts +32 -0
  211. package/runtime/node_modules/@metro-labs/{mcp/src/daemon → daemon/src/stations}/train-call.ts +1 -1
  212. package/runtime/node_modules/@metro-labs/daemon/src/stations/webhook-endpoints.ts +51 -0
  213. package/runtime/node_modules/@metro-labs/daemon/src/terminal/api.ts +84 -0
  214. package/runtime/node_modules/@metro-labs/daemon/src/terminal/socket.ts +124 -0
  215. package/runtime/node_modules/@metro-labs/daemon/src/terminal/tickets.ts +15 -0
  216. package/runtime/node_modules/@metro-labs/daemon/src/vault/api.ts +45 -0
  217. package/runtime/node_modules/@metro-labs/daemon/src/vault/config.ts +51 -0
  218. package/runtime/node_modules/@metro-labs/daemon/src/vault/firewall.ts +9 -0
  219. package/runtime/node_modules/@metro-labs/daemon/src/vault/index.ts +96 -0
  220. package/runtime/node_modules/@metro-labs/daemon/src/vault/install.ts +56 -0
  221. package/runtime/node_modules/@metro-labs/daemon/src/vault/paths.ts +26 -0
  222. package/runtime/node_modules/@metro-labs/daemon/src/vault/proxy.ts +147 -0
  223. package/runtime/node_modules/@metro-labs/daemon/src/vault/store.ts +129 -0
  224. package/runtime/node_modules/@metro-labs/daemon/src/vault/trust.ts +44 -0
  225. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/package.json +3 -5
  226. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/accounts.ts +8 -28
  227. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/actions.ts +19 -187
  228. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/attachments.ts +3 -3
  229. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/format.ts +31 -72
  230. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/group-actions.ts +2 -2
  231. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/group.ts +2 -2
  232. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/index.ts +15 -21
  233. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/members.ts +2 -2
  234. package/runtime/node_modules/@metro-labs/discord-bot/src/profile.ts +68 -0
  235. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/send-files.ts +12 -13
  236. package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +23 -0
  237. package/runtime/node_modules/@metro-labs/http/package.json +29 -0
  238. package/runtime/node_modules/@metro-labs/http/src/api-http.ts +135 -0
  239. package/runtime/node_modules/@metro-labs/{mcp/src/daemon/session-api.ts → http/src/mode-api.ts} +13 -9
  240. package/runtime/node_modules/@metro-labs/http/src/return-to.ts +21 -0
  241. package/runtime/node_modules/@metro-labs/http/src/workos-token.ts +149 -0
  242. package/runtime/node_modules/@metro-labs/{telegram-user → outlook}/package.json +2 -5
  243. package/runtime/node_modules/@metro-labs/outlook/src/accounts.ts +93 -0
  244. package/runtime/node_modules/@metro-labs/outlook/src/actions.ts +14 -0
  245. package/runtime/node_modules/@metro-labs/outlook/src/attachments.ts +97 -0
  246. package/runtime/node_modules/@metro-labs/outlook/src/auth.ts +150 -0
  247. package/runtime/node_modules/@metro-labs/outlook/src/browser.ts +53 -0
  248. package/runtime/node_modules/@metro-labs/outlook/src/config.ts +36 -0
  249. package/runtime/node_modules/@metro-labs/outlook/src/format.ts +140 -0
  250. package/runtime/node_modules/@metro-labs/outlook/src/graph.ts +60 -0
  251. package/runtime/node_modules/@metro-labs/outlook/src/html.ts +72 -0
  252. package/runtime/node_modules/@metro-labs/outlook/src/inbound.ts +104 -0
  253. package/runtime/node_modules/@metro-labs/outlook/src/index.ts +12 -0
  254. package/runtime/node_modules/@metro-labs/outlook/src/login.ts +150 -0
  255. package/runtime/node_modules/@metro-labs/outlook/src/me.ts +41 -0
  256. package/runtime/node_modules/@metro-labs/outlook/src/outbound.ts +116 -0
  257. package/runtime/node_modules/@metro-labs/outlook/src/query.ts +119 -0
  258. package/runtime/node_modules/@metro-labs/outlook/src/state.ts +43 -0
  259. package/runtime/node_modules/@metro-labs/outlook/src/station.ts +15 -0
  260. package/runtime/node_modules/@metro-labs/outlook/src/trust.ts +70 -0
  261. package/runtime/node_modules/@metro-labs/telegram/package.json +3 -2
  262. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +38 -96
  263. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +214 -253
  264. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +67 -108
  265. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/client.ts +6 -6
  266. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +101 -147
  267. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/group.ts +4 -4
  268. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/history.ts +1 -1
  269. package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +57 -0
  270. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +6 -102
  271. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/login.ts +20 -20
  272. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +64 -151
  273. package/runtime/node_modules/@metro-labs/telegram/src/members.ts +61 -33
  274. package/runtime/node_modules/@metro-labs/telegram/src/profile.ts +70 -0
  275. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/reactions.ts +3 -3
  276. package/runtime/node_modules/@metro-labs/telegram/src/station.ts +11 -1
  277. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +5 -47
  278. package/runtime/node_modules/@metro-labs/telegram-bot/package.json +25 -0
  279. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +86 -0
  280. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +240 -0
  281. package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
  282. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +170 -0
  283. package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +103 -0
  284. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +90 -0
  285. package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
  286. package/runtime/node_modules/@metro-labs/telegram-bot/src/profile.ts +85 -0
  287. package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +21 -0
  288. package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +56 -0
  289. package/runtime/node_modules/@metro-labs/threema/package.json +26 -0
  290. package/runtime/node_modules/@metro-labs/threema/src/accounts.ts +95 -0
  291. package/runtime/node_modules/@metro-labs/threema/src/actions.ts +124 -0
  292. package/runtime/node_modules/@metro-labs/threema/src/api.ts +152 -0
  293. package/runtime/node_modules/@metro-labs/threema/src/crypto.ts +100 -0
  294. package/runtime/node_modules/@metro-labs/threema/src/files.ts +47 -0
  295. package/runtime/node_modules/@metro-labs/threema/src/format.ts +164 -0
  296. package/runtime/node_modules/@metro-labs/threema/src/groups.ts +96 -0
  297. package/runtime/node_modules/@metro-labs/threema/src/ids.ts +21 -0
  298. package/runtime/node_modules/@metro-labs/threema/src/inbound.ts +89 -0
  299. package/runtime/node_modules/@metro-labs/threema/src/index.ts +24 -0
  300. package/runtime/node_modules/@metro-labs/threema/src/messages.ts +264 -0
  301. package/runtime/node_modules/@metro-labs/threema/src/outbound.ts +160 -0
  302. package/runtime/node_modules/@metro-labs/threema/src/station.ts +13 -0
  303. package/runtime/node_modules/@metro-labs/threema/src/verify.ts +117 -0
  304. package/runtime/node_modules/@metro-labs/webhook/package.json +1 -1
  305. package/runtime/node_modules/@metro-labs/webhook/src/station.ts +5 -8
  306. package/runtime/node_modules/@metro-labs/whatsapp/package.json +2 -5
  307. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +6 -28
  308. package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +1 -1
  309. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +54 -11
  310. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +2 -2
  311. package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +1 -1
  312. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +86 -4
  313. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +7 -58
  314. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +11 -28
  315. package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +4 -37
  316. package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +1 -1
  317. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +1 -1
  318. package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +1 -1
  319. package/runtime/node_modules/@metro-labs/whatsapp/src/names.ts +75 -0
  320. package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +44 -0
  321. package/runtime/node_modules/@metro-labs/whatsapp/src/resolve.ts +52 -0
  322. package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +16 -1
  323. package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +5 -9
  324. package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +0 -1
  325. package/runtime/node_modules/@metro-labs/xmtp/package.json +3 -2
  326. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +10 -20
  327. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +2 -2
  328. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +15 -204
  329. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +4 -43
  330. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +41 -219
  331. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +3 -3
  332. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +3 -3
  333. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +7 -24
  334. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +8 -18
  335. package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +1 -1
  336. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +2 -2
  337. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +30 -16
  338. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +12 -20
  339. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +1 -19
  340. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +1 -1
  341. package/runtime/node_modules/@metro-labs/xmtp/src/names.ts +61 -0
  342. package/runtime/node_modules/@metro-labs/xmtp/src/profile.ts +153 -0
  343. package/runtime/node_modules/@metro-labs/xmtp/src/sender.ts +112 -0
  344. package/runtime/node_modules/@metro-labs/xmtp/src/smart.ts +72 -0
  345. package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +5 -1
  346. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +4 -51
  347. package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +30 -44
  348. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +4 -6
  349. package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +19 -5
  350. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +14 -4
  351. package/runtime/runtime.json +1 -1
  352. package/runtime/server.ts +1 -0
  353. package/runtime/stations.json +37 -0
  354. package/dist/prompt.js +0 -10
  355. package/dist/store.js +0 -70
  356. package/runtime/node_modules/@metro-labs/discord/src/station.ts +0 -19
  357. package/runtime/node_modules/@metro-labs/discord/src/voice-speak.ts +0 -195
  358. package/runtime/node_modules/@metro-labs/discord/src/voice-transcribe.ts +0 -292
  359. package/runtime/node_modules/@metro-labs/discord/src/voice.ts +0 -197
  360. package/runtime/node_modules/@metro-labs/discord/src/wire.ts +0 -5
  361. package/runtime/node_modules/@metro-labs/mcp/package.json +0 -66
  362. package/runtime/node_modules/@metro-labs/mcp/src/channels/dedupe.ts +0 -24
  363. package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +0 -311
  364. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +0 -335
  365. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +0 -129
  366. package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +0 -322
  367. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +0 -111
  368. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair.ts +0 -20
  369. package/runtime/node_modules/@metro-labs/mcp/src/daemon/collection-api.ts +0 -202
  370. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-json.ts +0 -31
  371. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +0 -100
  372. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +0 -57
  373. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +0 -333
  374. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-auth.ts +0 -164
  375. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-oauth.ts +0 -208
  376. package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +0 -79
  377. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +0 -65
  378. package/runtime/node_modules/@metro-labs/mcp/src/daemon/pair-codes.ts +0 -58
  379. package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +0 -214
  380. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +0 -125
  381. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-pair.ts +0 -18
  382. package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-source.ts +0 -105
  383. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +0 -41
  384. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +0 -218
  385. package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +0 -169
  386. package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +0 -91
  387. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +0 -215
  388. package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +0 -37
  389. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-collections.ts +0 -218
  390. package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +0 -329
  391. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +0 -364
  392. package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +0 -300
  393. package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +0 -31
  394. package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +0 -105
  395. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +0 -131
  396. package/runtime/node_modules/@metro-labs/mcp/src/db/token-carry.ts +0 -45
  397. package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +0 -57
  398. package/runtime/node_modules/@metro-labs/mcp/src/db/whatsapp-login.ts +0 -18
  399. package/runtime/node_modules/@metro-labs/mcp/src/mcp/channel-owner.ts +0 -29
  400. package/runtime/node_modules/@metro-labs/mcp/src/mcp/frame-scope.ts +0 -21
  401. package/runtime/node_modules/@metro-labs/mcp/src/mcp/permission-relay.ts +0 -66
  402. package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +0 -73
  403. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-registry.ts +0 -172
  404. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +0 -43
  405. package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +0 -240
  406. package/runtime/node_modules/@metro-labs/mcp/src/server.ts +0 -2
  407. package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +0 -80
  408. package/runtime/node_modules/@metro-labs/mcp/src/stations/messaging-normalize.ts +0 -88
  409. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +0 -3
  410. package/runtime/node_modules/@metro-labs/telegram-user/src/accounts.ts +0 -81
  411. package/runtime/node_modules/@metro-labs/telegram-user/src/actions.ts +0 -254
  412. package/runtime/node_modules/@metro-labs/telegram-user/src/attachments.ts +0 -87
  413. package/runtime/node_modules/@metro-labs/telegram-user/src/format.ts +0 -141
  414. package/runtime/node_modules/@metro-labs/telegram-user/src/inbound.ts +0 -78
  415. package/runtime/node_modules/@metro-labs/telegram-user/src/index.ts +0 -46
  416. package/runtime/node_modules/@metro-labs/telegram-user/src/media-actions.ts +0 -80
  417. package/runtime/node_modules/@metro-labs/telegram-user/src/members.ts +0 -75
  418. package/runtime/node_modules/@metro-labs/telegram-user/src/normalize.ts +0 -22
  419. package/runtime/node_modules/@metro-labs/telegram-user/src/station.ts +0 -24
  420. package/runtime/node_modules/@metro-labs/telegram-user/src/types.ts +0 -7
  421. package/runtime/node_modules/@metro-labs/telegram-user/src/wire.ts +0 -1
  422. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +0 -22
  423. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +0 -5
  424. package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +0 -87
  425. package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +0 -55
  426. package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +0 -349
  427. /package/runtime/node_modules/@metro-labs/{mcp/src/daemon → core/src}/log.ts +0 -0
  428. /package/runtime/node_modules/@metro-labs/{mcp/src/mcp → core/src}/str.ts +0 -0
  429. /package/runtime/node_modules/@metro-labs/{mcp/src/daemon → core/src}/train-error.ts +0 -0
  430. /package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/channels/webhook-note.ts +0 -0
  431. /package/runtime/node_modules/@metro-labs/{mcp → daemon}/src/mcp/raw-get-stream.ts +0 -0
  432. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/api-base.ts +0 -0
  433. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/verify.ts +0 -0
  434. /package/runtime/node_modules/@metro-labs/{mcp/src/daemon → http/src}/api-error.ts +0 -0
  435. /package/runtime/node_modules/@metro-labs/{mcp/src/daemon → http/src}/cors.ts +0 -0
  436. /package/runtime/node_modules/@metro-labs/{telegram → telegram-bot}/src/verify.ts +0 -0
@@ -0,0 +1,132 @@
1
+ import { parsePolicy } from '../policy/policy.js';
2
+ import type { IncomingMessage, ServerResponse } from 'node:http';
3
+ import { errMsg, log } from '@metro-labs/core/log';
4
+ import { apiFailure, apiSession, cors, readJsonBody, requireAdmin, sendJson } from '@metro-labs/http/api-http';
5
+ import { ApiError } from '@metro-labs/http/api-error';
6
+ import { isRecord } from '@metro-labs/core/is-record';
7
+ import { parseId } from '@metro-labs/core/ids';
8
+ import type { LoadedAccount, LoadedAgent, LoadedConnector } from '../stations/materialize.js';
9
+ import { STATIONS } from '@metro-labs/core/station-names';
10
+ import { approversOf } from './files.js';
11
+
12
+ const AGENTS = '/api/agents';
13
+ const RESTORE_PATH = `${AGENTS}/restore`;
14
+ const BUNDLE_MAX = 2 * 1024 * 1024;
15
+ const STATION_NAMES = new Set<string>(STATIONS);
16
+
17
+ export interface AgentBundle {
18
+ version: 1;
19
+ agent: { id: string; name: string; stations: LoadedAccount[] };
20
+ connectors: LoadedConnector[];
21
+ }
22
+
23
+ export const loadedAgentOf = (bundle: AgentBundle): LoadedAgent => ({
24
+ id: bundle.agent.id,
25
+ name: bundle.agent.name,
26
+ key: null,
27
+ accounts: bundle.agent.stations,
28
+ });
29
+
30
+ interface RestoredAgent {
31
+ id: string;
32
+ name: string;
33
+ stations: number;
34
+ connectors: number;
35
+ }
36
+
37
+ type ImportMode = 'append' | 'overwrite';
38
+
39
+ export interface BundleApiDeps {
40
+ bundle: (agentId: string) => Promise<AgentBundle>;
41
+ restore: (bundle: AgentBundle, mode: ImportMode) => Promise<RestoredAgent>;
42
+ }
43
+
44
+ function parseMode(raw: unknown): ImportMode {
45
+ const mode = isRecord(raw) && typeof raw.mode === 'string' ? raw.mode : 'overwrite';
46
+ if (mode !== 'append' && mode !== 'overwrite') throw bad("mode is 'append' or 'overwrite'");
47
+ return mode;
48
+ }
49
+
50
+ const bad = (what: string): ApiError => new ApiError(`bundle: ${what}`, 400);
51
+
52
+ function stationOf(raw: unknown): LoadedAccount {
53
+ if (!isRecord(raw) || typeof raw.station !== 'string' || !STATION_NAMES.has(raw.station))
54
+ throw bad('a station is not a known station');
55
+ if (typeof raw.id !== 'string' || !isRecord(raw.config)) throw bad('a station has no id or config');
56
+ const allowlist = Array.isArray(raw.allowlist)
57
+ ? raw.allowlist.filter((s): s is string => typeof s === 'string')
58
+ : null;
59
+ const policy = parsePolicy(raw.policy, `bundle ${raw.station}/${raw.id}`);
60
+ return {
61
+ station: raw.station as LoadedAccount['station'],
62
+ id: raw.id,
63
+ allowlist,
64
+ ...approversOf(raw.approvers),
65
+ enabled: raw.enabled !== false,
66
+ ...(policy === undefined ? {} : { policy }),
67
+ config: raw.config,
68
+ };
69
+ }
70
+
71
+ function connectorOf(raw: unknown): LoadedConnector {
72
+ if (!isRecord(raw) || typeof raw.id !== 'string' || typeof raw.name !== 'string' || typeof raw.url !== 'string')
73
+ throw bad('a connector is missing its id, name or url');
74
+ if (!isRecord(raw.config)) throw bad('a connector has no config');
75
+ return { id: raw.id, name: raw.name, url: raw.url, config: raw.config };
76
+ }
77
+
78
+ function agentOf(raw: unknown): AgentBundle['agent'] {
79
+ if (!isRecord(raw)) throw bad('not a v1 agent bundle');
80
+ const { id, name, stations } = raw;
81
+ if (typeof id !== 'string' || parseId(id) === null) throw bad('agent id is not an id');
82
+ if (!Array.isArray(stations)) throw bad('agent has no station list');
83
+ return { id, name: typeof name === 'string' ? name : '', stations: stations.map(stationOf) };
84
+ }
85
+
86
+ export function parseBundle(raw: unknown): AgentBundle {
87
+ if (!isRecord(raw) || raw.version !== 1) throw bad('not a v1 agent bundle');
88
+ const connectors = Array.isArray(raw.connectors) ? raw.connectors : [];
89
+ return { version: 1, agent: agentOf(raw.agent), connectors: connectors.map(connectorOf) };
90
+ }
91
+
92
+ async function answer(req: IncomingMessage, deps: BundleApiDeps, path: string): Promise<unknown> {
93
+ if (path === RESTORE_PATH) {
94
+ const body: unknown = await readJsonBody(req, BUNDLE_MAX);
95
+ return deps.restore(parseBundle(body), parseMode(body));
96
+ }
97
+ const id = parseId(path.slice(AGENTS.length + 1).split('/')[0] ?? '');
98
+ if (id === null) throw new ApiError('no such agent', 404);
99
+ return deps.bundle(id);
100
+ }
101
+
102
+ export function handleBundleRequest(req: IncomingMessage, res: ServerResponse, deps: BundleApiDeps): boolean {
103
+ const path = (req.url ?? '').split('?')[0] ?? '';
104
+ const isRestore = path === RESTORE_PATH;
105
+ const isBundle = /^\/api\/agents\/[^/]+\/bundle$/.test(path);
106
+ if (!isRestore && !isBundle) return false;
107
+ if (req.method === 'OPTIONS') {
108
+ res.writeHead(204, cors(req)).end();
109
+ return true;
110
+ }
111
+ if (req.method !== (isRestore ? 'POST' : 'GET')) {
112
+ sendJson(req, res, 405, { error: 'method not allowed' });
113
+ return true;
114
+ }
115
+ apiSession(req)
116
+ .then((session) => {
117
+ if (!session) throw new ApiError('unauthorized', 401);
118
+ requireAdmin(session);
119
+ return answer(req, deps, path);
120
+ })
121
+ .then((body) => {
122
+ sendJson(req, res, isRestore ? 201 : 200, body);
123
+ })
124
+ .catch((err: unknown) => {
125
+ if (err instanceof ApiError) apiFailure(req, res, err, 'bundle-api');
126
+ else {
127
+ log.warn({ err: errMsg(err) }, 'bundle-api: request failed');
128
+ if (!res.headersSent) sendJson(req, res, 500, { error: 'bundle api failed' });
129
+ }
130
+ });
131
+ return true;
132
+ }
@@ -0,0 +1,305 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { ApiError } from '@metro-labs/http/api-error';
4
+ import { isOrganizationId } from '@metro-labs/http/workos-token';
5
+ import { ensureSecureDir, writeSecure } from '@metro-labs/core/secure-fs';
6
+ import {
7
+ newApiKey,
8
+ AgentAdminError,
9
+ normalizeAgentName,
10
+ type AgentSummary,
11
+ type CreatedAgent,
12
+ } from './admin.js';
13
+ import type { AccountRef } from './account-attach.js';
14
+ import {
15
+ agentFilePath,
16
+ AgentFileError,
17
+ agentsDir,
18
+ listAgentFiles,
19
+ parseAgentFile,
20
+ readAgentFile,
21
+ type AgentFile,
22
+ } from './files.js';
23
+ import { newId } from '@metro-labs/core/ids';
24
+ import { registerKey } from './keys.js';
25
+ import { MOVABLE_STATIONS, type LoadedAgent } from '../stations/materialize.js';
26
+ import type { StationName } from '@metro-labs/core/station-names';
27
+ import type { ToolPolicy } from '../policy/policy.js';
28
+
29
+ const OWNER_FILE = '.owner';
30
+
31
+ interface Stored {
32
+ path: string;
33
+ file: AgentFile;
34
+ }
35
+
36
+ const missing = (): AgentAdminError => new AgentAdminError('no such agent', 404);
37
+
38
+ function parseOwner(raw: string): string | null {
39
+ const text = raw.trim();
40
+ return isOrganizationId(text) ? text : null;
41
+ }
42
+
43
+ export function localOwner(dir = agentsDir()): string | null {
44
+ try {
45
+ return parseOwner(readFileSync(join(dir, OWNER_FILE), 'utf8'));
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ export function setLocalOwner(raw: string, dir = agentsDir()): string {
52
+ const owner = parseOwner(raw);
53
+ if (owner === null) throw new ApiError(`'${raw}' is not an organization id`, 400);
54
+ ensureSecureDir(dir);
55
+ writeSecure(join(dir, OWNER_FILE), `${owner}\n`);
56
+ return owner;
57
+ }
58
+
59
+ function storedAgents(dir: string): Stored[] {
60
+ return listAgentFiles(dir).map((path) => ({ path, file: readAgentFile(path) }));
61
+ }
62
+
63
+ function save(stored: Stored): void {
64
+ writeSecure(stored.path, `${JSON.stringify(stored.file, null, 2)}\n`);
65
+ }
66
+
67
+ function agentOrThrow(id: string, dir: string): Stored {
68
+ const found = storedAgents(dir).find((s) => s.file.id === id);
69
+ if (found === undefined) throw missing();
70
+ return found;
71
+ }
72
+
73
+ export async function localListAgents(dir = agentsDir()): Promise<AgentSummary[]> {
74
+ return Promise.resolve(storedAgents(dir).map(({ file }) => ({ id: file.id, name: file.name })));
75
+ }
76
+
77
+ function freshId(taken: Set<string>): string {
78
+ for (let attempt = 0; attempt < 5; attempt += 1) {
79
+ const id = newId();
80
+ if (!taken.has(id)) return id;
81
+ }
82
+ throw new AgentAdminError('could not allocate a free id', 500);
83
+ }
84
+
85
+ export async function localCreateAgent(rawName?: string, dir = agentsDir()): Promise<CreatedAgent> {
86
+ const name = rawName === undefined ? null : normalizeAgentName(rawName);
87
+ const existing = storedAgents(dir);
88
+ if (existing.length > 0) throw new AgentAdminError('this box already has its agent', 409);
89
+ const id = freshId(new Set());
90
+ const key = newApiKey();
91
+ ensureSecureDir(dir);
92
+ save({
93
+ path: agentFilePath(dir),
94
+ file: { version: 1, id, name, key, stations: [] },
95
+ });
96
+ registerKey(key, id);
97
+ return Promise.resolve({ id, name, key });
98
+ }
99
+
100
+ export type Ensured = 'created' | 'present' | 'no-owner';
101
+
102
+ export async function ensureLocalAgent(dir = agentsDir()): Promise<Ensured> {
103
+ if (storedAgents(dir).length > 0) return 'present';
104
+ if (localOwner(dir) === null) return 'no-owner';
105
+ await localCreateAgent(undefined, dir);
106
+ return 'created';
107
+ }
108
+
109
+ function assertUnclaimed(existing: Stored[], agent: LoadedAgent): void {
110
+ for (const { file } of existing) {
111
+ if (file.id === agent.id)
112
+ throw new AgentAdminError(`agent ${agent.id} is already on this machine`, 409);
113
+ if (agent.key !== null && file.key === agent.key)
114
+ throw new AgentAdminError('that agent key is already on this machine', 409);
115
+ }
116
+ }
117
+
118
+ function assertImportable(agent: LoadedAgent): void {
119
+ const immovable = agent.accounts.find((a) => !MOVABLE_STATIONS.has(a.station));
120
+ if (immovable !== undefined)
121
+ throw new AgentAdminError(
122
+ `a ${immovable.station} endpoint needs a public url and cannot live on a local daemon`,
123
+ 400,
124
+ );
125
+ }
126
+
127
+ interface ImportTarget {
128
+ path: string;
129
+ previous: AgentFile | undefined;
130
+ }
131
+
132
+ function importTarget(dir: string, agent: LoadedAgent): ImportTarget {
133
+ const existing = storedAgents(dir);
134
+ const same = existing.find((s) => s.file.id === agent.id);
135
+ if (same === undefined && existing.length > 0)
136
+ throw new AgentAdminError('this box already has its agent; import into it rather than bringing another', 409);
137
+ assertUnclaimed(existing.filter((s) => s !== same), agent);
138
+ return { path: same?.path ?? agentFilePath(dir), previous: same?.file };
139
+ }
140
+
141
+ export type ImportMode = 'append' | 'overwrite';
142
+
143
+ export async function localImportAgent(
144
+ agent: LoadedAgent,
145
+ dir = agentsDir(),
146
+ mode: ImportMode = 'overwrite',
147
+ ): Promise<{ id: string; name: string; key: string; stations: number }> {
148
+ assertImportable(agent);
149
+ const { path, previous } = importTarget(dir, agent);
150
+ const key = previous?.key ?? agent.key ?? newApiKey();
151
+ const file = fileFor({ ...agent, key }, path, previous, mode);
152
+ ensureSecureDir(join(path, '..'));
153
+ save({ path, file });
154
+ registerKey(key, agent.id);
155
+ return Promise.resolve({
156
+ id: agent.id,
157
+ name: agent.name,
158
+ key,
159
+ stations: file.stations.length,
160
+ });
161
+ }
162
+
163
+ function mergedStations(
164
+ agent: LoadedAgent,
165
+ previous: AgentFile | undefined,
166
+ mode: ImportMode,
167
+ ): AgentFile['stations'] {
168
+ const before = previous?.stations ?? [];
169
+ const here = (station: string, id: string): boolean => before.some((s) => s.station === station && s.id === id);
170
+ const fromMetro = agent.accounts
171
+ .map((a) => ({ ...a, allowlist: a.allowlist ?? ['*'] }))
172
+ .filter((m) => mode === 'overwrite' || !here(m.station, m.id));
173
+ const kept = before.filter((s) => !fromMetro.some((m) => m.station === s.station && m.id === s.id));
174
+ return [...fromMetro, ...kept];
175
+ }
176
+
177
+ function fileFor(
178
+ agent: LoadedAgent,
179
+ path: string,
180
+ previous: AgentFile | undefined,
181
+ mode: ImportMode,
182
+ ): AgentFile {
183
+ try {
184
+ return parseAgentFile(
185
+ JSON.stringify({
186
+ version: 1,
187
+ id: agent.id,
188
+ name: previous?.name ?? null,
189
+ key: agent.key,
190
+ stations: mergedStations(agent, previous, mode),
191
+ }),
192
+ path,
193
+ );
194
+ } catch (err) {
195
+ if (err instanceof AgentFileError)
196
+ throw new AgentAdminError(`metro's copy of this agent cannot be written here: ${err.message}`, 400);
197
+ throw err;
198
+ }
199
+ }
200
+
201
+ export function readLocalAgentFile(agentId: string, dir = agentsDir()): AgentFile {
202
+ const found = storedAgents(dir).find((s) => s.file.id === agentId);
203
+ if (found === undefined) throw missing();
204
+ return found.file;
205
+ }
206
+
207
+ const UNIQUE_CREDENTIALS: Record<string, string> = {
208
+ token: 'that bot token is already attached to an agent on this machine',
209
+ accountEmail: 'that mailbox is already connected to an agent on this machine',
210
+ };
211
+
212
+ function assertCredentialFree(all: Stored[], station: StationName, config: Record<string, unknown>): void {
213
+ for (const [key, refusal] of Object.entries(UNIQUE_CREDENTIALS)) {
214
+ const value = config[key];
215
+ if (typeof value !== 'string') continue;
216
+ const taken = all.some((s) => s.file.stations.some((a) => a.station === station && a.config[key] === value));
217
+ if (taken) throw new AgentAdminError(refusal, 409);
218
+ }
219
+ }
220
+
221
+ export async function localAttachAccount(
222
+ agentId: string,
223
+ station: StationName,
224
+ config: Record<string, unknown>,
225
+ dir = agentsDir(),
226
+ ): Promise<AccountRef> {
227
+ const stored = agentOrThrow(agentId, dir);
228
+ if (!MOVABLE_STATIONS.has(station))
229
+ throw new AgentAdminError(
230
+ `a ${station} endpoint needs a public url and cannot live on a local daemon`,
231
+ 400,
232
+ );
233
+ assertCredentialFree(storedAgents(dir), station, config);
234
+ const taken = new Set(stored.file.stations.map((a) => a.id));
235
+ const accountId = freshId(taken);
236
+ stored.file.stations.push({ station, id: accountId, allowlist: ['*'], enabled: true, config });
237
+ save(stored);
238
+ return Promise.resolve({ agentId, station, accountId });
239
+ }
240
+
241
+ export async function localSetAllowlist(
242
+ agentId: string,
243
+ station: StationName,
244
+ accountId: string,
245
+ allowlist: string[],
246
+ approvers: string[] = [],
247
+ dir = agentsDir(),
248
+ ): Promise<string[]> {
249
+ const stored = agentOrThrow(agentId, dir);
250
+ const account = stored.file.stations.find((a) => a.station === station && a.id === accountId);
251
+ if (account === undefined) throw new AgentAdminError('no such account on this agent', 404);
252
+ account.allowlist = allowlist;
253
+ if (approvers.length === 0) delete account.approvers;
254
+ else account.approvers = approvers;
255
+ save(stored);
256
+ return Promise.resolve(allowlist);
257
+ }
258
+
259
+ export async function localSetPolicy(
260
+ agentId: string,
261
+ station: StationName,
262
+ accountId: string,
263
+ policy: ToolPolicy,
264
+ dir = agentsDir(),
265
+ ): Promise<ToolPolicy> {
266
+ const stored = agentOrThrow(agentId, dir);
267
+ const account = stored.file.stations.find((a) => a.station === station && a.id === accountId);
268
+ if (account === undefined) throw new AgentAdminError('no such account on this agent', 404);
269
+ if (policy.read === undefined && policy.write === undefined && policy.tools === undefined) delete account.policy;
270
+ else account.policy = policy;
271
+ save(stored);
272
+ return Promise.resolve(policy);
273
+ }
274
+
275
+ export async function localSetAccountEnabled(
276
+ agentId: string,
277
+ station: StationName,
278
+ accountId: string,
279
+ enabled: boolean,
280
+ dir = agentsDir(),
281
+ ): Promise<boolean> {
282
+ const stored = agentOrThrow(agentId, dir);
283
+ const account = stored.file.stations.find((a) => a.station === station && a.id === accountId);
284
+ if (account === undefined) throw new AgentAdminError('no such account on this agent', 404);
285
+ account.enabled = enabled;
286
+ save(stored);
287
+ return Promise.resolve(enabled);
288
+ }
289
+
290
+ export async function localDetachAccount(
291
+ agentId: string,
292
+ station: StationName,
293
+ accountId: string,
294
+ dir = agentsDir(),
295
+ ): Promise<AccountRef> {
296
+ const stored = agentOrThrow(agentId, dir);
297
+ const before = stored.file.stations.length;
298
+ stored.file.stations = stored.file.stations.filter(
299
+ (a) => !(a.station === station && a.id === accountId),
300
+ );
301
+ if (stored.file.stations.length === before)
302
+ throw new AgentAdminError('no such account on this agent', 404);
303
+ save(stored);
304
+ return Promise.resolve({ agentId, station, accountId });
305
+ }
@@ -0,0 +1,142 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { AGENT_NAME_RE, ID_RE } from '@metro-labs/core/ids';
5
+ import { STATIONS, type StationName } from '@metro-labs/core/station-names';
6
+ import type { LoadedAccount, LoadedAgent, StationSource } from '../stations/materialize.js';
7
+ import { isRecord } from '@metro-labs/core/is-record';
8
+ import { parsePolicy } from '../policy/policy.js';
9
+
10
+ const AGENT_FILE = 'agent.json';
11
+
12
+ const KEY_RE = /^[A-Za-z0-9_-]{16,128}$/;
13
+ const STATION_NAMES = new Set<string>(STATIONS);
14
+
15
+ export class AgentFileError extends Error {}
16
+
17
+ export interface AgentFile {
18
+ version: 1;
19
+ id: string;
20
+ name: string | null;
21
+ key: string | null;
22
+ stations: LoadedAccount[];
23
+ }
24
+
25
+ export const agentFilePath = (dir = agentsDir()): string => join(dir, AGENT_FILE);
26
+
27
+ export function agentsDir(): string {
28
+ const explicit = process.env.METRO_AGENTS_DIR?.trim();
29
+ return explicit !== undefined && explicit !== ''
30
+ ? explicit
31
+ : join(homedir(), '.metro', 'agents');
32
+ }
33
+
34
+ function fail(path: string, reason: string): never {
35
+ throw new AgentFileError(`${path}: ${reason}`);
36
+ }
37
+
38
+ function isStationName(value: unknown): value is StationName {
39
+ return typeof value === 'string' && STATION_NAMES.has(value);
40
+ }
41
+
42
+ function allowlistOf(raw: unknown, path: string, where: string): string[] | null {
43
+ if (raw === null || raw === undefined) return null;
44
+ if (!Array.isArray(raw)) fail(path, `${where}.allowlist is not a list of strings`);
45
+ const list = raw.filter((entry): entry is string => typeof entry === 'string');
46
+ if (list.length !== raw.length)
47
+ fail(path, `${where}.allowlist is not a list of strings`);
48
+ return list;
49
+ }
50
+
51
+ export function approversOf(raw: unknown): { approvers?: string[] } {
52
+ if (!Array.isArray(raw)) return {};
53
+ const list = raw.filter((entry): entry is string => typeof entry === 'string' && entry !== '*' && entry.trim() !== '');
54
+ return list.length === 0 ? {} : { approvers: list };
55
+ }
56
+
57
+ function stationOf(raw: unknown, path: string, index: number): LoadedAccount {
58
+ const where = `stations[${String(index)}]`;
59
+ if (!isRecord(raw)) fail(path, `${where} is not an object`);
60
+ const { station, id, config } = raw;
61
+ if (!isStationName(station)) fail(path, `${where}.station is not a known station`);
62
+ if (typeof id !== 'string' || !ID_RE.test(id))
63
+ fail(path, `${where}.id is not an 11-character id`);
64
+ if (!isRecord(config)) fail(path, `${where}.config is not an object`);
65
+ const policy = parsePolicy(raw.policy, `${path} ${where}`);
66
+ return {
67
+ station,
68
+ id,
69
+ allowlist: allowlistOf(raw.allowlist, path, where),
70
+ ...approversOf(raw.approvers),
71
+ enabled: raw.enabled !== false,
72
+ ...(policy === undefined ? {} : { policy }),
73
+ config,
74
+ };
75
+ }
76
+
77
+ function optionalMatch(
78
+ value: unknown,
79
+ re: RegExp,
80
+ path: string,
81
+ what: string,
82
+ ): string | null {
83
+ if (value === null || value === undefined) return null;
84
+ if (typeof value !== 'string' || !re.test(value)) fail(path, what);
85
+ return value;
86
+ }
87
+
88
+ function nameOf(raw: unknown, path: string): string | null {
89
+ if (raw === undefined || raw === null) return null;
90
+ if (typeof raw !== 'string' || !AGENT_NAME_RE.test(raw)) fail(path, 'name is not a valid agent name');
91
+ return raw;
92
+ }
93
+
94
+ export function parseAgentFile(raw: string, path: string): AgentFile {
95
+ let parsed: unknown;
96
+ try {
97
+ parsed = JSON.parse(raw);
98
+ } catch {
99
+ fail(path, 'is not valid JSON');
100
+ }
101
+ if (!isRecord(parsed)) fail(path, 'is not a JSON object');
102
+ if (parsed.version !== 1) fail(path, 'version must be 1');
103
+ const { id, name, stations } = parsed;
104
+ if (typeof id !== 'string' || !ID_RE.test(id))
105
+ fail(path, 'id is not an 11-character id');
106
+ const label = nameOf(name, path);
107
+ const key = optionalMatch(parsed.key, KEY_RE, path, 'key is not an agent key');
108
+ if (!Array.isArray(stations)) fail(path, 'stations is not a list');
109
+ return {
110
+ version: 1,
111
+ id,
112
+ name: label,
113
+ key,
114
+ stations: stations.map((s, i) => stationOf(s, path, i)),
115
+ };
116
+ }
117
+
118
+ export function readAgentFile(path: string): AgentFile {
119
+ let raw: string;
120
+ try {
121
+ raw = readFileSync(path, 'utf8');
122
+ } catch {
123
+ fail(path, 'cannot be read');
124
+ }
125
+ return parseAgentFile(raw, path);
126
+ }
127
+
128
+ export function listAgentFiles(dir = agentsDir()): string[] {
129
+ const fixed = agentFilePath(dir);
130
+ return existsSync(fixed) ? [fixed] : [];
131
+ }
132
+
133
+ export function loadFileAgents(dir = agentsDir()): LoadedAgent[] {
134
+ return listAgentFiles(dir).map(readAgentFile).map((agent) => ({
135
+ id: agent.id,
136
+ name: agent.name ?? agent.id,
137
+ key: agent.key,
138
+ accounts: agent.stations,
139
+ }));
140
+ }
141
+
142
+ export const fileSource: StationSource = () => Promise.resolve(loadFileAgents());
@@ -1,5 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { ID_RE } from './ids.js';
2
+ import { ID_RE } from '@metro-labs/core/ids';
3
3
 
4
4
  const digest = (token: string): string =>
5
5
  createHash('sha256').update(token).digest('hex');
@@ -25,16 +25,6 @@ export function registerKey(key: string, agentId: string): void {
25
25
  agentIdByKeyHash.set(digest(key), agentId);
26
26
  }
27
27
 
28
- export function unregisterAgentKey(agentId: string): void {
29
- for (const [hash, id] of agentIdByKeyHash)
30
- if (id === agentId) agentIdByKeyHash.delete(hash);
31
- }
32
-
33
- export function rotateAgentKey(agentId: string, key: string | null): void {
34
- unregisterAgentKey(agentId);
35
- if (key !== null) registerKey(key, agentId);
36
- }
37
-
38
28
  export function agentIdForKey(token: string): string | undefined {
39
29
  if (token === '') return undefined;
40
30
  return agentIdByKeyHash.get(digest(token));
@@ -8,6 +8,8 @@ const mapKey = (station: string, accountId: string): string =>
8
8
  let agentMap: AgentMap = {};
9
9
  let agentNames: AgentNameMap = {};
10
10
  let allowlistMap: AllowlistMap = {};
11
+ let approversMap: AllowlistMap = {};
12
+ let disabledAccounts = new Set<string>();
11
13
 
12
14
  export function setAgentMap(map: AgentMap, names: AgentNameMap): void {
13
15
  agentMap = map;
@@ -18,6 +20,28 @@ export function setAllowlistMap(map: AllowlistMap): void {
18
20
  allowlistMap = map;
19
21
  }
20
22
 
23
+ export function setApproversMap(map: AllowlistMap): void {
24
+ approversMap = map;
25
+ }
26
+
27
+ export const approversForAccount = (station: string, accountId: string): string[] => approversMap[mapKey(station, accountId)] ?? [];
28
+
29
+ export function approversForLine(line: string): string[] {
30
+ const a = accountFromLine(line);
31
+ return a ? approversForAccount(a.station, a.accountId) : [];
32
+ }
33
+
34
+ export const mayApprove = (line: string, from: string): boolean => {
35
+ const approvers = approversForLine(line);
36
+ return approvers.length > 0 && senderMatchesAllowlist(approvers, from);
37
+ };
38
+
39
+ export function setDisabledAccounts(ids: Set<string>): void {
40
+ disabledAccounts = ids;
41
+ }
42
+
43
+ export const accountEnabled = (station: string, accountId: string): boolean => !disabledAccounts.has(mapKey(station, accountId));
44
+
21
45
  export function accountFromLine(
22
46
  line: string,
23
47
  ): { station: string; accountId: string } | undefined {
@@ -45,6 +69,19 @@ export function agentForLine(line: string): string | undefined {
45
69
  return id === undefined ? undefined : agentNames[id];
46
70
  }
47
71
 
72
+ export interface KnownAccount {
73
+ station: string;
74
+ id: string;
75
+ agentId: string;
76
+ }
77
+
78
+ export function knownAccounts(): KnownAccount[] {
79
+ return Object.entries(agentMap).flatMap(([key, agentId]) => {
80
+ const cut = key.indexOf('/');
81
+ return cut === -1 ? [] : [{ station: key.slice(0, cut), id: key.slice(cut + 1), agentId }];
82
+ });
83
+ }
84
+
48
85
  export function stationAgentIds(station: string): string[] {
49
86
  const prefix = `${station}/`;
50
87
  return Object.entries(agentMap)
@@ -52,11 +89,24 @@ export function stationAgentIds(station: string): string[] {
52
89
  .map(([, id]) => id);
53
90
  }
54
91
 
92
+ export function allowlistForAccount(station: string, accountId: string): string[] | undefined {
93
+ return allowlistMap[mapKey(station, accountId)];
94
+ }
95
+
55
96
  export function allowlistForLine(line: string): string[] | undefined {
56
97
  const a = accountFromLine(line);
57
98
  return a ? allowlistMap[mapKey(a.station, a.accountId)] : undefined;
58
99
  }
59
100
 
101
+ export function senderPermitted(
102
+ allowlist: string[] | undefined,
103
+ from: string,
104
+ verified?: boolean,
105
+ ): boolean {
106
+ if (allowlist === undefined || allowlist.length === 0 || allowlist.includes('*')) return true;
107
+ return verified !== false && senderMatchesAllowlist(allowlist, from);
108
+ }
109
+
60
110
  export function senderMatchesAllowlist(
61
111
  allowlist: string[],
62
112
  from: string,
@@ -66,6 +116,13 @@ export function senderMatchesAllowlist(
66
116
  const id = f.split('/').pop() ?? f;
67
117
  return allowlist.some((a) => {
68
118
  const v = a.toLowerCase();
69
- return v === f || v === id;
119
+ return v === f || v === id || domainMatches(v, id);
70
120
  });
71
121
  }
122
+
123
+ const EMAIL_RE = /^[^@\s/]+@([^@\s/]+)$/;
124
+
125
+ function domainMatches(entry: string, id: string): boolean {
126
+ if (!entry.startsWith('@') || entry.length < 2) return false;
127
+ return EMAIL_RE.exec(id)?.[1] === entry.slice(1);
128
+ }