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

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 (235) hide show
  1. package/dist/api.js +131 -0
  2. package/dist/bedrock-eventstream.js +120 -0
  3. package/dist/bedrock-proxy.js +260 -0
  4. package/dist/bedrock-upstream.js +72 -0
  5. package/dist/bedrock.js +106 -0
  6. package/dist/claude.js +15 -0
  7. package/dist/cli.js +164 -253
  8. package/dist/control.js +158 -0
  9. package/dist/plugin.js +82 -0
  10. package/dist/prompt.js +61 -0
  11. package/dist/runtime.js +94 -0
  12. package/dist/store.js +93 -0
  13. package/dist/tail.js +120 -0
  14. package/dist/update.js +33 -0
  15. package/dist/version.js +97 -0
  16. package/package.json +34 -21
  17. package/runtime/node_modules/@metro-labs/discord-bot/package.json +28 -0
  18. package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +119 -0
  19. package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +388 -0
  20. package/runtime/node_modules/@metro-labs/discord-bot/src/api-base.ts +1 -0
  21. package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +29 -0
  22. package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +210 -0
  23. package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +50 -0
  24. package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +115 -0
  25. package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +127 -0
  26. package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +215 -0
  27. package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +43 -0
  28. package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +19 -0
  29. package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +82 -0
  30. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-speak.ts +195 -0
  31. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-transcribe.ts +292 -0
  32. package/runtime/node_modules/@metro-labs/discord-bot/src/voice.ts +197 -0
  33. package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +5 -0
  34. package/runtime/node_modules/@metro-labs/mcp/package.json +66 -0
  35. package/runtime/node_modules/@metro-labs/mcp/src/channels/dedupe.ts +24 -0
  36. package/runtime/node_modules/@metro-labs/mcp/src/channels/inbound.ts +367 -0
  37. package/runtime/node_modules/@metro-labs/mcp/src/channels/media-note.ts +85 -0
  38. package/runtime/node_modules/@metro-labs/mcp/src/channels/pending.ts +75 -0
  39. package/runtime/node_modules/@metro-labs/mcp/src/channels/relay.ts +129 -0
  40. package/runtime/node_modules/@metro-labs/mcp/src/channels/webhook-note.ts +51 -0
  41. package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +311 -0
  42. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +332 -0
  43. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-connector-api.ts +150 -0
  44. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-pair.ts +20 -0
  45. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-error.ts +8 -0
  46. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +152 -0
  47. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +99 -0
  48. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-owner.ts +32 -0
  49. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +129 -0
  50. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +278 -0
  51. package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +330 -0
  52. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +125 -0
  53. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +313 -0
  54. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-json.ts +21 -0
  55. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth-routes.ts +157 -0
  56. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +127 -0
  57. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +100 -0
  58. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +57 -0
  59. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +333 -0
  60. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cors.ts +29 -0
  61. package/runtime/node_modules/@metro-labs/mcp/src/daemon/crash-guard.ts +49 -0
  62. package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +117 -0
  63. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-auth.ts +164 -0
  64. package/runtime/node_modules/@metro-labs/mcp/src/daemon/google-oauth.ts +208 -0
  65. package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +359 -0
  66. package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +79 -0
  67. package/runtime/node_modules/@metro-labs/mcp/src/daemon/log.ts +42 -0
  68. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +162 -0
  69. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +120 -0
  70. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +65 -0
  71. package/runtime/node_modules/@metro-labs/mcp/src/daemon/pair-codes.ts +58 -0
  72. package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +134 -0
  73. package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +214 -0
  74. package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +224 -0
  75. package/runtime/node_modules/@metro-labs/mcp/src/daemon/relay.ts +303 -0
  76. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +139 -0
  77. package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-source.ts +105 -0
  78. package/runtime/node_modules/@metro-labs/mcp/src/daemon/secure-fs.ts +57 -0
  79. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +27 -0
  80. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +48 -0
  81. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +218 -0
  82. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor-io.ts +97 -0
  83. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor.ts +323 -0
  84. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-call.ts +22 -0
  85. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-error.ts +53 -0
  86. package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +169 -0
  87. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +295 -0
  88. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-store.ts +181 -0
  89. package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +91 -0
  90. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +215 -0
  91. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-connectors.ts +143 -0
  92. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-map.ts +71 -0
  93. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-scope.ts +64 -0
  94. package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +37 -0
  95. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +147 -0
  96. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +126 -0
  97. package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +308 -0
  98. package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +19 -0
  99. package/runtime/node_modules/@metro-labs/mcp/src/db/key-map.ts +45 -0
  100. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +382 -0
  101. package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +300 -0
  102. package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +24 -0
  103. package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +105 -0
  104. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +117 -0
  105. package/runtime/node_modules/@metro-labs/mcp/src/db/token-carry.ts +45 -0
  106. package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +62 -0
  107. package/runtime/node_modules/@metro-labs/mcp/src/db/whatsapp-login.ts +18 -0
  108. package/runtime/node_modules/@metro-labs/mcp/src/mcp/accounts.ts +145 -0
  109. package/runtime/node_modules/@metro-labs/mcp/src/mcp/call-tools.ts +203 -0
  110. package/runtime/node_modules/@metro-labs/mcp/src/mcp/channel-owner.ts +29 -0
  111. package/runtime/node_modules/@metro-labs/mcp/src/mcp/ctx.ts +62 -0
  112. package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +91 -0
  113. package/runtime/node_modules/@metro-labs/mcp/src/mcp/frame-scope.ts +21 -0
  114. package/runtime/node_modules/@metro-labs/mcp/src/mcp/group-tools.ts +129 -0
  115. package/runtime/node_modules/@metro-labs/mcp/src/mcp/index.ts +192 -0
  116. package/runtime/node_modules/@metro-labs/mcp/src/mcp/member-tools.ts +72 -0
  117. package/runtime/node_modules/@metro-labs/mcp/src/mcp/permission-relay.ts +66 -0
  118. package/runtime/node_modules/@metro-labs/mcp/src/mcp/raw-get-stream.ts +207 -0
  119. package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +73 -0
  120. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-registry.ts +172 -0
  121. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +43 -0
  122. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session.ts +211 -0
  123. package/runtime/node_modules/@metro-labs/mcp/src/mcp/str.ts +5 -0
  124. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-dispatch.ts +212 -0
  125. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +397 -0
  126. package/runtime/node_modules/@metro-labs/mcp/src/mcp/upload-tool.ts +66 -0
  127. package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +240 -0
  128. package/runtime/node_modules/@metro-labs/mcp/src/server.ts +2 -0
  129. package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +80 -0
  130. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-inline.ts +100 -0
  131. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +161 -0
  132. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +200 -0
  133. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-xmtp.ts +111 -0
  134. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +182 -0
  135. package/runtime/node_modules/@metro-labs/mcp/src/stations/attachments.ts +196 -0
  136. package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +82 -0
  137. package/runtime/node_modules/@metro-labs/mcp/src/stations/messaging-normalize.ts +88 -0
  138. package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +35 -0
  139. package/runtime/node_modules/@metro-labs/mcp/src/stations/station-runtime.ts +64 -0
  140. package/runtime/node_modules/@metro-labs/mcp/src/stations/types.ts +107 -0
  141. package/runtime/node_modules/@metro-labs/telegram/package.json +28 -0
  142. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +81 -0
  143. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +254 -0
  144. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +87 -0
  145. package/runtime/node_modules/@metro-labs/telegram/src/client.ts +77 -0
  146. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +141 -0
  147. package/runtime/node_modules/@metro-labs/telegram/src/group.ts +261 -0
  148. package/runtime/node_modules/@metro-labs/telegram/src/history.ts +52 -0
  149. package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +78 -0
  150. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +46 -0
  151. package/runtime/node_modules/@metro-labs/telegram/src/login.ts +166 -0
  152. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +80 -0
  153. package/runtime/node_modules/@metro-labs/telegram/src/members.ts +75 -0
  154. package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +22 -0
  155. package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +167 -0
  156. package/runtime/node_modules/@metro-labs/telegram/src/station.ts +24 -0
  157. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +7 -0
  158. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -0
  159. package/runtime/node_modules/@metro-labs/telegram-bot/package.json +25 -0
  160. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +120 -0
  161. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +295 -0
  162. package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
  163. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +172 -0
  164. package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +103 -0
  165. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
  166. package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
  167. package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +17 -0
  168. package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +48 -0
  169. package/runtime/node_modules/@metro-labs/telegram-bot/src/verify.ts +38 -0
  170. package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +3 -0
  171. package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
  172. package/runtime/node_modules/@metro-labs/webhook/src/station.ts +40 -0
  173. package/runtime/node_modules/@metro-labs/whatsapp/package.json +29 -0
  174. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +67 -0
  175. package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
  176. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +178 -0
  177. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
  178. package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
  179. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +318 -0
  180. package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
  181. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +132 -0
  182. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
  183. package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
  184. package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
  185. package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
  186. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
  187. package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
  188. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
  189. package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
  190. package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +141 -0
  191. package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +17 -0
  192. package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
  193. package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
  194. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
  195. package/runtime/node_modules/@metro-labs/xmtp/package.json +33 -0
  196. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +113 -0
  197. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
  198. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
  199. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
  200. package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
  201. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +375 -0
  202. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
  203. package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
  204. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
  205. package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
  206. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +333 -0
  207. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +78 -0
  208. package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
  209. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
  210. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +33 -0
  211. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
  212. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
  213. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
  214. package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
  215. package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +13 -0
  216. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
  217. package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
  218. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
  219. package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +84 -0
  220. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +39 -0
  221. package/runtime/runtime.json +3 -0
  222. package/runtime/trains/.keep +0 -0
  223. package/LICENSE +0 -21
  224. package/README.md +0 -114
  225. package/dist/agents/claude.js +0 -229
  226. package/dist/agents/codex.js +0 -282
  227. package/dist/agents/types.js +0 -3
  228. package/dist/channels/discord.js +0 -142
  229. package/dist/channels/telegram.js +0 -209
  230. package/dist/lib/dotenv.js +0 -31
  231. package/dist/lib/scope-cache.js +0 -85
  232. package/dist/lib/streaming.js +0 -207
  233. package/dist/log.js +0 -12
  234. package/dist/orchestrator.js +0 -399
  235. package/dist/paths.js +0 -33
@@ -0,0 +1,85 @@
1
+ import { stat } from 'node:fs/promises';
2
+
3
+ const MAX_INLINE_BYTES = 4 * 1024 * 1024;
4
+
5
+ export interface SavedMedia {
6
+ contentType?: string;
7
+ attachmentFor?: string;
8
+ attachmentPath?: string;
9
+ localPath?: string;
10
+ url?: string;
11
+ mime?: string;
12
+ name?: string;
13
+ kind?: string;
14
+ index?: number;
15
+ reason?: string;
16
+ }
17
+
18
+ export interface MediaNote {
19
+ content: string;
20
+ kind: string;
21
+ name: string;
22
+ path: string;
23
+ }
24
+
25
+ export function mediaKind(mime?: string, name?: string): string {
26
+ const m = (mime ?? '').toLowerCase();
27
+ if (m.startsWith('image/')) return 'image';
28
+ if (m.startsWith('video/')) return 'video';
29
+ if (m.startsWith('audio/')) return 'audio';
30
+ const n = name ?? '';
31
+ if (/\.(png|jpe?g|gif|webp|heic)$/i.test(n)) return 'image';
32
+ if (/\.(mp4|mov|webm|m4v)$/i.test(n)) return 'video';
33
+ if (/\.(m4a|mp3|ogg|wav)$/i.test(n)) return 'audio';
34
+ return 'file';
35
+ }
36
+
37
+ async function fileSize(path: string): Promise<number> {
38
+ try {
39
+ return (await stat(path)).size;
40
+ } catch {
41
+ return 0;
42
+ }
43
+ }
44
+
45
+ function howToRead(url: string | undefined, tooBig: boolean): string {
46
+ if (!url)
47
+ return 'That path is on the daemon host, not on yours - this file is readable only by an agent running there.';
48
+ if (tooBig)
49
+ return 'Large file - fetch the Public URL only if you need the bytes, and do not inline it. The path is on the daemon host, not on yours.';
50
+ return 'Fetch the Public URL to view it. The path is on the daemon host, so the Read tool works only for an agent running there.';
51
+ }
52
+
53
+ export async function buildMediaNote(
54
+ p: SavedMedia,
55
+ caption: string,
56
+ ): Promise<MediaNote | null> {
57
+ const path = p.attachmentPath ?? p.localPath;
58
+ if (!path) return null;
59
+ const kind = p.kind ?? mediaKind(p.mime, p.name);
60
+ const name = p.name ?? path.split('/').pop() ?? 'attachment';
61
+ const size = await fileSize(path);
62
+ const sizeNote = size ? ` (${(size / 1024 / 1024).toFixed(2)} MB)` : '';
63
+ const content =
64
+ (caption ? `${caption}\n` : '') +
65
+ `[${kind} attachment received: ${name}${p.mime ? `, ${p.mime}` : ''}${sizeNote}]\n` +
66
+ (p.url ? `Public URL: ${p.url}\n` : '') +
67
+ `Daemon-host path: ${path}\n` +
68
+ howToRead(p.url, size > MAX_INLINE_BYTES);
69
+ return { content, kind, name, path };
70
+ }
71
+
72
+ export function buildMediaFailureNote(
73
+ p: SavedMedia,
74
+ caption: string,
75
+ ): MediaNote {
76
+ const kind = p.kind ?? mediaKind(p.mime, p.name);
77
+ const name = p.name ?? 'attachment';
78
+ const reason = p.reason ?? 'no reason reported';
79
+ const content =
80
+ (caption ? `${caption}\n` : '') +
81
+ `[${kind} attachment could not be fetched: ${name}]\n` +
82
+ `Reason: ${reason}\n` +
83
+ 'The sender still has the file; nothing was saved. Ask them to resend it, or say why it could not be taken.';
84
+ return { content, kind, name, path: '' };
85
+ }
@@ -0,0 +1,75 @@
1
+ import { str } from '../mcp/str.js';
2
+
3
+ export interface PendingAtt {
4
+ kind?: string;
5
+ name?: string;
6
+ }
7
+
8
+ export interface PendingMsg {
9
+ line: string;
10
+ from: string;
11
+ station: string;
12
+ text: string;
13
+ ts: string;
14
+ messageId: string;
15
+ lineName: string;
16
+ fromName: string;
17
+ fromDisplayName: string;
18
+ attachments: PendingAtt[];
19
+ saved: Set<number>;
20
+ timer: ReturnType<typeof setTimeout>;
21
+ }
22
+
23
+ export interface MediaCtx {
24
+ line: string;
25
+ from: string;
26
+ station: string;
27
+ text?: string;
28
+ ts?: string;
29
+ messageId?: string;
30
+ lineName?: string;
31
+ fromName?: string;
32
+ fromDisplayName?: string;
33
+ }
34
+
35
+ export function capSet(set: Set<string>, max: number): void {
36
+ while (set.size > max) {
37
+ const oldest = set.values().next();
38
+ if (oldest.done) break;
39
+ set.delete(oldest.value);
40
+ }
41
+ }
42
+
43
+ export const tsMeta = (v: unknown): Record<string, string> => {
44
+ const ts = str(v);
45
+ return ts ? { ts } : {};
46
+ };
47
+
48
+ export const displayNameMeta = (v: unknown): Record<string, string> => {
49
+ const name = str(v);
50
+ return name ? { from_display_name: name } : {};
51
+ };
52
+
53
+ export const senderMeta = (c: MediaCtx): Record<string, string> => ({
54
+ ...tsMeta(c.ts),
55
+ ...(c.messageId ? { message_id: c.messageId } : {}),
56
+ ...(c.lineName ? { line_name: c.lineName } : {}),
57
+ ...(c.fromName ? { from_name: c.fromName } : {}),
58
+ ...displayNameMeta(c.fromDisplayName),
59
+ });
60
+
61
+ export function takeMediaCtx(buf: PendingMsg): MediaCtx {
62
+ const ctx: MediaCtx = {
63
+ line: buf.line,
64
+ from: buf.from,
65
+ station: buf.station,
66
+ text: buf.text,
67
+ ts: buf.ts,
68
+ messageId: buf.messageId,
69
+ lineName: buf.lineName,
70
+ fromName: buf.fromName,
71
+ fromDisplayName: buf.fromDisplayName,
72
+ };
73
+ buf.text = '';
74
+ return ctx;
75
+ }
@@ -0,0 +1,129 @@
1
+ import {
2
+ bufferedSince,
3
+ currentBusSeq,
4
+ subscribeEvents,
5
+ type MetroEvent,
6
+ } from '../daemon/events.js';
7
+ import type { InboundRelay } from './inbound.js';
8
+
9
+ export interface ReplayLedger {
10
+ startAt: number;
11
+ delivered: Set<number>;
12
+ }
13
+
14
+ export const newReplayLedger = (startAt = -1): ReplayLedger => ({
15
+ startAt,
16
+ delivered: new Set<number>(),
17
+ });
18
+
19
+ interface ChannelRelayDeps {
20
+ relay: InboundRelay;
21
+ log: (...a: unknown[]) => void;
22
+ inScope: (line: string) => boolean;
23
+ ledger?: ReplayLedger;
24
+ }
25
+
26
+ const PENDING_MAX = 2000;
27
+
28
+ export class ChannelRelay {
29
+ private readonly deps: ChannelRelayDeps;
30
+ private readonly ledger: ReplayLedger;
31
+ private chain: Promise<void> = Promise.resolve();
32
+
33
+ constructor(deps: ChannelRelayDeps) {
34
+ this.deps = deps;
35
+ this.ledger = deps.ledger ?? newReplayLedger();
36
+ }
37
+
38
+ start(): () => void {
39
+ if (this.ledger.startAt < 0) this.ledger.startAt = currentBusSeq();
40
+ return subscribeEvents((event, busSeq) => {
41
+ this.enqueue(event, busSeq, false);
42
+ });
43
+ }
44
+
45
+ replayMissed(): void {
46
+ const missed = bufferedSince(this.ledger.startAt).filter(
47
+ (b) => !this.ledger.delivered.has(b.busSeq),
48
+ );
49
+ const first = missed.at(0);
50
+ const last = missed.at(-1);
51
+ if (!first || !last) return;
52
+ this.deps.log(
53
+ 'relay: replay',
54
+ `(${missed.length} events)`,
55
+ 'range',
56
+ `${first.busSeq}..${last.busSeq}`,
57
+ );
58
+ for (const b of missed) this.enqueue(b.event, b.busSeq, true);
59
+ }
60
+
61
+ private enqueue(event: MetroEvent, busSeq: number, replay: boolean): void {
62
+ const { delivered } = this.ledger;
63
+ if (delivered.has(busSeq)) return;
64
+ delivered.add(busSeq);
65
+ if (delivered.size > PENDING_MAX) {
66
+ const cutoff = busSeq - PENDING_MAX;
67
+ for (const s of delivered) if (s <= cutoff) delivered.delete(s);
68
+ }
69
+ this.chain = this.chain.then(() =>
70
+ this.deliver(event, busSeq, replay).catch((err: unknown) => {
71
+ this.deps.log(
72
+ 'relay: delivery step failed; chain kept alive',
73
+ 'busSeq',
74
+ busSeq,
75
+ 'line',
76
+ event.line,
77
+ err,
78
+ );
79
+ }),
80
+ );
81
+ }
82
+
83
+ private withhold(event: MetroEvent, busSeq: number): void {
84
+ this.ledger.delivered.delete(busSeq);
85
+ this.deps.log(
86
+ 'relay: withheld (line outside the channel session scope)',
87
+ 'busSeq',
88
+ busSeq,
89
+ 'line',
90
+ event.line,
91
+ );
92
+ }
93
+
94
+ private async deliver(
95
+ event: MetroEvent,
96
+ busSeq: number,
97
+ replay: boolean,
98
+ ): Promise<void> {
99
+ if (!this.deps.inScope(event.line)) {
100
+ this.withhold(event, busSeq);
101
+ return;
102
+ }
103
+ this.deps.log(
104
+ 'relay: notify',
105
+ 'busSeq',
106
+ busSeq,
107
+ 'id',
108
+ event.id,
109
+ 'replay',
110
+ replay,
111
+ );
112
+ try {
113
+ await this.deps.relay.handleEvent(
114
+ event as unknown as Record<string, unknown>,
115
+ replay,
116
+ );
117
+ } catch (err) {
118
+ this.ledger.delivered.delete(busSeq);
119
+ this.deps.log(
120
+ 'channel delivery failed; bounded replay on reconnect',
121
+ 'busSeq',
122
+ busSeq,
123
+ 'line',
124
+ event.line,
125
+ err,
126
+ );
127
+ }
128
+ }
129
+ }
@@ -0,0 +1,51 @@
1
+ const MAX_BODY_CHARS = 8 * 1024;
2
+
3
+ const HEADERS_KEPT = [
4
+ 'x-github-event',
5
+ 'x-github-delivery',
6
+ 'x-intercom-topic',
7
+ 'x-request-id',
8
+ 'user-agent',
9
+ ];
10
+
11
+ interface WebhookPayload {
12
+ headers?: Record<string, string>;
13
+ body?: unknown;
14
+ }
15
+
16
+ function renderBody(body: unknown): string {
17
+ if (body === undefined || body === null) return '';
18
+ const text =
19
+ typeof body === 'string' ? body : JSON.stringify(body, null, 2);
20
+ if (text.length <= MAX_BODY_CHARS) return text;
21
+ return (
22
+ text.slice(0, MAX_BODY_CHARS) +
23
+ `\n[truncated: ${String(text.length - MAX_BODY_CHARS)} more characters]`
24
+ );
25
+ }
26
+
27
+ function renderHeaders(headers: Record<string, string> | undefined): string {
28
+ if (!headers) return '';
29
+ const kept = HEADERS_KEPT.flatMap((k) =>
30
+ headers[k] === undefined ? [] : [`${k}: ${headers[k]}`],
31
+ );
32
+ return kept.length === 0 ? '' : kept.join('\n') + '\n';
33
+ }
34
+
35
+ export function buildWebhookNote(
36
+ summary: string,
37
+ lineName: string | undefined,
38
+ payload: unknown,
39
+ ): string {
40
+ const p = (
41
+ payload !== null && typeof payload === 'object' ? payload : {}
42
+ ) as WebhookPayload;
43
+ const label = lineName ? `${lineName}: ` : '';
44
+ const body = renderBody(p.body);
45
+ return (
46
+ `[webhook received] ${label}${summary}\n` +
47
+ renderHeaders(p.headers) +
48
+ (body ? `\n${body}\n` : '') +
49
+ 'Inbound only: this line takes no send, reply or react.'
50
+ );
51
+ }
@@ -0,0 +1,311 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { errMsg, log } from './log.js';
3
+ import { ApiError } from './api-error.js';
4
+ import {
5
+ apiFailure,
6
+ bodyField,
7
+ readJsonBody,
8
+ sendJson,
9
+ type ApiSession,
10
+ } from './api-http.js';
11
+ import {
12
+ isStationName,
13
+ parseAccountId,
14
+ type AccountRef,
15
+ } from '../db/account-attach.js';
16
+ import type { StationName } from '../db/schema.js';
17
+ import {
18
+ ATTACHABLE_STATIONS,
19
+ isAttachStation,
20
+ type AttachInput,
21
+ type OneTimeSecret,
22
+ type PreparedAccount,
23
+ } from '../stations/attach.js';
24
+ import {
25
+ INTERACTIVE_STATIONS,
26
+ isInteractiveStation,
27
+ type InteractiveStation,
28
+ } from '../stations/attach-interactive.js';
29
+ import {
30
+ ATTACH_ID_RE,
31
+ type AttachOwner,
32
+ type AttachView,
33
+ } from './attach-session.js';
34
+
35
+ export interface AttachSessionApi {
36
+ start: (
37
+ owner: AttachOwner,
38
+ station: InteractiveStation,
39
+ input: Record<string, unknown>,
40
+ ) => Promise<AttachView>;
41
+ view: (owner: AttachOwner, attachId: string) => AttachView;
42
+ submit: (
43
+ owner: AttachOwner,
44
+ attachId: string,
45
+ input: { code?: unknown; password?: unknown },
46
+ ) => Promise<AttachView>;
47
+ cancel: (owner: AttachOwner, attachId: string) => Promise<void>;
48
+ }
49
+
50
+ export interface AccountApiDeps {
51
+ attachSessions: AttachSessionApi;
52
+ prepareAccount: (input: AttachInput) => Promise<PreparedAccount>;
53
+ attachAccount: (
54
+ email: string,
55
+ agentId: string,
56
+ station: StationName,
57
+ config: Record<string, unknown>,
58
+ ) => Promise<AccountRef>;
59
+ detachAccount: (
60
+ email: string,
61
+ agentId: string,
62
+ station: StationName,
63
+ accountId: string,
64
+ ) => Promise<AccountRef>;
65
+ syncStations: (station: StationName) => Promise<void>;
66
+ }
67
+
68
+ export type AccountRoute =
69
+ | { kind: 'start' }
70
+ | { kind: 'session'; attachId: string }
71
+ | { kind: 'step'; attachId: string }
72
+ | { kind: 'account'; station: StationName; accountId: string };
73
+
74
+ export const ATTACHABLE: string[] = [
75
+ ...ATTACHABLE_STATIONS,
76
+ ...INTERACTIVE_STATIONS,
77
+ ];
78
+
79
+ const ROUTE_METHODS: Record<AccountRoute['kind'], string[]> = {
80
+ start: ['POST'],
81
+ session: ['GET', 'DELETE'],
82
+ step: ['POST'],
83
+ account: ['DELETE'],
84
+ };
85
+
86
+ function twoSegmentRoute(head: string, tail: string): AccountRoute | null {
87
+ if (ATTACH_ID_RE.test(head))
88
+ return tail === 'step' ? { kind: 'step', attachId: head } : null;
89
+ if (!isStationName(head)) return null;
90
+ const accountId = parseAccountId(tail);
91
+ return accountId === null ? null : { kind: 'account', station: head, accountId };
92
+ }
93
+
94
+ export function accountRoute(rest: string[]): AccountRoute | null {
95
+ const [head, tail] = rest;
96
+ if (head === undefined) return null;
97
+ if (rest.length === 1)
98
+ return head === 'start'
99
+ ? { kind: 'start' }
100
+ : ATTACH_ID_RE.test(head)
101
+ ? { kind: 'session', attachId: head }
102
+ : null;
103
+ if (rest.length !== 2 || tail === undefined) return null;
104
+ return twoSegmentRoute(head, tail);
105
+ }
106
+
107
+ export function accountRouteAllows(
108
+ route: AccountRoute,
109
+ method: string | undefined,
110
+ ): boolean {
111
+ return ROUTE_METHODS[route.kind].includes(method ?? '');
112
+ }
113
+
114
+ async function activate(
115
+ deps: AccountApiDeps,
116
+ station: StationName,
117
+ ): Promise<boolean> {
118
+ try {
119
+ await deps.syncStations(station);
120
+ return true;
121
+ } catch (err) {
122
+ log.warn(
123
+ { station, err: errMsg(err) },
124
+ 'account-api: station reload failed, the change lands at the next boot',
125
+ );
126
+ return false;
127
+ }
128
+ }
129
+
130
+ interface AttachPayload {
131
+ status: 'done';
132
+ agentId: string;
133
+ station: string;
134
+ accountId: string;
135
+ identity: Record<string, string>;
136
+ activated: boolean;
137
+ secret?: OneTimeSecret;
138
+ }
139
+
140
+ async function storeAccount(
141
+ deps: AccountApiDeps,
142
+ session: ApiSession,
143
+ agentId: string,
144
+ station: StationName,
145
+ prepared: PreparedAccount,
146
+ ): Promise<AccountRef> {
147
+ try {
148
+ return await deps.attachAccount(
149
+ session.email,
150
+ agentId,
151
+ station,
152
+ prepared.config,
153
+ );
154
+ } catch (err) {
155
+ prepared.discard?.();
156
+ throw err;
157
+ }
158
+ }
159
+
160
+ function ownerOf(session: ApiSession, agentId: string): AttachOwner {
161
+ return { email: session.email, agentId };
162
+ }
163
+
164
+ function asInput(body: unknown): Record<string, unknown> {
165
+ const out: Record<string, unknown> = {};
166
+ for (const key of ['apiId', 'apiHash', 'phone'])
167
+ out[key] = bodyField(body, key);
168
+ return out;
169
+ }
170
+
171
+ async function handleStart(
172
+ req: IncomingMessage,
173
+ res: ServerResponse,
174
+ deps: AccountApiDeps,
175
+ session: ApiSession,
176
+ agentId: string,
177
+ ): Promise<void> {
178
+ const body = await readJsonBody(req);
179
+ const station = bodyField(body, 'station');
180
+ if (isInteractiveStation(station)) {
181
+ const view = await deps.attachSessions.start(
182
+ ownerOf(session, agentId),
183
+ station,
184
+ asInput(body),
185
+ );
186
+ sendJson(req, res, 201, view);
187
+ return;
188
+ }
189
+ if (!isAttachStation(station))
190
+ throw new ApiError(`station must be one of ${ATTACHABLE.join(', ')}`, 400);
191
+ const prepared = await deps.prepareAccount({
192
+ station,
193
+ token: bodyField(body, 'token'),
194
+ });
195
+ const ref = await storeAccount(deps, session, agentId, station, prepared);
196
+ log.info(
197
+ { agentId: ref.agentId, station, account: ref.accountId },
198
+ 'account-api: attached a station account',
199
+ );
200
+ const payload: AttachPayload = {
201
+ status: 'done',
202
+ agentId: ref.agentId,
203
+ station,
204
+ accountId: ref.accountId,
205
+ identity: prepared.identity,
206
+ activated: await activate(deps, station),
207
+ };
208
+ sendJson(
209
+ req,
210
+ res,
211
+ 201,
212
+ prepared.secret === undefined
213
+ ? payload
214
+ : { ...payload, secret: prepared.secret },
215
+ );
216
+ }
217
+
218
+ async function handleDetach(
219
+ req: IncomingMessage,
220
+ res: ServerResponse,
221
+ deps: AccountApiDeps,
222
+ session: ApiSession,
223
+ agentId: string,
224
+ target: { station: StationName; accountId: string },
225
+ ): Promise<void> {
226
+ const ref = await deps.detachAccount(
227
+ session.email,
228
+ agentId,
229
+ target.station,
230
+ target.accountId,
231
+ );
232
+ log.info(
233
+ { agentId: ref.agentId, station: ref.station, account: ref.accountId },
234
+ 'account-api: detached a station account',
235
+ );
236
+ sendJson(req, res, 200, {
237
+ agentId: ref.agentId,
238
+ station: ref.station,
239
+ accountId: ref.accountId,
240
+ detached: true,
241
+ activated: await activate(deps, ref.station),
242
+ });
243
+ }
244
+
245
+ async function handleSession(
246
+ req: IncomingMessage,
247
+ res: ServerResponse,
248
+ deps: AccountApiDeps,
249
+ owner: AttachOwner,
250
+ attachId: string,
251
+ ): Promise<void> {
252
+ if (req.method === 'DELETE') {
253
+ await deps.attachSessions.cancel(owner, attachId);
254
+ sendJson(req, res, 200, { attachId, cancelled: true });
255
+ return;
256
+ }
257
+ sendJson(req, res, 200, deps.attachSessions.view(owner, attachId));
258
+ }
259
+
260
+ async function handleStep(
261
+ req: IncomingMessage,
262
+ res: ServerResponse,
263
+ deps: AccountApiDeps,
264
+ owner: AttachOwner,
265
+ attachId: string,
266
+ ): Promise<void> {
267
+ const body = await readJsonBody(req);
268
+ const view = await deps.attachSessions.submit(owner, attachId, {
269
+ code: bodyField(body, 'code'),
270
+ password: bodyField(body, 'password'),
271
+ });
272
+ sendJson(req, res, 200, view);
273
+ }
274
+
275
+ async function dispatchRoute(
276
+ req: IncomingMessage,
277
+ res: ServerResponse,
278
+ deps: AccountApiDeps,
279
+ session: ApiSession,
280
+ agentId: string,
281
+ route: AccountRoute,
282
+ ): Promise<void> {
283
+ if (route.kind === 'start')
284
+ return handleStart(req, res, deps, session, agentId);
285
+ if (route.kind === 'session')
286
+ return handleSession(
287
+ req,
288
+ res,
289
+ deps,
290
+ ownerOf(session, agentId),
291
+ route.attachId,
292
+ );
293
+ if (route.kind === 'step')
294
+ return handleStep(req, res, deps, ownerOf(session, agentId), route.attachId);
295
+ return handleDetach(req, res, deps, session, agentId, route);
296
+ }
297
+
298
+ export async function handleAccountRoute(
299
+ req: IncomingMessage,
300
+ res: ServerResponse,
301
+ deps: AccountApiDeps,
302
+ session: ApiSession,
303
+ agentId: string,
304
+ route: AccountRoute,
305
+ ): Promise<void> {
306
+ try {
307
+ await dispatchRoute(req, res, deps, session, agentId, route);
308
+ } catch (err) {
309
+ apiFailure(req, res, err);
310
+ }
311
+ }