@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,5 @@
1
+ export function str(v: unknown): string {
2
+ if (typeof v === 'string') return v;
3
+ if (typeof v === 'number' || typeof v === 'boolean') return String(v);
4
+ return '';
5
+ }
@@ -0,0 +1,212 @@
1
+ import { createHash } from 'node:crypto';
2
+ import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import {
4
+ ListToolsRequestSchema,
5
+ CallToolRequestSchema,
6
+ } from '@modelcontextprotocol/sdk/types.js';
7
+ import { gatherAccounts } from './accounts.js';
8
+ import {
9
+ STATIONS,
10
+ accountStationCapabilities,
11
+ } from '../stations/registry.js';
12
+ import type { Station, StationTool, ToolResult } from '../stations/types.js';
13
+ import { COMMON_TOOLS, LIST_ACCOUNTS_TOOL } from './tool-schemas.js';
14
+ import { errResult, makeCtx, okJson, toErr } from './ctx.js';
15
+ import { dispatchMessageTool } from './call-tools.js';
16
+ import { dispatchListMembers } from './member-tools.js';
17
+ import {
18
+ dispatchAddMembers,
19
+ dispatchCreateGroup,
20
+ dispatchInviteLink,
21
+ dispatchRemoveMembers,
22
+ } from './group-tools.js';
23
+ import { dispatchCreateUpload } from './upload-tool.js';
24
+ import { callTargetDenied, lineTargetDenied } from '../db/agent-scope.js';
25
+ import { SOURCE_KEYS } from '../stations/attach-resolve.js';
26
+ import { decodedLengthOf } from '../stations/attach-inline.js';
27
+ import { log } from '../daemon/log.js';
28
+ import {
29
+ allowedAgents,
30
+ currentIdentity,
31
+ type RequestIdentity,
32
+ } from './request-identity.js';
33
+ import { str } from './str.js';
34
+
35
+ const STATION_TOOLS = new Map<
36
+ string,
37
+ { station: Station; tool: StationTool }
38
+ >();
39
+ for (const s of STATIONS)
40
+ for (const t of s.tools) STATION_TOOLS.set(t.name, { station: s, tool: t });
41
+
42
+ const CORE_DISPATCH: Record<
43
+ string,
44
+ (a: Record<string, unknown>) => Promise<ToolResult>
45
+ > = {
46
+ list_members: dispatchListMembers,
47
+ create_group: dispatchCreateGroup,
48
+ add_members: dispatchAddMembers,
49
+ remove_members: dispatchRemoveMembers,
50
+ export_invite: dispatchInviteLink,
51
+ create_upload: dispatchCreateUpload,
52
+ };
53
+
54
+ const toolList = (): { tools: unknown[] } => ({
55
+ tools: [
56
+ ...COMMON_TOOLS,
57
+ ...STATIONS.flatMap((s) =>
58
+ s.tools.map((t) => ({
59
+ name: t.name,
60
+ description: t.description,
61
+ inputSchema: t.inputSchema,
62
+ })),
63
+ ),
64
+ LIST_ACCOUNTS_TOOL,
65
+ ],
66
+ });
67
+
68
+ let schemaSignature: string | undefined;
69
+
70
+ export const toolSchemaSignature = (): string => {
71
+ schemaSignature ??= createHash('sha256')
72
+ .update(JSON.stringify(toolList()))
73
+ .digest('hex')
74
+ .slice(0, 16);
75
+ return schemaSignature;
76
+ };
77
+
78
+ async function handleListAccounts(
79
+ identity: RequestIdentity | undefined,
80
+ ): Promise<ToolResult> {
81
+ try {
82
+ return okJson({
83
+ accounts: await gatherAccounts(allowedAgents(identity)),
84
+ capabilities: accountStationCapabilities(),
85
+ });
86
+ } catch (e) {
87
+ return errResult(`metro list_accounts failed: ${String(e)}`);
88
+ }
89
+ }
90
+
91
+ function stationForTool(
92
+ name: string,
93
+ args: Record<string, unknown>,
94
+ ): string | undefined {
95
+ if (name === 'create_group') return str(args.station) || undefined;
96
+ return STATION_TOOLS.get(name)?.station.name;
97
+ }
98
+
99
+ export function scopeDenied(
100
+ identity: RequestIdentity | undefined,
101
+ name: string,
102
+ args: Record<string, unknown>,
103
+ ): boolean {
104
+ const allowed = allowedAgents(identity);
105
+ const station = stationForTool(name, args);
106
+ if (station !== undefined) return callTargetDenied(allowed, station, args);
107
+ return lineTargetDenied(allowed, args);
108
+ }
109
+
110
+ const sourceOf = (entry: unknown): string => {
111
+ const att = (entry ?? {}) as Record<string, unknown>;
112
+ const named = SOURCE_KEYS.filter(
113
+ (k) => typeof att[k] === 'string' && att[k] !== '',
114
+ );
115
+ if (named.length === 0) return 'none';
116
+ return named
117
+ .map((k) =>
118
+ k === 'data' ? `data(${decodedLengthOf(String(att.data))}B)` : k,
119
+ )
120
+ .join('+');
121
+ };
122
+
123
+ export const attachmentShape = (
124
+ a: Record<string, unknown>,
125
+ ): string[] | undefined =>
126
+ Array.isArray(a.attachments) ? a.attachments.map(sourceOf) : undefined;
127
+
128
+ function logToolFailure(
129
+ name: string,
130
+ a: Record<string, unknown>,
131
+ result: ToolResult,
132
+ ): void {
133
+ log.warn(
134
+ {
135
+ tool: name,
136
+ agents: [...allowedAgents(currentIdentity())],
137
+ line: str(a.line),
138
+ attachments: attachmentShape(a),
139
+ err: result.content[0]?.text,
140
+ },
141
+ 'mcp: tool call failed',
142
+ );
143
+ }
144
+
145
+ async function runTool(req: {
146
+ params: { name: string; arguments?: Record<string, unknown> };
147
+ }): Promise<ToolResult> {
148
+ const name = req.params.name;
149
+ const a = req.params.arguments ?? {};
150
+
151
+ const identity = currentIdentity();
152
+ if (name !== 'list_accounts' && scopeDenied(identity, name, a))
153
+ return errResult('metro: this account is outside your authorized scope');
154
+
155
+ const owned = STATION_TOOLS.get(name);
156
+ if (owned) {
157
+ try {
158
+ return await owned.tool.handle(a, makeCtx(owned.station.name));
159
+ } catch (e) {
160
+ return toErr(name, e);
161
+ }
162
+ }
163
+
164
+ const core = CORE_DISPATCH[name];
165
+ if (core) return core(a);
166
+
167
+ if (name === 'list_accounts') return handleListAccounts(identity);
168
+
169
+ return dispatchMessageTool(name, a);
170
+ }
171
+
172
+ export async function callToolHandler(req: {
173
+ params: { name: string; arguments?: Record<string, unknown> };
174
+ }): Promise<ToolResult> {
175
+ const result = await runTool(req);
176
+ if (result.isError === true)
177
+ logToolFailure(req.params.name, req.params.arguments ?? {}, result);
178
+ return result;
179
+ }
180
+
181
+ const TOOL_LIST_CHANGED = 'notifications/tools/list_changed';
182
+
183
+ export interface SchemaNotice {
184
+ markCurrent: () => void;
185
+ deliver: (send: () => Promise<void>) => void;
186
+ }
187
+
188
+ interface NotifyingExtra {
189
+ sendNotification: (n: { method: string }) => Promise<void>;
190
+ }
191
+
192
+ export function registerToolHandlers(
193
+ server: Server,
194
+ notice?: SchemaNotice,
195
+ ): void {
196
+ server.setRequestHandler(ListToolsRequestSchema, () => {
197
+ notice?.markCurrent();
198
+ return toolList();
199
+ });
200
+ server.setRequestHandler(
201
+ CallToolRequestSchema,
202
+ ((
203
+ req: { params: { name: string; arguments?: Record<string, unknown> } },
204
+ extra: NotifyingExtra,
205
+ ): Promise<ToolResult> => {
206
+ notice?.deliver(() =>
207
+ extra.sendNotification({ method: TOOL_LIST_CHANGED }),
208
+ );
209
+ return callToolHandler(req);
210
+ }) as Parameters<typeof server.setRequestHandler>[1],
211
+ );
212
+ }
@@ -0,0 +1,397 @@
1
+ const lineProp = {
2
+ type: 'string',
3
+ description:
4
+ 'The metro:// line (from the inbound <channel> tag). The station is derived from it.',
5
+ } as const;
6
+ const msgIdProp = {
7
+ type: 'string',
8
+ description: 'The target message_id.',
9
+ } as const;
10
+
11
+ const attachmentItem = {
12
+ type: 'object',
13
+ description:
14
+ 'A file to attach. Provide EXACTLY ONE source; naming two is an error and so is naming ' +
15
+ 'none. Which one to use: `upload` for any real file on your own machine, including a ' +
16
+ 'confidential one (call `create_upload` first, push the bytes over HTTP, then name the ' +
17
+ 'id here); `data` ONLY for a tiny file, a few KB at most, because the base64 has to be ' +
18
+ 'written out verbatim in this tool call and long base64 gets corrupted in the writing; ' +
19
+ '`url` when the file is ALREADY published somewhere the daemon can fetch it; `path` ONLY ' +
20
+ 'for a file that is already sitting on the daemon host, which is almost never your ' +
21
+ 'machine.',
22
+ properties: {
23
+ upload: {
24
+ type: 'string',
25
+ description:
26
+ 'The `upload_id` of a file already pushed to metro over HTTP (`create_upload` mints ' +
27
+ 'the slot and the one-line command; the bytes never pass through this conversation). ' +
28
+ 'THIS IS THE ROUTE FOR ANYTHING REAL: it takes files up to 64 MiB, works for a file on ' +
29
+ 'your own machine, and publishes nothing. The upload belongs to your agent, expires ' +
30
+ '30 minutes after it is created, and no other agent can name it.',
31
+ },
32
+ data: {
33
+ type: 'string',
34
+ description:
35
+ 'Base64-encoded file bytes, inline in this call. ONLY FOR TINY FILES. The hard cap is ' +
36
+ '8 MiB decoded, but the real ceiling is far lower and is not enforced by the daemon: ' +
37
+ 'this base64 has to be emitted verbatim as part of the tool call, and past roughly ' +
38
+ '10 KB (~13 KB of base64) it comes out corrupted, so the file arrives silently ' +
39
+ 'damaged or fails to decode. Use `upload` instead for anything bigger. A ' +
40
+ '`data:<mime>;base64,` prefix is accepted and supplies the mime when `mime` is ' +
41
+ 'omitted. Stations impose their own, lower limits (xmtp refuses non-image files over ' +
42
+ '~190 KiB).',
43
+ },
44
+ url: {
45
+ type: 'string',
46
+ description:
47
+ 'http(s) URL fetched BY THE DAEMON, so it must already be publicly reachable from the ' +
48
+ 'daemon host. Right when the file is already published; never for anything ' +
49
+ 'confidential, because it has to be public for this to work at all. Use `upload` for ' +
50
+ 'a confidential file.',
51
+ },
52
+ path: {
53
+ type: 'string',
54
+ description:
55
+ 'Absolute path resolved ON THE DAEMON HOST, which is a different machine from yours. ' +
56
+ 'A path on your own disk does NOT resolve here and is refused, however local it ' +
57
+ 'looks. Use `upload` for a file on your machine.',
58
+ },
59
+ mime: {
60
+ type: 'string',
61
+ description: 'MIME type (guessed from the name/extension if omitted).',
62
+ },
63
+ name: {
64
+ type: 'string',
65
+ description:
66
+ 'Filename to present (defaults to the basename, or to the name given at upload time).',
67
+ },
68
+ },
69
+ } as const;
70
+
71
+ const CREATE_UPLOAD_TOOL = {
72
+ name: 'create_upload',
73
+ description:
74
+ 'Reserve a slot to push a file to metro over HTTP so you can attach it with `send`. Use ' +
75
+ 'this for any file that actually matters: it is the only attachment route that carries a ' +
76
+ 'file from YOUR machine without publishing it and without the bytes passing through this ' +
77
+ 'conversation. Args: name? (the filename to present), mime?. Returns `upload_id`, a ' +
78
+ 'single-use `upload_url` and a ready-to-run `curl` line. ONE STEP NEEDS A SHELL: run that ' +
79
+ 'command (or any HTTP client) to push the bytes; there is no way to move a local file to ' +
80
+ 'the daemon over MCP alone, because anything in an MCP call has to be written out by the ' +
81
+ 'model first. If you have no shell, hand the single command to a subagent that does and ' +
82
+ 'keep the `upload_id` -- the slot belongs to this metro agent, not to whoever runs the ' +
83
+ 'command. Then: send({line, attachments:[{upload:"<upload_id>"}]}). Up to 64 MiB, expires ' +
84
+ '30 minutes after it is created, and no other agent can reference it.',
85
+ inputSchema: {
86
+ type: 'object',
87
+ properties: {
88
+ name: {
89
+ type: 'string',
90
+ description: 'Filename to present to the recipient (e.g. `report.pdf`).',
91
+ },
92
+ mime: {
93
+ type: 'string',
94
+ description: 'MIME type (guessed from the name/extension if omitted).',
95
+ },
96
+ },
97
+ },
98
+ };
99
+
100
+ export const COMMON_TOOLS = [
101
+ {
102
+ name: 'reply',
103
+ description:
104
+ 'Reply to a specific message in a Metro conversation (text quotes the target). Args: ' +
105
+ "line, message_id, text. The station is derived from the line. Returns the daemon's " +
106
+ 'reason if the station does not support replies.',
107
+ inputSchema: {
108
+ type: 'object',
109
+ properties: {
110
+ line: lineProp,
111
+ message_id: msgIdProp,
112
+ text: { type: 'string', description: 'The reply text.' },
113
+ },
114
+ required: ['line', 'message_id', 'text'],
115
+ },
116
+ },
117
+ {
118
+ name: 'send',
119
+ description:
120
+ 'Send a message (and/or media) to a Metro conversation. Args: line, text?, reply_to?, ' +
121
+ 'attachments?. The station is derived from the line. Each attachment names EXACTLY ONE ' +
122
+ 'source, and the choice matters: `upload` (an `upload_id` from `create_upload`, the ' +
123
+ 'route for any real file on your own machine, confidential or not, up to 64 MiB); ' +
124
+ '`data` (base64 inline -- TINY FILES ONLY, a few KB, because the base64 has to be ' +
125
+ 'written out verbatim in the call and longer than that it corrupts); `url` (the daemon ' +
126
+ 'fetches it, so the file must already be public); `path` (resolved on the DAEMON host, ' +
127
+ 'which is not your machine). At least one of text/attachments is required. ' +
128
+ 'The success line names each attachment the station actually delivered; a station that ' +
129
+ 'cannot carry a file errors instead of reporting success.',
130
+ inputSchema: {
131
+ type: 'object',
132
+ properties: {
133
+ line: lineProp,
134
+ text: {
135
+ type: 'string',
136
+ description: 'The message text (optional if sending only media).',
137
+ },
138
+ reply_to: {
139
+ type: 'string',
140
+ description: 'Optional message_id to quote/reply to.',
141
+ },
142
+ attachments: {
143
+ type: 'array',
144
+ description: 'Optional files to attach.',
145
+ items: attachmentItem,
146
+ },
147
+ },
148
+ required: ['line'],
149
+ },
150
+ },
151
+ {
152
+ name: 'react',
153
+ description:
154
+ 'Add an emoji reaction to a message. Args: line, message_id, emoji. The station is derived from the line.',
155
+ inputSchema: {
156
+ type: 'object',
157
+ properties: {
158
+ line: lineProp,
159
+ message_id: msgIdProp,
160
+ emoji: { type: 'string', description: 'The emoji to react with.' },
161
+ },
162
+ required: ['line', 'message_id', 'emoji'],
163
+ },
164
+ },
165
+ {
166
+ name: 'unreact',
167
+ description:
168
+ 'Remove an emoji reaction from a message. Args: line, message_id, emoji. The station is derived from the line.',
169
+ inputSchema: {
170
+ type: 'object',
171
+ properties: {
172
+ line: lineProp,
173
+ message_id: msgIdProp,
174
+ emoji: {
175
+ type: 'string',
176
+ description: 'The emoji reaction to remove.',
177
+ },
178
+ },
179
+ required: ['line', 'message_id', 'emoji'],
180
+ },
181
+ },
182
+ {
183
+ name: 'edit',
184
+ description:
185
+ 'Edit the text of a message you sent. Args: line, message_id, text. The station is ' +
186
+ "derived from the line. Returns the daemon's reason if the station does not support edits.",
187
+ inputSchema: {
188
+ type: 'object',
189
+ properties: {
190
+ line: lineProp,
191
+ message_id: msgIdProp,
192
+ text: { type: 'string', description: 'The new message text.' },
193
+ },
194
+ required: ['line', 'message_id', 'text'],
195
+ },
196
+ },
197
+ {
198
+ name: 'delete',
199
+ description:
200
+ 'Delete a message you sent. Args: line, message_id. The station is derived from the line. ' +
201
+ "Returns the daemon's reason if the station does not support deletes.",
202
+ inputSchema: {
203
+ type: 'object',
204
+ properties: { line: lineProp, message_id: msgIdProp },
205
+ required: ['line', 'message_id'],
206
+ },
207
+ },
208
+ {
209
+ name: 'read',
210
+ description:
211
+ 'Read recent message history for a conversation. Args: line, limit?, before?, since?. The ' +
212
+ 'station is derived from the line. Returns the raw history JSON (shapes differ per ' +
213
+ "station), or the daemon's reason if the station does not support reads.",
214
+ inputSchema: {
215
+ type: 'object',
216
+ properties: {
217
+ line: lineProp,
218
+ limit: { type: 'number', description: 'Max messages to return.' },
219
+ before: {
220
+ type: 'string',
221
+ description: 'Return messages before this message_id.',
222
+ },
223
+ since: {
224
+ type: 'string',
225
+ description: 'Return messages since this timestamp.',
226
+ },
227
+ },
228
+ required: ['line'],
229
+ },
230
+ },
231
+ {
232
+ name: 'list_members',
233
+ description:
234
+ 'List the members of a Metro group/channel in a station-agnostic shape. Args: line, ' +
235
+ 'limit?. The station is derived from the line. Returns {line, station, memberCount, ' +
236
+ 'members:[{id, name?, display_name?, address?, roles?, is_admin?, is_bot?}], capability:' +
237
+ '{supported, complete, reason?, total?}}. Each station fills the fields it has (xmtp: id=' +
238
+ 'inboxId + address; discord: id + username/nick + roles + is_bot; telegram/telegram-user: ' +
239
+ 'id + username/first_name). Never throws on "not supported": stations that cannot ' +
240
+ 'enumerate (e.g. the Telegram Bot API) return an empty or partial list with a reason in ' +
241
+ '`capability`. Check `capability.complete` before assuming the roster is exhaustive.',
242
+ inputSchema: {
243
+ type: 'object',
244
+ properties: {
245
+ line: lineProp,
246
+ limit: {
247
+ type: 'number',
248
+ description: 'Max members to return (best-effort; capped per station).',
249
+ },
250
+ },
251
+ required: ['line'],
252
+ },
253
+ },
254
+ {
255
+ name: 'create_group',
256
+ description:
257
+ 'Create a new group/channel on a station and add members, in a station-agnostic shape. ' +
258
+ 'Args: station (required, xmtp|telegram-user|discord|telegram), name (required), members? ' +
259
+ '(string[]; xmtp: 0x address or inboxId, telegram-user: @username or user id, discord: ' +
260
+ 'user id), account? (which station account), parent? (discord only: the metro:// line of ' +
261
+ 'the channel to open the thread under). Returns {op, line (the NEW group line), station, ' +
262
+ 'supported, reason?, id?, name?, members:[{id, status: added|invited|removed|failed, ' +
263
+ 'reason?}], inviteLink?}. Never throws on "not supported": stations that lack the op ' +
264
+ '(e.g. the Telegram Bot API) return {supported:false, reason}. Telegram direct-add is ' +
265
+ 'limited to mutual contacts / permissive privacy — members who cannot be added come back ' +
266
+ 'as status "invited" and an `inviteLink` is returned to share with them.',
267
+ inputSchema: {
268
+ type: 'object',
269
+ properties: {
270
+ station: {
271
+ type: 'string',
272
+ description: 'The station to create the group on (xmtp|telegram-user|discord).',
273
+ },
274
+ name: { type: 'string', description: 'The group/channel name.' },
275
+ members: {
276
+ type: 'array',
277
+ description: 'Members to seed the group with (station-specific identifiers).',
278
+ items: { type: 'string' },
279
+ },
280
+ account: {
281
+ type: 'string',
282
+ description: 'Station account to create under (defaults to the station default).',
283
+ },
284
+ parent: {
285
+ type: 'string',
286
+ description:
287
+ 'Discord only: the metro:// line of the channel to create the thread under.',
288
+ },
289
+ },
290
+ required: ['station', 'name'],
291
+ },
292
+ },
293
+ {
294
+ name: 'add_members',
295
+ description:
296
+ 'Add members to an existing Metro group, in a station-agnostic shape. Args: line ' +
297
+ '(required), members? (string[]; station-specific identifiers). xmtp also accepts ' +
298
+ 'addresses? (0x[]) and inboxIds? (string[]) for backward compatibility. The station is ' +
299
+ 'derived from the line. Returns {op, line, station, supported, reason?, members:[{id, ' +
300
+ 'status, reason?}], inviteLink?}. Never throws on "not supported". On Telegram, members ' +
301
+ 'who cannot be direct-added come back status "invited" with an `inviteLink`.',
302
+ inputSchema: {
303
+ type: 'object',
304
+ properties: {
305
+ line: lineProp,
306
+ members: {
307
+ type: 'array',
308
+ description: 'Members to add (station-specific identifiers).',
309
+ items: { type: 'string' },
310
+ },
311
+ addresses: {
312
+ type: 'array',
313
+ description: 'xmtp only: Ethereum 0x addresses to add.',
314
+ items: { type: 'string' },
315
+ },
316
+ inboxIds: {
317
+ type: 'array',
318
+ description: 'xmtp only: XMTP inboxIds to add.',
319
+ items: { type: 'string' },
320
+ },
321
+ },
322
+ required: ['line'],
323
+ },
324
+ },
325
+ {
326
+ name: 'remove_members',
327
+ description:
328
+ 'Remove members from an existing Metro group, in a station-agnostic shape. Args: line ' +
329
+ '(required), members? (string[]). xmtp also accepts addresses?/inboxIds? for backward ' +
330
+ 'compatibility. The station is derived from the line. Returns {op, line, station, ' +
331
+ 'supported, reason?, members:[{id, status, reason?}]}. Never throws on "not supported".',
332
+ inputSchema: {
333
+ type: 'object',
334
+ properties: {
335
+ line: lineProp,
336
+ members: {
337
+ type: 'array',
338
+ description: 'Members to remove (station-specific identifiers).',
339
+ items: { type: 'string' },
340
+ },
341
+ addresses: {
342
+ type: 'array',
343
+ description: 'xmtp only: Ethereum 0x addresses to remove.',
344
+ items: { type: 'string' },
345
+ },
346
+ inboxIds: {
347
+ type: 'array',
348
+ description: 'xmtp only: XMTP inboxIds to remove.',
349
+ items: { type: 'string' },
350
+ },
351
+ },
352
+ required: ['line'],
353
+ },
354
+ },
355
+ {
356
+ name: 'export_invite',
357
+ description:
358
+ 'Get a join/invite link for a Metro group, for platforms where direct-add is not always ' +
359
+ 'permitted. Args: line (required). The station is derived from the line. Returns {op, ' +
360
+ 'line, station, supported, reason?, inviteLink?}. Only telegram-user supports this today; ' +
361
+ 'other stations return {supported:false, reason}. Never throws on "not supported".',
362
+ inputSchema: {
363
+ type: 'object',
364
+ properties: { line: lineProp },
365
+ required: ['line'],
366
+ },
367
+ },
368
+ CREATE_UPLOAD_TOOL,
369
+ ];
370
+
371
+ export const LIST_ACCOUNTS_TOOL = {
372
+ name: 'list_accounts',
373
+ description:
374
+ 'List the configured messaging accounts across all stations (PUBLIC identity only: ' +
375
+ 'addresses, bot ids/usernames). No args. Never returns tokens, private keys, or the ' +
376
+ 'mnemonic. Also returns `capabilities`: the ' +
377
+ 'message verbs (send/reply/react/unreact/edit/delete/read) each station honors, so a ' +
378
+ 'verb need not be discovered by trial and error.',
379
+ inputSchema: { type: 'object', properties: {} },
380
+ };
381
+
382
+ export const MCP_INSTRUCTIONS =
383
+ 'Messages from Metro chat arrive as <channel source="metro" line="..." from="..." ' +
384
+ 'station="..." message_id="...">. To respond, use the messaging tools, always passing the ' +
385
+ '`line` attribute verbatim (the station is derived from it): `send` (text and/or media via ' +
386
+ '`attachments`, optional `reply_to`), `reply` (quote a `message_id` with `text`), `react`/' +
387
+ '`unreact` (emoji on a `message_id`), `edit`/`delete` (a `message_id`), and `read` (recent ' +
388
+ "history). Station support varies - the tool returns the daemon's reason if a verb is " +
389
+ 'unsupported on that line. An inbound attachment is surfaced as a note carrying the ' +
390
+ "sender's own text alongside a `Public URL` - fetch that url to read the file; the note's " +
391
+ '`local_path` resolves on the DAEMON host, not on yours, so the Read tool works on it only ' +
392
+ 'for an agent running there. To send a file OUT, call `create_upload`, ' +
393
+ 'run the one curl line it hands back (that step needs a shell; the bytes go from your disk ' +
394
+ 'straight to metro and never through this conversation), then `send` with ' +
395
+ 'attachments:[{upload:"<upload_id>"}]. Inline `data` is for tiny files only, `url` only for ' +
396
+ 'an already-public file, and `path` is read on the DAEMON host, not yours. Tool-approval ' +
397
+ 'prompts are relayed to the same chat - answer "yes <id>"/"no <id>".';
@@ -0,0 +1,66 @@
1
+ import { publicBaseOrDefault } from '../daemon/attach-serve.js';
2
+ import { safeFileName } from '../stations/attach-inline.js';
3
+ import { guessMime } from '../stations/attachments.js';
4
+ import {
5
+ createUploadSlot,
6
+ issueUploadTicket,
7
+ MAX_UPLOAD_BYTES,
8
+ removeUpload,
9
+ UPLOAD_TTL_MS,
10
+ } from '../daemon/upload-store.js';
11
+ import type { ToolResult } from '../stations/types.js';
12
+ import { errResult, okJson } from './ctx.js';
13
+ import { allowedAgents, currentIdentity } from './request-identity.js';
14
+ import { str } from './str.js';
15
+
16
+ const ttlMinutes = (): number => Math.round(UPLOAD_TTL_MS / 60_000);
17
+
18
+ const NO_AGENT =
19
+ 'create_upload needs an agent credential; this session is not scoped to one agent';
20
+
21
+ const manyAgents = (n: number): string =>
22
+ `create_upload is scoped to one agent, but this session covers ${String(n)}` +
23
+ ' (a multi-agent sign-in); use `POST /api/uploads?agent=<id>` directly instead';
24
+
25
+ type OwnerAgent = { agentId: string } | { error: string };
26
+
27
+ function ownerAgent(): OwnerAgent {
28
+ const allowed = allowedAgents(currentIdentity());
29
+ const [only] = [...allowed];
30
+ if (allowed.size === 1 && only !== undefined) return { agentId: only };
31
+ return { error: allowed.size === 0 ? NO_AGENT : manyAgents(allowed.size) };
32
+ }
33
+
34
+ export function dispatchCreateUpload(
35
+ a: Record<string, unknown>,
36
+ ): Promise<ToolResult> {
37
+ const owner = ownerAgent();
38
+ if ('error' in owner) return Promise.resolve(errResult(owner.error));
39
+ const agentId = owner.agentId;
40
+ const name = safeFileName(str(a.name) || undefined);
41
+ const mime = str(a.mime) || guessMime(name);
42
+ const id = createUploadSlot(agentId, { name, mime });
43
+ const token = issueUploadTicket(id, agentId);
44
+ if (token === undefined) {
45
+ removeUpload(id);
46
+ return Promise.resolve(errResult('metro could not reserve an upload slot'));
47
+ }
48
+ const url = `${publicBaseOrDefault()}/api/uploads/${id}?token=${token}`;
49
+ return Promise.resolve(
50
+ okJson({
51
+ upload_id: id,
52
+ upload_url: url,
53
+ curl: `curl -sS -T '<the file on your own machine>' '${url}'`,
54
+ name,
55
+ mime,
56
+ max_bytes: MAX_UPLOAD_BYTES,
57
+ expires_in_minutes: ttlMinutes(),
58
+ next:
59
+ 'Run that command in a shell, then attach the file with ' +
60
+ `send({line, attachments:[{upload:'${id}'}]}). The url is single-use, is good ` +
61
+ `only for this one file, and expires in ${ttlMinutes()} minutes. If you have no ` +
62
+ 'shell, delegate the one command to a subagent that does and keep the upload_id; ' +
63
+ 'the slot belongs to this metro agent, not to whoever runs the command.',
64
+ }),
65
+ );
66
+ }