@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,29 @@
1
+ import { eventInScope } from '../db/agent-scope.js';
2
+ import { allowedAgents, type RequestIdentity } from './request-identity.js';
3
+ import { sessionScopeKey } from './session-route.js';
4
+
5
+ export class ChannelOwner {
6
+ private stream: RequestIdentity | undefined;
7
+
8
+ bindStream(identity: RequestIdentity): void {
9
+ this.stream = identity;
10
+ }
11
+
12
+ releaseStream(): void {
13
+ this.stream = undefined;
14
+ }
15
+
16
+ scope(): Set<string> {
17
+ return allowedAgents(this.stream);
18
+ }
19
+
20
+ streamBelongsTo(identity: RequestIdentity): boolean {
21
+ if (this.stream === undefined) return false;
22
+ return sessionScopeKey(this.stream) === sessionScopeKey(identity);
23
+ }
24
+
25
+ inScope(line: string): boolean {
26
+ if (this.stream === undefined) return false;
27
+ return eventInScope(this.scope(), line);
28
+ }
29
+ }
@@ -0,0 +1,62 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { forwardTrainCall } from '../daemon/train-call.js';
3
+ import { errMsg, log } from '../daemon/log.js';
4
+ import { TrainError } from '../daemon/train-error.js';
5
+ import { type ToolContext, type ToolResult } from '../stations/types.js';
6
+
7
+ export async function metroCall(
8
+ train: string,
9
+ action: string,
10
+ args: Record<string, unknown>,
11
+ ): Promise<{ result: unknown }> {
12
+ const line = typeof args.line === 'string' ? args.line : undefined;
13
+ log.info({ train, action, line }, 'metro: dispatching train call');
14
+ let response;
15
+ try {
16
+ response = await forwardTrainCall(train, action, args);
17
+ } catch (err) {
18
+ log.warn(
19
+ { train, action, line, err: errMsg(err) },
20
+ 'metro: train call failed to dispatch',
21
+ );
22
+ throw new TrainError(
23
+ 'metro_call_failed',
24
+ `metro ${action} ${train}: ${errMsg(err)}`,
25
+ );
26
+ }
27
+ if (response.error) {
28
+ log.warn(
29
+ { train, action, line, err: response.error },
30
+ 'metro: train call returned error',
31
+ );
32
+ throw new TrainError(
33
+ 'metro_call_error',
34
+ `metro ${action} ${train}: ${response.error}`,
35
+ );
36
+ }
37
+ log.info({ train, action, line }, 'metro: train call ok');
38
+ return { result: response.result ?? null };
39
+ }
40
+
41
+ export const ok = (text: string): ToolResult => ({
42
+ content: [{ type: 'text', text }],
43
+ });
44
+ export const okJson = (v: unknown): ToolResult => ({
45
+ content: [{ type: 'text', text: JSON.stringify(v, null, 2) }],
46
+ });
47
+ export const errResult = (text: string): ToolResult => ({
48
+ content: [{ type: 'text', text }],
49
+ isError: true,
50
+ });
51
+ export const toErr = (name: string, e: unknown): ToolResult =>
52
+ e instanceof TrainError
53
+ ? errResult(e.message)
54
+ : errResult(`metro ${name} failed: ${String(e)}`);
55
+
56
+ export const makeCtx = (station: string): ToolContext => ({
57
+ call: (action, args) => metroCall(station, action, args),
58
+ ok,
59
+ okJson,
60
+ err: errResult,
61
+ readFile: (path) => readFile(path),
62
+ });
@@ -0,0 +1,91 @@
1
+ import type {
2
+ EventStore,
3
+ EventId,
4
+ StreamId,
5
+ } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
6
+ import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
7
+ import { frameInScope, frameLine } from './frame-scope.js';
8
+
9
+ export const EVENT_STORE_MAX = 500;
10
+
11
+ interface StoredEvent {
12
+ eventId: EventId;
13
+ streamId: StreamId;
14
+ message: JSONRPCMessage;
15
+ line: string | undefined;
16
+ owner: Set<string>;
17
+ }
18
+
19
+ export interface EventStoreDeps {
20
+ scopeOf: () => Set<string>;
21
+ max?: number;
22
+ }
23
+
24
+ export interface ScopedReplay {
25
+ send: (eventId: EventId, message: JSONRPCMessage) => Promise<void>;
26
+ scope: Set<string> | undefined;
27
+ onWithheld?: (eventId: EventId, line: string | undefined) => void;
28
+ }
29
+
30
+ const SEP = '_';
31
+
32
+ const encodeEventId = (streamId: StreamId, seq: number): EventId =>
33
+ `${streamId}${SEP}${seq}`;
34
+
35
+ const decodeStreamId = (eventId: EventId): StreamId | undefined => {
36
+ const idx = eventId.lastIndexOf(SEP);
37
+ if (idx <= 0) return undefined;
38
+ return eventId.slice(0, idx);
39
+ };
40
+
41
+ export class BoundedEventStore implements EventStore {
42
+ private readonly max: number;
43
+ private readonly scopeOf: () => Set<string>;
44
+ private readonly events: StoredEvent[] = [];
45
+ private seq = 0;
46
+
47
+ constructor(deps: EventStoreDeps) {
48
+ this.scopeOf = deps.scopeOf;
49
+ this.max = deps.max ?? EVENT_STORE_MAX;
50
+ }
51
+
52
+ storeEvent(streamId: StreamId, message: JSONRPCMessage): Promise<EventId> {
53
+ const eventId = encodeEventId(streamId, ++this.seq);
54
+ this.events.push({
55
+ eventId,
56
+ streamId,
57
+ message,
58
+ line: frameLine(message),
59
+ owner: new Set(this.scopeOf()),
60
+ });
61
+ if (this.events.length > this.max) this.events.shift();
62
+ return Promise.resolve(eventId);
63
+ }
64
+
65
+ getStreamIdForEventId(eventId: EventId): Promise<StreamId | undefined> {
66
+ return Promise.resolve(decodeStreamId(eventId));
67
+ }
68
+
69
+ async replayEventsAfter(
70
+ lastEventId: EventId,
71
+ { send, scope, onWithheld }: ScopedReplay,
72
+ ): Promise<StreamId> {
73
+ const streamId = decodeStreamId(lastEventId);
74
+ if (streamId === undefined) return '';
75
+ const allowed = scope ?? new Set<string>();
76
+ let seen = false;
77
+ for (const e of this.events) {
78
+ if (e.streamId !== streamId) continue;
79
+ if (!seen) {
80
+ if (e.eventId === lastEventId) seen = true;
81
+ continue;
82
+ }
83
+ if (!frameInScope(allowed, e)) {
84
+ onWithheld?.(e.eventId, e.line);
85
+ continue;
86
+ }
87
+ await send(e.eventId, e.message);
88
+ }
89
+ return streamId;
90
+ }
91
+ }
@@ -0,0 +1,21 @@
1
+ import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
2
+ import { eventInScope } from '../db/agent-scope.js';
3
+ import { str } from './str.js';
4
+
5
+ export interface FrameOrigin {
6
+ line: string | undefined;
7
+ owner: Set<string>;
8
+ }
9
+
10
+ export function frameLine(message: JSONRPCMessage): string | undefined {
11
+ const params: unknown = (message as { params?: unknown }).params;
12
+ if (typeof params !== 'object' || params === null) return undefined;
13
+ const meta: unknown = (params as { meta?: unknown }).meta;
14
+ if (typeof meta !== 'object' || meta === null) return undefined;
15
+ return str((meta as { line?: unknown }).line) || undefined;
16
+ }
17
+
18
+ export function frameInScope(scope: Set<string>, origin: FrameOrigin): boolean {
19
+ if (origin.line !== undefined) return eventInScope(scope, origin.line);
20
+ return origin.owner.size > 0 && [...origin.owner].every((id) => scope.has(id));
21
+ }
@@ -0,0 +1,129 @@
1
+ import { stationByName, stationForLine } from '../stations/registry.js';
2
+ import type {
3
+ GroupOp,
4
+ GroupResult,
5
+ MemberOutcome,
6
+ Station,
7
+ ToolResult,
8
+ } from '../stations/types.js';
9
+ import { makeCtx, okJson, toErr } from './ctx.js';
10
+ import { str } from './str.js';
11
+
12
+ export interface GroupOpResult {
13
+ op: GroupOp;
14
+ line: string;
15
+ station: string;
16
+ supported: boolean;
17
+ reason?: string;
18
+ id?: string;
19
+ name?: string;
20
+ members: MemberOutcome[];
21
+ inviteLink?: string;
22
+ }
23
+
24
+ export function wrapGroupResult(
25
+ op: GroupOp,
26
+ line: string,
27
+ station: string,
28
+ r: GroupResult,
29
+ ): GroupOpResult {
30
+ const out: GroupOpResult = {
31
+ op,
32
+ line: r.line ?? line,
33
+ station,
34
+ supported: r.capability.supported,
35
+ members: Array.isArray(r.members) ? r.members : [],
36
+ };
37
+ if (r.capability.reason) out.reason = r.capability.reason;
38
+ if (r.id) out.id = r.id;
39
+ if (r.name) out.name = r.name;
40
+ if (r.inviteLink) out.inviteLink = r.inviteLink;
41
+ return out;
42
+ }
43
+
44
+ export function unsupportedGroup(
45
+ op: GroupOp,
46
+ line: string,
47
+ station: string,
48
+ reason: string,
49
+ ): GroupOpResult {
50
+ return { op, line, station, supported: false, reason, members: [] };
51
+ }
52
+
53
+ const defaultReason = (op: GroupOp, station: string): string =>
54
+ `${op} is not supported on ${station}`;
55
+
56
+ const supportsOp = (station: Station, op: GroupOp): boolean =>
57
+ station.groupOps?.has(op) ?? false;
58
+
59
+ function strList(value: unknown): string[] {
60
+ if (!Array.isArray(value)) return [];
61
+ return value.filter((v) => typeof v === 'string' && v.length > 0) as string[];
62
+ }
63
+
64
+ function forwardArgs(a: Record<string, unknown>): Record<string, unknown> {
65
+ const args: Record<string, unknown> = {};
66
+ const members = strList(a.members);
67
+ if (members.length) args.members = members;
68
+ for (const key of ['name', 'account', 'parent', 'addresses', 'inboxIds'])
69
+ if (a[key] !== undefined) args[key] = a[key];
70
+ if (a.private !== undefined) args.private = a.private;
71
+ return args;
72
+ }
73
+
74
+ async function runGroupOp(
75
+ op: GroupOp,
76
+ verb: string,
77
+ line: string,
78
+ station: Station,
79
+ args: Record<string, unknown>,
80
+ ): Promise<ToolResult> {
81
+ if (!supportsOp(station, op))
82
+ return okJson(
83
+ unsupportedGroup(op, line, station.name, defaultReason(op, station.name)),
84
+ );
85
+ try {
86
+ const { result } = await makeCtx(station.name).call(verb, args);
87
+ return okJson(wrapGroupResult(op, line, station.name, result as GroupResult));
88
+ } catch (e) {
89
+ return toErr(op, e);
90
+ }
91
+ }
92
+
93
+ export async function dispatchCreateGroup(
94
+ a: Record<string, unknown>,
95
+ ): Promise<ToolResult> {
96
+ const name = str(a.station);
97
+ if (!name)
98
+ return okJson(unsupportedGroup('create_group', '', '', 'create_group requires `station`'));
99
+ const station = stationByName(name);
100
+ if (!station)
101
+ return okJson(unsupportedGroup('create_group', '', name, `no station named ${name}`));
102
+ return runGroupOp('create_group', 'groupCreate', '', station, forwardArgs(a));
103
+ }
104
+
105
+ async function dispatchLineOp(
106
+ op: GroupOp,
107
+ verb: string,
108
+ a: Record<string, unknown>,
109
+ ): Promise<ToolResult> {
110
+ const line = str(a.line);
111
+ if (!line) return okJson(unsupportedGroup(op, '', '', `${op} requires \`line\``));
112
+ const station = stationForLine(line);
113
+ if (!station)
114
+ return okJson(unsupportedGroup(op, line, '', `no station for line ${line}`));
115
+ return runGroupOp(op, verb, line, station, { line, ...forwardArgs(a) });
116
+ }
117
+
118
+ export const dispatchAddMembers = (
119
+ a: Record<string, unknown>,
120
+ ): Promise<ToolResult> => dispatchLineOp('add_members', 'groupAddMembers', a);
121
+
122
+ export const dispatchRemoveMembers = (
123
+ a: Record<string, unknown>,
124
+ ): Promise<ToolResult> =>
125
+ dispatchLineOp('remove_members', 'groupRemoveMembers', a);
126
+
127
+ export const dispatchInviteLink = (
128
+ a: Record<string, unknown>,
129
+ ): Promise<ToolResult> => dispatchLineOp('invite_link', 'groupInviteLink', a);
@@ -0,0 +1,192 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import {
3
+ BodyTooLargeError,
4
+ readBody as readBodyBuffer,
5
+ } from '../daemon/http.js';
6
+ import {
7
+ allowedAgents,
8
+ authConfigFromEnv,
9
+ authenticate,
10
+ runWithIdentity,
11
+ type RequestIdentity,
12
+ } from './request-identity.js';
13
+ import {
14
+ headerValue,
15
+ isStandaloneGet,
16
+ serveChannelGet,
17
+ } from './raw-get-stream.js';
18
+ import { channelLog, type McpSession } from './session.js';
19
+ import {
20
+ SessionCapacityError,
21
+ SessionRegistry,
22
+ type AgentLiveness,
23
+ } from './session-registry.js';
24
+ import { routeSession, sessionScopeKey } from './session-route.js';
25
+
26
+ const isInitialize = (b: unknown): boolean =>
27
+ !!b &&
28
+ typeof b === 'object' &&
29
+ (b as { method?: string }).method === 'initialize';
30
+
31
+ const MCP_BODY_MAX = 32 * 1024 * 1024;
32
+
33
+ async function readBody(req: IncomingMessage): Promise<unknown> {
34
+ const raw = (await readBodyBuffer(req, MCP_BODY_MAX)).toString('utf8');
35
+ try {
36
+ return raw ? JSON.parse(raw) : undefined;
37
+ } catch {
38
+ return undefined;
39
+ }
40
+ }
41
+
42
+ type ParsedBody = { ok: true; body: unknown } | { ok: false };
43
+
44
+ async function readOrReject(
45
+ req: IncomingMessage,
46
+ res: ServerResponse,
47
+ ): Promise<ParsedBody> {
48
+ try {
49
+ const body = req.method === 'POST' ? await readBody(req) : undefined;
50
+ return { ok: true, body };
51
+ } catch (err) {
52
+ if (!(err instanceof BodyTooLargeError)) throw err;
53
+ res.writeHead(413).end('payload too large');
54
+ return { ok: false };
55
+ }
56
+ }
57
+
58
+ async function resolveSession(
59
+ registry: SessionRegistry,
60
+ req: IncomingMessage,
61
+ body: unknown,
62
+ identity: RequestIdentity,
63
+ res: ServerResponse,
64
+ ): Promise<McpSession | undefined> {
65
+ const presented = headerValue(req, 'mcp-session-id');
66
+ const scopeKey = sessionScopeKey(identity);
67
+ const route = routeSession({
68
+ isInitialize: isInitialize(body),
69
+ presented,
70
+ ownership: registry.ownership(presented, scopeKey),
71
+ hasOwnSession: registry.forScope(scopeKey) !== undefined,
72
+ });
73
+ if (route.kind === 'reject') {
74
+ channelLog(
75
+ 'session: refused',
76
+ 'status',
77
+ route.status,
78
+ 'presented',
79
+ presented ?? '(none)',
80
+ 'scope',
81
+ scopeKey,
82
+ );
83
+ res.writeHead(route.status).end(route.message);
84
+ return undefined;
85
+ }
86
+ if (route.kind === 'use') {
87
+ const found =
88
+ presented === undefined
89
+ ? registry.forScope(scopeKey)
90
+ : registry.get(presented);
91
+ if (found) return found;
92
+ res.writeHead(404).end('Session not found');
93
+ return undefined;
94
+ }
95
+ try {
96
+ return await registry.create(identity, route.adoptId);
97
+ } catch (err) {
98
+ if (!(err instanceof SessionCapacityError)) throw err;
99
+ res.writeHead(503).end(err.message);
100
+ return undefined;
101
+ }
102
+ }
103
+
104
+ async function serveGet(
105
+ session: McpSession,
106
+ req: IncomingMessage,
107
+ res: ServerResponse,
108
+ identity: RequestIdentity,
109
+ ): Promise<void> {
110
+ if (session.currentSink && !session.owner.streamBelongsTo(identity)) {
111
+ channelLog(
112
+ 'session: refused stream takeover',
113
+ 'session',
114
+ session.id,
115
+ 'scope',
116
+ session.scopeKey,
117
+ );
118
+ res.writeHead(409).end('Conflict: stream held by another identity');
119
+ return;
120
+ }
121
+ const served = await serveChannelGet({
122
+ transport: session.transport,
123
+ eventStore: session.eventStore,
124
+ scope: allowedAgents(identity),
125
+ req,
126
+ res,
127
+ previous: session.currentSink,
128
+ log: channelLog,
129
+ registerSink: (sink) => {
130
+ session.bindSink(sink, identity);
131
+ },
132
+ });
133
+ if (served) session.channel.replayMissed();
134
+ }
135
+
136
+ let activeRegistry: SessionRegistry | undefined;
137
+
138
+ export function agentLiveness(): Map<string, AgentLiveness> {
139
+ return activeRegistry?.liveness() ?? new Map<string, AgentLiveness>();
140
+ }
141
+
142
+ export async function closeAgentSession(agentId: string): Promise<boolean> {
143
+ const scopeKey = sessionScopeKey({ kind: 'agent', agentId });
144
+ return (await activeRegistry?.closeScope(scopeKey)) ?? false;
145
+ }
146
+
147
+ export async function createMetroMcp(): Promise<{
148
+ httpHandler: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
149
+ startInbound: () => void;
150
+ }> {
151
+ await activeRegistry?.closeAll();
152
+ const registry = new SessionRegistry(channelLog);
153
+ activeRegistry = registry;
154
+
155
+ const httpHandler = async (
156
+ req: IncomingMessage,
157
+ res: ServerResponse,
158
+ ): Promise<void> => {
159
+ const identity = authenticate(req, authConfigFromEnv());
160
+ if (!identity) {
161
+ res.writeHead(401).end('unauthorized');
162
+ return;
163
+ }
164
+ const parsed = await readOrReject(req, res);
165
+ if (!parsed.ok) return;
166
+ await runWithIdentity(identity, async () => {
167
+ const session = await resolveSession(
168
+ registry,
169
+ req,
170
+ parsed.body,
171
+ identity,
172
+ res,
173
+ );
174
+ if (!session) return;
175
+ session.touch();
176
+ if (isStandaloneGet(req)) {
177
+ await serveGet(session, req, res, identity);
178
+ return;
179
+ }
180
+ await session.transport.handleRequest(req, res, parsed.body);
181
+ });
182
+ };
183
+
184
+ const startInbound = (): void => {
185
+ registry.startInbound();
186
+ channelLog(
187
+ 'inbound: per-session bus subscriptions (bounded replay on reconnect)',
188
+ );
189
+ };
190
+
191
+ return { httpHandler, startInbound };
192
+ }
@@ -0,0 +1,72 @@
1
+ import { stationForLine } from '../stations/registry.js';
2
+ import type {
3
+ MemberCapability,
4
+ MemberList,
5
+ MetroMember,
6
+ ToolResult,
7
+ } from '../stations/types.js';
8
+ import { makeCtx, okJson, toErr } from './ctx.js';
9
+ import { str } from './str.js';
10
+
11
+ export interface MemberListResult {
12
+ line: string;
13
+ station: string;
14
+ memberCount: number;
15
+ members: MetroMember[];
16
+ capability: MemberCapability;
17
+ }
18
+
19
+ export function wrapMemberList(
20
+ line: string,
21
+ station: string,
22
+ list: MemberList,
23
+ ): MemberListResult {
24
+ const members = Array.isArray(list.members) ? list.members : [];
25
+ return {
26
+ line,
27
+ station,
28
+ memberCount: members.length,
29
+ members,
30
+ capability: list.capability,
31
+ };
32
+ }
33
+
34
+ export function unsupportedMembers(
35
+ line: string,
36
+ station: string,
37
+ reason: string,
38
+ ): MemberListResult {
39
+ return {
40
+ line,
41
+ station,
42
+ memberCount: 0,
43
+ members: [],
44
+ capability: { supported: false, complete: false, reason },
45
+ };
46
+ }
47
+
48
+ export async function dispatchListMembers(
49
+ a: Record<string, unknown>,
50
+ ): Promise<ToolResult> {
51
+ const line = str(a.line);
52
+ if (!line) return okJson(unsupportedMembers('', '', 'list_members requires `line`'));
53
+ const station = stationForLine(line);
54
+ if (!station)
55
+ return okJson(unsupportedMembers(line, '', `no station for line ${line}`));
56
+ if (!station.hasAccounts || !station.hasTrain)
57
+ return okJson(
58
+ unsupportedMembers(
59
+ line,
60
+ station.name,
61
+ `${station.name} lines have no member roster`,
62
+ ),
63
+ );
64
+ const args: Record<string, unknown> = { line };
65
+ if (typeof a.limit === 'number') args.limit = a.limit;
66
+ try {
67
+ const { result } = await makeCtx(station.name).call('listMembers', args);
68
+ return okJson(wrapMemberList(line, station.name, result as MemberList));
69
+ } catch (e) {
70
+ return toErr('list_members', e);
71
+ }
72
+ }
@@ -0,0 +1,66 @@
1
+ import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { z } from 'zod';
3
+ import type { InboundRelay } from '../channels/inbound.js';
4
+ import type { ChannelOwner } from './channel-owner.js';
5
+ import { metroCall } from './ctx.js';
6
+
7
+ const PermissionRequestSchema = z.object({
8
+ method: z.literal('notifications/claude/channel/permission_request'),
9
+ params: z.object({
10
+ request_id: z.string(),
11
+ tool_name: z.string(),
12
+ description: z.string(),
13
+ input_preview: z.string(),
14
+ }),
15
+ });
16
+
17
+ type PermissionRequest = z.infer<typeof PermissionRequestSchema>;
18
+
19
+ const trainOf = (line: string): string => line.split('/')[2] ?? '';
20
+
21
+ async function metroSend(line: string, text: string): Promise<void> {
22
+ await metroCall(trainOf(line), 'send', { line, text });
23
+ }
24
+
25
+ function promptBody(params: PermissionRequest['params']): string {
26
+ return (
27
+ `Claude wants to run ${params.tool_name}: ${params.description}\n` +
28
+ (params.input_preview ? `\n${params.input_preview}\n` : '') +
29
+ `\nReply "yes ${params.request_id}" or "no ${params.request_id}"`
30
+ );
31
+ }
32
+
33
+ export interface PermissionRelayDeps {
34
+ mcp: Server;
35
+ relay: InboundRelay;
36
+ owner: ChannelOwner;
37
+ log: (...a: unknown[]) => void;
38
+ }
39
+
40
+ export function registerPermissionRelay(deps: PermissionRelayDeps): void {
41
+ const { mcp, relay, owner, log } = deps;
42
+ mcp.setNotificationHandler(
43
+ PermissionRequestSchema as never,
44
+ async (n: PermissionRequest) => {
45
+ const { params } = n;
46
+ const line = relay.knownLine;
47
+ if (!line) {
48
+ log('permission_request but no known line to relay to', params.request_id);
49
+ return;
50
+ }
51
+ if (!owner.inScope(line)) {
52
+ log(
53
+ 'permission_request: known line is outside the channel session scope',
54
+ params.request_id,
55
+ );
56
+ return;
57
+ }
58
+ relay.registerPermission(params.request_id);
59
+ try {
60
+ await metroSend(line, promptBody(params));
61
+ } catch (e) {
62
+ log('relay send failed', e);
63
+ }
64
+ },
65
+ );
66
+ }