@stage-labs/metro 0.1.0-beta.4 → 0.1.0-beta.41

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 (249) 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 +181 -207
  8. package/dist/control.js +173 -0
  9. package/dist/plugin.js +82 -0
  10. package/dist/prompt.js +61 -0
  11. package/dist/runtime.js +117 -0
  12. package/dist/serve.js +77 -0
  13. package/dist/store.js +93 -0
  14. package/dist/tail.js +120 -0
  15. package/dist/update.js +33 -0
  16. package/dist/version.js +97 -0
  17. package/package.json +34 -21
  18. package/runtime/node_modules/@metro-labs/discord-bot/package.json +28 -0
  19. package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +119 -0
  20. package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +388 -0
  21. package/runtime/node_modules/@metro-labs/discord-bot/src/api-base.ts +1 -0
  22. package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +29 -0
  23. package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +210 -0
  24. package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +50 -0
  25. package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +115 -0
  26. package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +127 -0
  27. package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +215 -0
  28. package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +43 -0
  29. package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +19 -0
  30. package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +82 -0
  31. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-speak.ts +195 -0
  32. package/runtime/node_modules/@metro-labs/discord-bot/src/voice-transcribe.ts +292 -0
  33. package/runtime/node_modules/@metro-labs/discord-bot/src/voice.ts +197 -0
  34. package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +5 -0
  35. package/runtime/node_modules/@metro-labs/mcp/package.json +67 -0
  36. package/runtime/node_modules/@metro-labs/mcp/src/channels/dedupe.ts +24 -0
  37. package/runtime/node_modules/@metro-labs/mcp/src/channels/inbound.ts +367 -0
  38. package/runtime/node_modules/@metro-labs/mcp/src/channels/media-note.ts +85 -0
  39. package/runtime/node_modules/@metro-labs/mcp/src/channels/pending.ts +75 -0
  40. package/runtime/node_modules/@metro-labs/mcp/src/channels/relay.ts +129 -0
  41. package/runtime/node_modules/@metro-labs/mcp/src/channels/webhook-note.ts +51 -0
  42. package/runtime/node_modules/@metro-labs/mcp/src/daemon/account-api.ts +311 -0
  43. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +334 -0
  44. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-connector-api.ts +150 -0
  45. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-import.ts +76 -0
  46. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-pair.ts +20 -0
  47. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-error.ts +8 -0
  48. package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +153 -0
  49. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-grant.ts +99 -0
  50. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-owner.ts +32 -0
  51. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-serve.ts +133 -0
  52. package/runtime/node_modules/@metro-labs/mcp/src/daemon/attach-session.ts +278 -0
  53. package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +383 -0
  54. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +143 -0
  55. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connect-hint.ts +33 -0
  56. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +315 -0
  57. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-json.ts +21 -0
  58. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth-routes.ts +157 -0
  59. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-oauth.ts +127 -0
  60. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-tools.ts +100 -0
  61. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-url.ts +57 -0
  62. package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-verify.ts +333 -0
  63. package/runtime/node_modules/@metro-labs/mcp/src/daemon/cors.ts +29 -0
  64. package/runtime/node_modules/@metro-labs/mcp/src/daemon/crash-guard.ts +49 -0
  65. package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +117 -0
  66. package/runtime/node_modules/@metro-labs/mcp/src/daemon/hosted-connectors.ts +118 -0
  67. package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +369 -0
  68. package/runtime/node_modules/@metro-labs/mcp/src/daemon/identity.ts +86 -0
  69. package/runtime/node_modules/@metro-labs/mcp/src/daemon/import-api.ts +67 -0
  70. package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-connectors-api.ts +56 -0
  71. package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-mode.ts +187 -0
  72. package/runtime/node_modules/@metro-labs/mcp/src/daemon/local-secret.ts +21 -0
  73. package/runtime/node_modules/@metro-labs/mcp/src/daemon/log.ts +42 -0
  74. package/runtime/node_modules/@metro-labs/mcp/src/daemon/mode-api.ts +31 -0
  75. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-client.ts +162 -0
  76. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-discovery.ts +120 -0
  77. package/runtime/node_modules/@metro-labs/mcp/src/daemon/oauth-pending.ts +65 -0
  78. package/runtime/node_modules/@metro-labs/mcp/src/daemon/pair-codes.ts +58 -0
  79. package/runtime/node_modules/@metro-labs/mcp/src/daemon/paths.ts +145 -0
  80. package/runtime/node_modules/@metro-labs/mcp/src/daemon/project-api.ts +214 -0
  81. package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +224 -0
  82. package/runtime/node_modules/@metro-labs/mcp/src/daemon/relay.ts +303 -0
  83. package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +139 -0
  84. package/runtime/node_modules/@metro-labs/mcp/src/daemon/runtime-source.ts +105 -0
  85. package/runtime/node_modules/@metro-labs/mcp/src/daemon/secure-fs.ts +57 -0
  86. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-api.ts +27 -0
  87. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +63 -0
  88. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-config.ts +27 -0
  89. package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +218 -0
  90. package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-auth.ts +82 -0
  91. package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-nonces.ts +43 -0
  92. package/runtime/node_modules/@metro-labs/mcp/src/daemon/siwe-routes.ts +89 -0
  93. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor-io.ts +97 -0
  94. package/runtime/node_modules/@metro-labs/mcp/src/daemon/supervisor.ts +320 -0
  95. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-call.ts +22 -0
  96. package/runtime/node_modules/@metro-labs/mcp/src/daemon/train-error.ts +53 -0
  97. package/runtime/node_modules/@metro-labs/mcp/src/daemon/tunnel.ts +257 -0
  98. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-api.ts +295 -0
  99. package/runtime/node_modules/@metro-labs/mcp/src/daemon/upload-store.ts +181 -0
  100. package/runtime/node_modules/@metro-labs/mcp/src/db/account-attach.ts +91 -0
  101. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-admin.ts +214 -0
  102. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-connectors.ts +143 -0
  103. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-map.ts +84 -0
  104. package/runtime/node_modules/@metro-labs/mcp/src/db/agent-scope.ts +64 -0
  105. package/runtime/node_modules/@metro-labs/mcp/src/db/client.ts +37 -0
  106. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-config.ts +147 -0
  107. package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +126 -0
  108. package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +326 -0
  109. package/runtime/node_modules/@metro-labs/mcp/src/db/file-admin.ts +319 -0
  110. package/runtime/node_modules/@metro-labs/mcp/src/db/file-source.ts +152 -0
  111. package/runtime/node_modules/@metro-labs/mcp/src/db/ids.ts +21 -0
  112. package/runtime/node_modules/@metro-labs/mcp/src/db/key-map.ts +45 -0
  113. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +373 -0
  114. package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +309 -0
  115. package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +24 -0
  116. package/runtime/node_modules/@metro-labs/mcp/src/db/runtimes.ts +105 -0
  117. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +118 -0
  118. package/runtime/node_modules/@metro-labs/mcp/src/db/token-carry.ts +45 -0
  119. package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +72 -0
  120. package/runtime/node_modules/@metro-labs/mcp/src/db/whatsapp-login.ts +18 -0
  121. package/runtime/node_modules/@metro-labs/mcp/src/mcp/accounts.ts +141 -0
  122. package/runtime/node_modules/@metro-labs/mcp/src/mcp/call-tools.ts +203 -0
  123. package/runtime/node_modules/@metro-labs/mcp/src/mcp/channel-owner.ts +29 -0
  124. package/runtime/node_modules/@metro-labs/mcp/src/mcp/ctx.ts +62 -0
  125. package/runtime/node_modules/@metro-labs/mcp/src/mcp/event-store.ts +91 -0
  126. package/runtime/node_modules/@metro-labs/mcp/src/mcp/frame-scope.ts +21 -0
  127. package/runtime/node_modules/@metro-labs/mcp/src/mcp/group-tools.ts +129 -0
  128. package/runtime/node_modules/@metro-labs/mcp/src/mcp/index.ts +192 -0
  129. package/runtime/node_modules/@metro-labs/mcp/src/mcp/member-tools.ts +72 -0
  130. package/runtime/node_modules/@metro-labs/mcp/src/mcp/permission-relay.ts +66 -0
  131. package/runtime/node_modules/@metro-labs/mcp/src/mcp/raw-get-stream.ts +207 -0
  132. package/runtime/node_modules/@metro-labs/mcp/src/mcp/request-identity.ts +73 -0
  133. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-registry.ts +172 -0
  134. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session-route.ts +43 -0
  135. package/runtime/node_modules/@metro-labs/mcp/src/mcp/session.ts +211 -0
  136. package/runtime/node_modules/@metro-labs/mcp/src/mcp/str.ts +5 -0
  137. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-dispatch.ts +212 -0
  138. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +397 -0
  139. package/runtime/node_modules/@metro-labs/mcp/src/mcp/upload-tool.ts +66 -0
  140. package/runtime/node_modules/@metro-labs/mcp/src/monitor/api.ts +240 -0
  141. package/runtime/node_modules/@metro-labs/mcp/src/server.ts +2 -0
  142. package/runtime/node_modules/@metro-labs/mcp/src/stations/account-store.ts +80 -0
  143. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-inline.ts +100 -0
  144. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +161 -0
  145. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-resolve.ts +200 -0
  146. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-xmtp.ts +111 -0
  147. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +182 -0
  148. package/runtime/node_modules/@metro-labs/mcp/src/stations/attachments.ts +196 -0
  149. package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +82 -0
  150. package/runtime/node_modules/@metro-labs/mcp/src/stations/messaging-normalize.ts +88 -0
  151. package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +35 -0
  152. package/runtime/node_modules/@metro-labs/mcp/src/stations/station-runtime.ts +64 -0
  153. package/runtime/node_modules/@metro-labs/mcp/src/stations/types.ts +107 -0
  154. package/runtime/node_modules/@metro-labs/telegram/package.json +28 -0
  155. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +81 -0
  156. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +254 -0
  157. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +87 -0
  158. package/runtime/node_modules/@metro-labs/telegram/src/client.ts +77 -0
  159. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +141 -0
  160. package/runtime/node_modules/@metro-labs/telegram/src/group.ts +261 -0
  161. package/runtime/node_modules/@metro-labs/telegram/src/history.ts +52 -0
  162. package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +78 -0
  163. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +46 -0
  164. package/runtime/node_modules/@metro-labs/telegram/src/login.ts +166 -0
  165. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +80 -0
  166. package/runtime/node_modules/@metro-labs/telegram/src/members.ts +75 -0
  167. package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +22 -0
  168. package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +167 -0
  169. package/runtime/node_modules/@metro-labs/telegram/src/station.ts +24 -0
  170. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +7 -0
  171. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -0
  172. package/runtime/node_modules/@metro-labs/telegram-bot/package.json +25 -0
  173. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +120 -0
  174. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +295 -0
  175. package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
  176. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +172 -0
  177. package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +103 -0
  178. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
  179. package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
  180. package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +17 -0
  181. package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +48 -0
  182. package/runtime/node_modules/@metro-labs/telegram-bot/src/verify.ts +38 -0
  183. package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +3 -0
  184. package/runtime/node_modules/@metro-labs/webhook/package.json +23 -0
  185. package/runtime/node_modules/@metro-labs/webhook/src/station.ts +40 -0
  186. package/runtime/node_modules/@metro-labs/whatsapp/package.json +29 -0
  187. package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +67 -0
  188. package/runtime/node_modules/@metro-labs/whatsapp/src/ack.ts +101 -0
  189. package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +178 -0
  190. package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +72 -0
  191. package/runtime/node_modules/@metro-labs/whatsapp/src/auth-state.ts +129 -0
  192. package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +318 -0
  193. package/runtime/node_modules/@metro-labs/whatsapp/src/delivery.ts +49 -0
  194. package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +132 -0
  195. package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +57 -0
  196. package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +40 -0
  197. package/runtime/node_modules/@metro-labs/whatsapp/src/keys.ts +59 -0
  198. package/runtime/node_modules/@metro-labs/whatsapp/src/logger.ts +60 -0
  199. package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +176 -0
  200. package/runtime/node_modules/@metro-labs/whatsapp/src/media.ts +84 -0
  201. package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +22 -0
  202. package/runtime/node_modules/@metro-labs/whatsapp/src/outbox.ts +38 -0
  203. package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +141 -0
  204. package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +17 -0
  205. package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +98 -0
  206. package/runtime/node_modules/@metro-labs/whatsapp/src/types.ts +6 -0
  207. package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +5 -0
  208. package/runtime/node_modules/@metro-labs/xmtp/package.json +33 -0
  209. package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +113 -0
  210. package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +110 -0
  211. package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +299 -0
  212. package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +157 -0
  213. package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +87 -0
  214. package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +375 -0
  215. package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +107 -0
  216. package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +171 -0
  217. package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +239 -0
  218. package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +55 -0
  219. package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +333 -0
  220. package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +78 -0
  221. package/runtime/node_modules/@metro-labs/xmtp/src/group-members.ts +34 -0
  222. package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +56 -0
  223. package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +33 -0
  224. package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +125 -0
  225. package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +157 -0
  226. package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +40 -0
  227. package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +349 -0
  228. package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +13 -0
  229. package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +126 -0
  230. package/runtime/node_modules/@metro-labs/xmtp/src/tools.ts +212 -0
  231. package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +90 -0
  232. package/runtime/node_modules/@metro-labs/xmtp/src/verify.ts +84 -0
  233. package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +39 -0
  234. package/runtime/runtime.json +3 -0
  235. package/runtime/trains/.keep +0 -0
  236. package/LICENSE +0 -21
  237. package/README.md +0 -117
  238. package/dist/agents/claude.js +0 -207
  239. package/dist/agents/codex.js +0 -207
  240. package/dist/agents/types.js +0 -2
  241. package/dist/channels/discord.js +0 -104
  242. package/dist/channels/telegram.js +0 -189
  243. package/dist/helpers/scope-cache.js +0 -65
  244. package/dist/helpers/streaming.js +0 -209
  245. package/dist/helpers/telegram-format.js +0 -39
  246. package/dist/helpers/turn.js +0 -40
  247. package/dist/log.js +0 -10
  248. package/dist/orchestrator.js +0 -208
  249. package/dist/paths.js +0 -68
@@ -0,0 +1,105 @@
1
+ import { and, eq, isNull } from 'drizzle-orm';
2
+ import { getDb } from './client.js';
3
+ import { newId } from './ids.js';
4
+ import { agents, runtimes } from './schema.js';
5
+ import { ApiError } from '../daemon/api-error.js';
6
+
7
+ export interface RuntimeLease {
8
+ runtimeId: string;
9
+ agentId: string;
10
+ label: string;
11
+ }
12
+
13
+ export interface RuntimeView {
14
+ id: string;
15
+ label: string;
16
+ lastSeenAt: string | null;
17
+ }
18
+
19
+ const nowIso = (): string => new Date().toISOString();
20
+
21
+ const stale = (): ApiError =>
22
+ new ApiError('this runtime no longer holds the agent', 409);
23
+
24
+ export async function claimRuntime(
25
+ agentId: string,
26
+ label: string,
27
+ ): Promise<RuntimeLease> {
28
+ const db = getDb();
29
+ const id = newId();
30
+ return db.transaction(async (tx) => {
31
+ await tx
32
+ .insert(runtimes)
33
+ .values({ id, agentId, label, createdAt: nowIso(), lastSeenAt: nowIso() });
34
+ const rows = await tx
35
+ .update(agents)
36
+ .set({ runtimeId: id })
37
+ .where(eq(agents.id, agentId))
38
+ .returning({ id: agents.id });
39
+ if (rows[0] === undefined) throw new ApiError('no such agent', 404);
40
+ return { runtimeId: id, agentId, label };
41
+ });
42
+ }
43
+
44
+ export async function fenceRuntime(
45
+ runtimeId: string,
46
+ agentId: string,
47
+ ): Promise<void> {
48
+ const rows = await getDb()
49
+ .select({ holder: agents.runtimeId })
50
+ .from(agents)
51
+ .innerJoin(runtimes, eq(runtimes.id, agents.runtimeId))
52
+ .where(
53
+ and(
54
+ eq(agents.id, agentId),
55
+ eq(agents.runtimeId, runtimeId),
56
+ isNull(runtimes.revokedAt),
57
+ ),
58
+ );
59
+ if (rows[0] === undefined) throw stale();
60
+ }
61
+
62
+ export async function touchRuntime(runtimeId: string): Promise<void> {
63
+ await getDb()
64
+ .update(runtimes)
65
+ .set({ lastSeenAt: nowIso() })
66
+ .where(eq(runtimes.id, runtimeId));
67
+ }
68
+
69
+ export async function releaseRuntime(agentId: string): Promise<void> {
70
+ const db = getDb();
71
+ await db.transaction(async (tx) => {
72
+ await tx
73
+ .update(runtimes)
74
+ .set({ revokedAt: nowIso() })
75
+ .where(and(eq(runtimes.agentId, agentId), isNull(runtimes.revokedAt)));
76
+ await tx
77
+ .update(agents)
78
+ .set({ runtimeId: null })
79
+ .where(eq(agents.id, agentId));
80
+ });
81
+ }
82
+
83
+ export async function runtimeLabels(
84
+ agentIds: string[],
85
+ ): Promise<Map<string, string>> {
86
+ const out = new Map<string, string>();
87
+ for (const id of agentIds) {
88
+ const found = await runtimeFor(id);
89
+ if (found !== null) out.set(id, found.label);
90
+ }
91
+ return out;
92
+ }
93
+
94
+ export async function runtimeFor(agentId: string): Promise<RuntimeView | null> {
95
+ const rows = await getDb()
96
+ .select({
97
+ id: runtimes.id,
98
+ label: runtimes.label,
99
+ lastSeenAt: runtimes.lastSeenAt,
100
+ })
101
+ .from(runtimes)
102
+ .innerJoin(agents, eq(agents.runtimeId, runtimes.id))
103
+ .where(and(eq(agents.id, agentId), isNull(runtimes.revokedAt)));
104
+ return rows[0] ?? null;
105
+ }
@@ -0,0 +1,118 @@
1
+ import {
2
+ boolean,
3
+ jsonb,
4
+ pgTable,
5
+ text,
6
+ unique,
7
+ } from 'drizzle-orm/pg-core';
8
+
9
+ export const STATIONS = [
10
+ 'xmtp',
11
+ 'telegram-bot',
12
+ 'telegram',
13
+ 'discord-bot',
14
+ 'whatsapp',
15
+ 'webhook',
16
+ ] as const;
17
+
18
+ export type StationName = (typeof STATIONS)[number];
19
+
20
+ export const users = pgTable('users', {
21
+ id: text('id').primaryKey(),
22
+ email: text('email').unique(),
23
+ address: text('address').unique(),
24
+ });
25
+
26
+ export const PROJECT_ROLES = ['admin', 'member'] as const;
27
+
28
+ export type ProjectRole = (typeof PROJECT_ROLES)[number];
29
+
30
+ export const projects = pgTable('projects', {
31
+ id: text('id').primaryKey(),
32
+ name: text('name').notNull(),
33
+ ownerId: text('owner_id')
34
+ .notNull()
35
+ .references(() => users.id, { onDelete: 'restrict' }),
36
+ isDefault: boolean('is_default').notNull().default(false),
37
+ });
38
+
39
+ export const projectMembers = pgTable(
40
+ 'project_members',
41
+ {
42
+ id: text('id').primaryKey(),
43
+ projectId: text('project_id')
44
+ .notNull()
45
+ .references(() => projects.id, { onDelete: 'cascade' }),
46
+ userId: text('user_id')
47
+ .notNull()
48
+ .references(() => users.id, { onDelete: 'restrict' }),
49
+ role: text('role').$type<ProjectRole>().notNull(),
50
+ },
51
+ (t) => [unique('project_members_project_id_user_id_unique').on(t.projectId, t.userId)],
52
+ );
53
+
54
+ export const agents = pgTable('agents', {
55
+ id: text('id').primaryKey(),
56
+ name: text('name').notNull(),
57
+ projectId: text('project_id')
58
+ .notNull()
59
+ .references(() => projects.id, { onDelete: 'restrict' }),
60
+ key: text('key').unique(),
61
+ runtimeId: text('runtime_id'),
62
+ });
63
+
64
+ export const runtimes = pgTable('runtimes', {
65
+ id: text('id').primaryKey(),
66
+ agentId: text('agent_id')
67
+ .notNull()
68
+ .references(() => agents.id, { onDelete: 'cascade' }),
69
+ label: text('label').notNull(),
70
+ createdAt: text('created_at').notNull(),
71
+ lastSeenAt: text('last_seen_at'),
72
+ revokedAt: text('revoked_at'),
73
+ });
74
+
75
+ export const stations = pgTable('stations', {
76
+ id: text('id').primaryKey(),
77
+ agentId: text('agent_id').notNull(),
78
+ station: text('station').$type<StationName>().notNull(),
79
+ allowlist: text('allowlist').array().default(['*']),
80
+ config: jsonb('config').notNull(),
81
+ });
82
+
83
+ export const CONNECTOR_TRANSPORTS = ['http', 'sse'] as const;
84
+
85
+ export type ConnectorTransport = (typeof CONNECTOR_TRANSPORTS)[number];
86
+
87
+ export const connectors = pgTable(
88
+ 'connectors',
89
+ {
90
+ id: text('id').primaryKey(),
91
+ projectId: text('project_id')
92
+ .notNull()
93
+ .references(() => projects.id, { onDelete: 'restrict' }),
94
+ name: text('name').notNull(),
95
+ url: text('url').notNull(),
96
+ transport: text('transport').$type<ConnectorTransport>().notNull(),
97
+ config: jsonb('config').notNull(),
98
+ },
99
+ );
100
+
101
+ export const agentConnectors = pgTable(
102
+ 'agent_connectors',
103
+ {
104
+ id: text('id').primaryKey(),
105
+ agentId: text('agent_id')
106
+ .notNull()
107
+ .references(() => agents.id, { onDelete: 'cascade' }),
108
+ connectorId: text('connector_id')
109
+ .notNull()
110
+ .references(() => connectors.id, { onDelete: 'cascade' }),
111
+ },
112
+ (t) => [
113
+ unique('agent_connectors_agent_id_connector_id_unique').on(
114
+ t.agentId,
115
+ t.connectorId,
116
+ ),
117
+ ],
118
+ );
@@ -0,0 +1,45 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { tokenStorePath } from '@metro-labs/whatsapp/tokens';
3
+ import { errMsg, log } from '../daemon/log.js';
4
+ import { writeSecure } from '../daemon/secure-fs.js';
5
+
6
+ export const PREVIOUS_ACCOUNT_ID = 'previousAccountId';
7
+
8
+ export interface CarriedStation {
9
+ station: string;
10
+ id: string;
11
+ config: Record<string, unknown>;
12
+ }
13
+
14
+ export function previousAccountId(
15
+ config: Record<string, unknown>,
16
+ ): string | undefined {
17
+ const raw = config[PREVIOUS_ACCOUNT_ID];
18
+ return typeof raw === 'string' && raw !== '' ? raw : undefined;
19
+ }
20
+
21
+ export function carryTokenStores(list: CarriedStation[]): number {
22
+ let carried = 0;
23
+ for (const s of list) {
24
+ if (s.station !== 'whatsapp') continue;
25
+ const previous = previousAccountId(s.config);
26
+ if (previous === undefined || previous === s.id) continue;
27
+ const from = tokenStorePath(previous);
28
+ const to = tokenStorePath(s.id);
29
+ if (!existsSync(from) || existsSync(to)) continue;
30
+ try {
31
+ writeSecure(to, readFileSync(from, 'utf8'));
32
+ carried += 1;
33
+ log.info(
34
+ { from: previous, to: s.id },
35
+ 'db: carried the whatsapp token store to the station id',
36
+ );
37
+ } catch (err) {
38
+ log.error(
39
+ { from: previous, to: s.id, err: errMsg(err) },
40
+ 'db: could not carry the whatsapp token store',
41
+ );
42
+ }
43
+ }
44
+ return carried;
45
+ }
@@ -0,0 +1,72 @@
1
+ import { eq, type SQL } from 'drizzle-orm';
2
+ import { ApiError } from '../daemon/api-error.js';
3
+ import { getDb } from './client.js';
4
+ import { newId } from './ids.js';
5
+ import { users } from './schema.js';
6
+
7
+ const UNIQUE_VIOLATION = '23505';
8
+ const ADDRESS_RE = /^0x[0-9a-f]{40}$/;
9
+
10
+ export class UserError extends ApiError {}
11
+
12
+ const CAUSE_DEPTH = 5;
13
+
14
+ export function isUniqueViolation(err: unknown): boolean {
15
+ let current: unknown = err;
16
+ for (let depth = 0; depth < CAUSE_DEPTH; depth += 1) {
17
+ if (typeof current !== 'object' || current === null) return false;
18
+ const { code, cause } = current as { code?: unknown; cause?: unknown };
19
+ if (code === UNIQUE_VIOLATION) return true;
20
+ current = cause;
21
+ }
22
+ return false;
23
+ }
24
+
25
+ export function normalizeEmail(raw: string): string {
26
+ return raw.trim().toLowerCase();
27
+ }
28
+
29
+ export function normalizeAddress(raw: string): string | null {
30
+ const address = raw.trim().toLowerCase();
31
+ return ADDRESS_RE.test(address) ? address : null;
32
+ }
33
+
34
+ async function userIdWhere(where: SQL): Promise<string | null> {
35
+ const rows = await getDb().select({ id: users.id }).from(users).where(where);
36
+ return rows[0]?.id ?? null;
37
+ }
38
+
39
+ export async function userIdForSubject(subject: string): Promise<string | null> {
40
+ const address = normalizeAddress(subject);
41
+ if (address !== null) return userIdWhere(eq(users.address, address));
42
+ return userIdWhere(eq(users.email, normalizeEmail(subject)));
43
+ }
44
+
45
+ export async function resolveUserId(
46
+ insert: () => Promise<string | undefined>,
47
+ lookup: () => Promise<string | null>,
48
+ ): Promise<string> {
49
+ const inserted = await insert();
50
+ if (inserted !== undefined) return inserted;
51
+ const existing = await lookup();
52
+ if (existing === null)
53
+ throw new UserError('user lookup returned no id', 500);
54
+ return existing;
55
+ }
56
+
57
+ export async function ensureUserByAddress(raw: string): Promise<string> {
58
+ const address = normalizeAddress(raw);
59
+ if (address === null)
60
+ throw new UserError('an Ethereum address is required', 400);
61
+ return resolveUserId(
62
+ async () => {
63
+ const rows = await getDb()
64
+ .insert(users)
65
+ .values({ id: newId(), address })
66
+ .onConflictDoNothing({ target: users.address })
67
+ .returning({ id: users.id });
68
+ return rows[0]?.id;
69
+ },
70
+ () => userIdWhere(eq(users.address, address)),
71
+ );
72
+ }
@@ -0,0 +1,18 @@
1
+ import { and, eq, sql } from 'drizzle-orm';
2
+ import { getDb } from './client.js';
3
+ import { stations } from './schema.js';
4
+
5
+ export async function writeWhatsappCredentials(
6
+ accountId: string,
7
+ credentials: unknown,
8
+ ): Promise<void> {
9
+ const db = getDb();
10
+ await db
11
+ .update(stations)
12
+ .set({
13
+ config: sql`${stations.config} || ${JSON.stringify({ credentials })}::jsonb`,
14
+ })
15
+ .where(
16
+ and(eq(stations.station, 'whatsapp'), eq(stations.id, accountId)),
17
+ );
18
+ }
@@ -0,0 +1,141 @@
1
+ import { forwardTrainCall } from '../daemon/train-call.js';
2
+ import {
3
+ accountStationNames,
4
+ stationByName,
5
+ } from '../stations/registry.js';
6
+ import { listEndpoints } from '../daemon/tunnel.js';
7
+ import { hookUrl } from '../stations/attach.js';
8
+ import { agentIdForAccount, knownAccounts, type KnownAccount } from '../db/agent-map.js';
9
+ import { stationRunsHere } from '../db/materialize.js';
10
+ import { type StationName } from '../db/schema.js';
11
+
12
+ const accountId = (acc: unknown): string | undefined => {
13
+ const id = (acc as { id?: unknown }).id;
14
+ return typeof id === 'string' ? id : undefined;
15
+ };
16
+
17
+ const asRecord = (acc: unknown): Record<string, unknown> | undefined =>
18
+ typeof acc === 'object' && acc !== null && !Array.isArray(acc)
19
+ ? (acc as Record<string, unknown>)
20
+ : undefined;
21
+
22
+ function withAgentId(station: string, acc: unknown): unknown {
23
+ const rec = asRecord(acc);
24
+ if (rec === undefined) return acc;
25
+ const id = typeof rec.id === 'string' ? rec.id : undefined;
26
+ if (id === undefined) return acc;
27
+ const agentId = agentIdForAccount(station, id);
28
+ return agentId === undefined ? acc : { ...rec, agentId };
29
+ }
30
+
31
+ export function attachAgentIds(
32
+ byStation: Record<string, unknown[]>,
33
+ ): Record<string, unknown[]> {
34
+ const out: Record<string, unknown[]> = {};
35
+ for (const [station, list] of Object.entries(byStation))
36
+ out[station] = list.map((acc) => withAgentId(station, acc));
37
+ return out;
38
+ }
39
+
40
+ export function scopeAccountsByAgent(
41
+ byStation: Record<string, unknown[]>,
42
+ allowed: Set<string>,
43
+ ): Record<string, unknown[]> {
44
+ const out: Record<string, unknown[]> = {};
45
+ for (const [station, list] of Object.entries(byStation)) {
46
+ out[station] = list.filter((acc) => {
47
+ const id = accountId(acc);
48
+ if (id === undefined) return false;
49
+ const agentId = agentIdForAccount(station, id);
50
+ return agentId !== undefined && allowed.has(agentId);
51
+ });
52
+ }
53
+ return out;
54
+ }
55
+
56
+ const hasTrain = (station: string): boolean =>
57
+ stationByName(station)?.hasTrain !== false;
58
+
59
+ function inCoreAccounts(station: string): unknown[] {
60
+ if (station !== 'webhook') return [];
61
+ return listEndpoints().flatMap((e) =>
62
+ e.secret === undefined || e.webhookId === undefined
63
+ ? []
64
+ : [
65
+ {
66
+ id: e.id,
67
+ handle: `/api/webhooks/${e.webhookId}`,
68
+ endpoint: hookUrl(e.webhookId, e.secret),
69
+ },
70
+ ],
71
+ );
72
+ }
73
+
74
+ export interface ScopedAccounts {
75
+ accounts: Record<string, unknown[]>;
76
+ unavailable: string[];
77
+ }
78
+
79
+ async function liveAccounts(
80
+ station: string,
81
+ ): Promise<{ rows: unknown[]; reachable: boolean }> {
82
+ if (!hasTrain(station))
83
+ return { rows: inCoreAccounts(station), reachable: true };
84
+ if (!stationRunsHere(station as StationName))
85
+ return { rows: [], reachable: true };
86
+ try {
87
+ const resp = await forwardTrainCall(station, 'accounts', {});
88
+ const list = (resp.result as { accounts?: unknown[] } | undefined)?.accounts;
89
+ return { rows: Array.isArray(list) ? list : [], reachable: true };
90
+ } catch {
91
+ return { rows: [], reachable: false };
92
+ }
93
+ }
94
+
95
+ function mergeKnown(
96
+ station: string,
97
+ live: unknown[],
98
+ known: KnownAccount[],
99
+ ): unknown[] {
100
+ const seen = new Set(
101
+ live.flatMap((row) => {
102
+ const id = accountId(row);
103
+ return id === undefined ? [] : [id];
104
+ }),
105
+ );
106
+ const missing = known
107
+ .filter((r) => r.station === station && !seen.has(r.id))
108
+ .map((r) => ({ id: r.id }));
109
+ return [...live, ...missing];
110
+ }
111
+
112
+ async function loadStations(): Promise<ScopedAccounts> {
113
+ const unavailable: string[] = [];
114
+ const accounts: Record<string, unknown[]> = {};
115
+ const known = knownAccounts();
116
+ await Promise.all(
117
+ accountStationNames().map(async (station) => {
118
+ const { rows, reachable } = await liveAccounts(station);
119
+ if (!reachable) unavailable.push(station);
120
+ accounts[station] = hasTrain(station) ? mergeKnown(station, rows, known) : rows;
121
+ }),
122
+ );
123
+ return { accounts, unavailable };
124
+ }
125
+
126
+ export async function gatherAccounts(
127
+ allowedAgents?: Set<string>,
128
+ ): Promise<Record<string, unknown[]>> {
129
+ const { accounts } = await loadStations();
130
+ return allowedAgents ? scopeAccountsByAgent(accounts, allowedAgents) : accounts;
131
+ }
132
+
133
+ export async function gatherAccountsForAgents(
134
+ allowed: Set<string>,
135
+ ): Promise<ScopedAccounts> {
136
+ const { accounts, unavailable } = await loadStations();
137
+ return {
138
+ accounts: attachAgentIds(scopeAccountsByAgent(accounts, allowed)),
139
+ unavailable,
140
+ };
141
+ }
@@ -0,0 +1,203 @@
1
+ import { stationForLine } from '../stations/registry.js';
2
+ import { kindOf, toCanonical } from '../stations/attachments.js';
3
+ import {
4
+ cleanupAttachments,
5
+ resolveAttachments,
6
+ type ResolvedAttachment,
7
+ } from '../stations/attach-resolve.js';
8
+ import type { CanonicalAttachment, ToolResult } from '../stations/types.js';
9
+ import { errResult, makeCtx, ok, okJson, toErr } from './ctx.js';
10
+ import { allowedAgents, currentIdentity } from './request-identity.js';
11
+ import { str } from './str.js';
12
+
13
+ type Station = NonNullable<ReturnType<typeof stationForLine>>;
14
+
15
+ interface MessageArgs {
16
+ line: string;
17
+ a: Record<string, unknown>;
18
+ ctx: ReturnType<typeof makeCtx>;
19
+ station: Station;
20
+ }
21
+
22
+ const messageIdOf = (response: { result: unknown }): string | undefined => {
23
+ const id = (response.result as { messageId?: unknown } | null)?.messageId;
24
+ return typeof id === 'string' && id !== '' ? id : undefined;
25
+ };
26
+
27
+ const withId = (text: string, messageId: string | undefined): string =>
28
+ messageId === undefined ? text : `${text} — message_id: ${messageId}`;
29
+
30
+ interface Sent {
31
+ labels: string[];
32
+ messageId?: string;
33
+ }
34
+
35
+ const deliveredLabels = (response: { result: unknown }): string[] => {
36
+ const list = (response.result as { attachments?: unknown } | null)
37
+ ?.attachments;
38
+ return Array.isArray(list)
39
+ ? list.filter((x): x is string => typeof x === 'string')
40
+ : [];
41
+ };
42
+
43
+ function assertDelivered(
44
+ station: Station,
45
+ delivered: string[],
46
+ atts: ResolvedAttachment[],
47
+ ): void {
48
+ if (delivered.length < atts.length)
49
+ throw new Error(
50
+ `${station.name} delivered ${delivered.length} of ${atts.length} attachment(s); ` +
51
+ 'the message text may still have been sent',
52
+ );
53
+ }
54
+
55
+ async function sendNative(
56
+ m: MessageArgs,
57
+ text: string | undefined,
58
+ replyTo: string | undefined,
59
+ atts: ResolvedAttachment[],
60
+ ): Promise<Sent> {
61
+ const { line, ctx, station } = m;
62
+ const labels: string[] = [];
63
+ let messageId: string | undefined;
64
+ if (text) {
65
+ const response = await ctx.call(
66
+ 'send',
67
+ replyTo ? { line, text, replyTo } : { line, text },
68
+ );
69
+ messageId = messageIdOf(response);
70
+ labels.push('text');
71
+ }
72
+ if (!atts.length) return { labels, messageId };
73
+ const delivered = station.sendAttachments
74
+ ? await station.sendAttachments(line, atts, ctx)
75
+ : [];
76
+ assertDelivered(station, delivered, atts);
77
+ labels.push(...delivered);
78
+ return { labels, messageId };
79
+ }
80
+
81
+ async function sendForwarded(
82
+ m: MessageArgs,
83
+ text: string | undefined,
84
+ replyTo: string | undefined,
85
+ atts: ResolvedAttachment[],
86
+ ): Promise<Sent> {
87
+ const { line, ctx, station } = m;
88
+ const args: Record<string, unknown> = { line };
89
+ if (text) args.text = text;
90
+ if (replyTo) args.replyTo = replyTo;
91
+ if (atts.length) args.attachments = atts.map(toCanonical);
92
+ const response = await ctx.call('send', args);
93
+ const messageId = messageIdOf(response);
94
+ const labels: string[] = [];
95
+ if (text) labels.push('text');
96
+ if (!atts.length) return { labels, messageId };
97
+ const delivered = deliveredLabels(response);
98
+ assertDelivered(station, delivered, atts);
99
+ labels.push(...delivered);
100
+ return { labels, messageId };
101
+ }
102
+
103
+ const unsupported = (station: Station, atts: CanonicalAttachment[]): string =>
104
+ `${station.name} cannot send attachments (${atts
105
+ .map((a) => kindOf(a.mime ?? '', a.path ?? a.url ?? a.name ?? ''))
106
+ .join(', ')}); send a link in \`text\` instead`;
107
+
108
+ async function handleSend(m: MessageArgs): Promise<ToolResult> {
109
+ const text = m.a.text as string | undefined;
110
+ const replyTo = m.a.reply_to as string | undefined;
111
+ const requested = (m.a.attachments as CanonicalAttachment[] | undefined) ?? [];
112
+ if (!text && !requested.length)
113
+ return errResult('send requires `text` or `attachments`');
114
+ if (requested.length && m.station.attachmentMode === 'none')
115
+ return errResult(unsupported(m.station, requested));
116
+ const atts = await resolveAttachments(requested, {
117
+ allowed: allowedAgents(currentIdentity()),
118
+ });
119
+ try {
120
+ const native =
121
+ m.station.attachmentMode === 'native' &&
122
+ typeof m.station.sendAttachments === 'function';
123
+ const sent = native
124
+ ? await sendNative(m, text, replyTo, atts)
125
+ : await sendForwarded(m, text, replyTo, atts);
126
+ if (!sent.labels.length)
127
+ return errResult('send requires `text` or `attachments`');
128
+ return ok(withId(`sent: ${sent.labels.join(', ')}`, sent.messageId));
129
+ } finally {
130
+ await cleanupAttachments(atts);
131
+ }
132
+ }
133
+
134
+ async function handleRead({ line, a, ctx }: MessageArgs): Promise<ToolResult> {
135
+ const args: Record<string, unknown> = { line };
136
+ if (typeof a.limit === 'number') args.limit = a.limit;
137
+ if (a.before) args.before = str(a.before);
138
+ if (a.since) args.since = str(a.since);
139
+ return okJson(await ctx.call('read', args));
140
+ }
141
+
142
+ type MessageHandler = (m: MessageArgs) => Promise<ToolResult>;
143
+
144
+ interface VerbSpec {
145
+ args: readonly (readonly [string, string])[];
146
+ success: string;
147
+ }
148
+
149
+ const MESSAGE_VERBS: Record<string, VerbSpec> = {
150
+ reply: { args: [['message_id', 'replyTo'], ['text', 'text']], success: 'replied' },
151
+ react: { args: [['message_id', 'messageId'], ['emoji', 'emoji']], success: 'reacted' },
152
+ unreact: {
153
+ args: [['message_id', 'messageId'], ['emoji', 'emoji']],
154
+ success: 'reaction removed',
155
+ },
156
+ edit: { args: [['message_id', 'messageId'], ['text', 'text']], success: 'edited' },
157
+ delete: { args: [['message_id', 'messageId']], success: 'deleted' },
158
+ };
159
+
160
+ function makeVerbHandler(verb: string, spec: VerbSpec): MessageHandler {
161
+ return async ({ line, a, ctx }) => {
162
+ const payload: Record<string, unknown> = { line };
163
+ for (const [snake, camel] of spec.args) {
164
+ const value = str(a[snake]);
165
+ if (!value) {
166
+ const fields = spec.args.map(([snakeName]) => `\`${snakeName}\``).join(' and ');
167
+ return errResult(`${verb} requires ${fields}`);
168
+ }
169
+ payload[camel] = value;
170
+ }
171
+ const response = await ctx.call(verb, payload);
172
+ return ok(withId(spec.success, messageIdOf(response)));
173
+ };
174
+ }
175
+
176
+ const MESSAGE_HANDLERS: Record<string, MessageHandler> = {
177
+ send: handleSend,
178
+ read: handleRead,
179
+ ...Object.fromEntries(
180
+ Object.entries(MESSAGE_VERBS).map(([verb, spec]) => [verb, makeVerbHandler(verb, spec)]),
181
+ ),
182
+ };
183
+
184
+ export async function dispatchMessageTool(
185
+ name: string,
186
+ a: Record<string, unknown>,
187
+ ): Promise<ToolResult> {
188
+ const line = str(a.line);
189
+ if (!line) return errResult(`${name} requires \`line\``);
190
+ const station = stationForLine(line);
191
+ if (!station || station.messageVerbs.size === 0) {
192
+ return errResult(
193
+ `${station?.name ?? 'these'} lines do not support outbound messaging (send/reply/react/unreact/edit/delete/read).`,
194
+ );
195
+ }
196
+ const handler = MESSAGE_HANDLERS[name];
197
+ if (!handler) return errResult(`unknown tool: ${name}`);
198
+ try {
199
+ return await handler({ line, a, ctx: makeCtx(station.name), station });
200
+ } catch (e) {
201
+ return toErr(name, e);
202
+ }
203
+ }