@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,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,131 @@
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',
12
+ 'telegram-user',
13
+ 'discord',
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').notNull().unique(),
23
+ });
24
+
25
+ export const PROJECT_ROLES = ['admin', 'member'] as const;
26
+
27
+ export type ProjectRole = (typeof PROJECT_ROLES)[number];
28
+
29
+ export const projects = pgTable('projects', {
30
+ id: text('id').primaryKey(),
31
+ name: text('name').notNull(),
32
+ ownerId: text('owner_id')
33
+ .notNull()
34
+ .references(() => users.id, { onDelete: 'restrict' }),
35
+ isDefault: boolean('is_default').notNull().default(false),
36
+ });
37
+
38
+ export const projectMembers = pgTable(
39
+ 'project_members',
40
+ {
41
+ id: text('id').primaryKey(),
42
+ projectId: text('project_id')
43
+ .notNull()
44
+ .references(() => projects.id, { onDelete: 'cascade' }),
45
+ userId: text('user_id')
46
+ .notNull()
47
+ .references(() => users.id, { onDelete: 'restrict' }),
48
+ role: text('role').$type<ProjectRole>().notNull(),
49
+ },
50
+ (t) => [unique('project_members_project_id_user_id_unique').on(t.projectId, t.userId)],
51
+ );
52
+
53
+ export const agents = pgTable('agents', {
54
+ id: text('id').primaryKey(),
55
+ name: text('name').notNull(),
56
+ projectId: text('project_id')
57
+ .notNull()
58
+ .references(() => projects.id, { onDelete: 'restrict' }),
59
+ key: text('key').unique(),
60
+ runtimeId: text('runtime_id'),
61
+ });
62
+
63
+ export const runtimes = pgTable('runtimes', {
64
+ id: text('id').primaryKey(),
65
+ agentId: text('agent_id')
66
+ .notNull()
67
+ .references(() => agents.id, { onDelete: 'cascade' }),
68
+ label: text('label').notNull(),
69
+ createdAt: text('created_at').notNull(),
70
+ lastSeenAt: text('last_seen_at'),
71
+ revokedAt: text('revoked_at'),
72
+ });
73
+
74
+ export const stations = pgTable('stations', {
75
+ id: text('id').primaryKey(),
76
+ agentId: text('agent_id').notNull(),
77
+ station: text('station').$type<StationName>().notNull(),
78
+ allowlist: text('allowlist').array().default(['*']),
79
+ config: jsonb('config').notNull(),
80
+ });
81
+
82
+ export const CONNECTOR_TRANSPORTS = ['http', 'sse'] as const;
83
+
84
+ export type ConnectorTransport = (typeof CONNECTOR_TRANSPORTS)[number];
85
+
86
+ export const connectors = pgTable(
87
+ 'connectors',
88
+ {
89
+ id: text('id').primaryKey(),
90
+ projectId: text('project_id')
91
+ .notNull()
92
+ .references(() => projects.id, { onDelete: 'restrict' }),
93
+ name: text('name').notNull(),
94
+ url: text('url').notNull(),
95
+ transport: text('transport').$type<ConnectorTransport>().notNull(),
96
+ config: jsonb('config').notNull(),
97
+ },
98
+ );
99
+
100
+ export const collections = pgTable(
101
+ 'collections',
102
+ {
103
+ id: text('id').primaryKey(),
104
+ projectId: text('project_id')
105
+ .notNull()
106
+ .references(() => projects.id, { onDelete: 'restrict' }),
107
+ name: text('name').notNull(),
108
+ },
109
+ (t) => [
110
+ unique('collections_project_id_name_unique').on(t.projectId, t.name),
111
+ ],
112
+ );
113
+
114
+ export const collectionItems = pgTable(
115
+ 'collection_items',
116
+ {
117
+ id: text('id').primaryKey(),
118
+ collectionId: text('collection_id')
119
+ .notNull()
120
+ .references(() => collections.id, { onDelete: 'cascade' }),
121
+ connectorId: text('connector_id')
122
+ .notNull()
123
+ .references(() => connectors.id, { onDelete: 'cascade' }),
124
+ },
125
+ (t) => [
126
+ unique('collection_items_collection_id_connector_id_unique').on(
127
+ t.collectionId,
128
+ t.connectorId,
129
+ ),
130
+ ],
131
+ );
@@ -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,57 @@
1
+ import { eq } 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
+
9
+ export class UserError extends ApiError {}
10
+
11
+ export function isUniqueViolation(err: unknown): boolean {
12
+ return (
13
+ typeof err === 'object' &&
14
+ err !== null &&
15
+ (err as { code?: unknown }).code === UNIQUE_VIOLATION
16
+ );
17
+ }
18
+
19
+ export function normalizeEmail(raw: string): string {
20
+ return raw.trim().toLowerCase();
21
+ }
22
+
23
+ export async function userIdForEmail(rawEmail: string): Promise<string | null> {
24
+ const email = normalizeEmail(rawEmail);
25
+ const rows = await getDb()
26
+ .select({ id: users.id })
27
+ .from(users)
28
+ .where(eq(users.email, email));
29
+ return rows[0]?.id ?? null;
30
+ }
31
+
32
+ export async function resolveUserId(
33
+ insert: () => Promise<string | undefined>,
34
+ lookup: () => Promise<string | null>,
35
+ ): Promise<string> {
36
+ const inserted = await insert();
37
+ if (inserted !== undefined) return inserted;
38
+ const existing = await lookup();
39
+ if (existing === null)
40
+ throw new UserError('user lookup returned no id', 500);
41
+ return existing;
42
+ }
43
+
44
+ export async function ensureUser(rawEmail: string): Promise<string> {
45
+ const email = normalizeEmail(rawEmail);
46
+ return resolveUserId(
47
+ async () => {
48
+ const rows = await getDb()
49
+ .insert(users)
50
+ .values({ id: newId(), email })
51
+ .onConflictDoNothing({ target: users.email })
52
+ .returning({ id: users.id });
53
+ return rows[0]?.id;
54
+ },
55
+ () => userIdForEmail(email),
56
+ );
57
+ }
@@ -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,145 @@
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 } from '../db/agent-map.js';
9
+ import {
10
+ listAllStations,
11
+ stationRunsHere,
12
+ type StationRecord,
13
+ } from '../db/materialize.js';
14
+ import { type StationName } from '../db/schema.js';
15
+
16
+ const accountId = (acc: unknown): string | undefined => {
17
+ const id = (acc as { id?: unknown }).id;
18
+ return typeof id === 'string' ? id : undefined;
19
+ };
20
+
21
+ const asRecord = (acc: unknown): Record<string, unknown> | undefined =>
22
+ typeof acc === 'object' && acc !== null && !Array.isArray(acc)
23
+ ? (acc as Record<string, unknown>)
24
+ : undefined;
25
+
26
+ function withAgentId(station: string, acc: unknown): unknown {
27
+ const rec = asRecord(acc);
28
+ if (rec === undefined) return acc;
29
+ const id = typeof rec.id === 'string' ? rec.id : undefined;
30
+ if (id === undefined) return acc;
31
+ const agentId = agentIdForAccount(station, id);
32
+ return agentId === undefined ? acc : { ...rec, agentId };
33
+ }
34
+
35
+ export function attachAgentIds(
36
+ byStation: Record<string, unknown[]>,
37
+ ): Record<string, unknown[]> {
38
+ const out: Record<string, unknown[]> = {};
39
+ for (const [station, list] of Object.entries(byStation))
40
+ out[station] = list.map((acc) => withAgentId(station, acc));
41
+ return out;
42
+ }
43
+
44
+ export function scopeAccountsByAgent(
45
+ byStation: Record<string, unknown[]>,
46
+ allowed: Set<string>,
47
+ ): Record<string, unknown[]> {
48
+ const out: Record<string, unknown[]> = {};
49
+ for (const [station, list] of Object.entries(byStation)) {
50
+ out[station] = list.filter((acc) => {
51
+ const id = accountId(acc);
52
+ if (id === undefined) return false;
53
+ const agentId = agentIdForAccount(station, id);
54
+ return agentId !== undefined && allowed.has(agentId);
55
+ });
56
+ }
57
+ return out;
58
+ }
59
+
60
+ const hasTrain = (station: string): boolean =>
61
+ stationByName(station)?.hasTrain !== false;
62
+
63
+ function inCoreAccounts(station: string): unknown[] {
64
+ if (station !== 'webhook') return [];
65
+ return listEndpoints().flatMap((e) =>
66
+ e.secret === undefined || e.webhookId === undefined
67
+ ? []
68
+ : [
69
+ {
70
+ id: e.id,
71
+ handle: `/api/webhooks/${e.webhookId}`,
72
+ endpoint: hookUrl(e.webhookId, e.secret),
73
+ },
74
+ ],
75
+ );
76
+ }
77
+
78
+ export interface ScopedAccounts {
79
+ accounts: Record<string, unknown[]>;
80
+ unavailable: string[];
81
+ }
82
+
83
+ async function liveAccounts(
84
+ station: string,
85
+ ): Promise<{ rows: unknown[]; reachable: boolean }> {
86
+ if (!hasTrain(station))
87
+ return { rows: inCoreAccounts(station), reachable: true };
88
+ if (!stationRunsHere(station as StationName))
89
+ return { rows: [], reachable: true };
90
+ try {
91
+ const resp = await forwardTrainCall(station, 'accounts', {});
92
+ const list = (resp.result as { accounts?: unknown[] } | undefined)?.accounts;
93
+ return { rows: Array.isArray(list) ? list : [], reachable: true };
94
+ } catch {
95
+ return { rows: [], reachable: false };
96
+ }
97
+ }
98
+
99
+ function mergeKnown(
100
+ station: string,
101
+ live: unknown[],
102
+ known: StationRecord[],
103
+ ): unknown[] {
104
+ const seen = new Set(
105
+ live.flatMap((row) => {
106
+ const id = accountId(row);
107
+ return id === undefined ? [] : [id];
108
+ }),
109
+ );
110
+ const missing = known
111
+ .filter((r) => r.station === station && !seen.has(r.id))
112
+ .map((r) => ({ id: r.id }));
113
+ return [...live, ...missing];
114
+ }
115
+
116
+ async function loadStations(): Promise<ScopedAccounts> {
117
+ const unavailable: string[] = [];
118
+ const accounts: Record<string, unknown[]> = {};
119
+ const known = await listAllStations();
120
+ await Promise.all(
121
+ accountStationNames().map(async (station) => {
122
+ const { rows, reachable } = await liveAccounts(station);
123
+ if (!reachable) unavailable.push(station);
124
+ accounts[station] = mergeKnown(station, rows, known);
125
+ }),
126
+ );
127
+ return { accounts, unavailable };
128
+ }
129
+
130
+ export async function gatherAccounts(
131
+ allowedAgents?: Set<string>,
132
+ ): Promise<Record<string, unknown[]>> {
133
+ const { accounts } = await loadStations();
134
+ return allowedAgents ? scopeAccountsByAgent(accounts, allowedAgents) : accounts;
135
+ }
136
+
137
+ export async function gatherAccountsForAgents(
138
+ allowed: Set<string>,
139
+ ): Promise<ScopedAccounts> {
140
+ const { accounts, unavailable } = await loadStations();
141
+ return {
142
+ accounts: attachAgentIds(scopeAccountsByAgent(accounts, allowed)),
143
+ unavailable,
144
+ };
145
+ }
@@ -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
+ }