mercury-agent 0.4.5

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 (218) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +438 -0
  3. package/container/Dockerfile +127 -0
  4. package/container/Dockerfile.base +109 -0
  5. package/container/Dockerfile.power +17 -0
  6. package/container/agent-package.json +8 -0
  7. package/container/build.sh +54 -0
  8. package/docs/TODOS.md +147 -0
  9. package/docs/auth/dashboard.md +28 -0
  10. package/docs/auth/overview.md +109 -0
  11. package/docs/auth/whatsapp.md +173 -0
  12. package/docs/configuration.md +54 -0
  13. package/docs/container-lifecycle.md +349 -0
  14. package/docs/context-architecture.md +87 -0
  15. package/docs/deployment.md +199 -0
  16. package/docs/extensions.md +375 -0
  17. package/docs/graceful-shutdown.md +62 -0
  18. package/docs/kb-distillation.md +77 -0
  19. package/docs/media/overview.md +140 -0
  20. package/docs/media/whatsapp.md +171 -0
  21. package/docs/memory.md +137 -0
  22. package/docs/permissions.md +217 -0
  23. package/docs/pipeline.md +228 -0
  24. package/docs/prd-chat-memory.md +76 -0
  25. package/docs/prd-config-load.md +82 -0
  26. package/docs/rate-limiting.md +166 -0
  27. package/docs/scheduler.md +288 -0
  28. package/docs/setup-discord.md +100 -0
  29. package/docs/setup-slack.md +119 -0
  30. package/docs/setup-whatsapp.md +94 -0
  31. package/docs/subagents.md +166 -0
  32. package/docs/web-search.md +62 -0
  33. package/examples/extensions/README.md +12 -0
  34. package/examples/extensions/charts/index.ts +13 -0
  35. package/examples/extensions/charts/skill/SKILL.md +98 -0
  36. package/examples/extensions/gws/README.md +52 -0
  37. package/examples/extensions/gws/index.ts +106 -0
  38. package/examples/extensions/gws/skill/SKILL.md +57 -0
  39. package/examples/extensions/gws/skill/references/calendar.md +101 -0
  40. package/examples/extensions/gws/skill/references/docs.md +65 -0
  41. package/examples/extensions/gws/skill/references/drive.md +79 -0
  42. package/examples/extensions/gws/skill/references/gmail.md +85 -0
  43. package/examples/extensions/gws/skill/references/sheets.md +60 -0
  44. package/examples/extensions/napkin/index.ts +821 -0
  45. package/examples/extensions/napkin/prompts/consolidation-monthly.md +73 -0
  46. package/examples/extensions/napkin/prompts/consolidation-weekly.md +67 -0
  47. package/examples/extensions/napkin/prompts/kb-distillation.md +176 -0
  48. package/examples/extensions/napkin/skill/SKILL.md +728 -0
  49. package/examples/extensions/pdf/index.ts +23 -0
  50. package/examples/extensions/pdf/skill/LICENSE.txt +30 -0
  51. package/examples/extensions/pdf/skill/SKILL.md +314 -0
  52. package/examples/extensions/pdf/skill/forms.md +294 -0
  53. package/examples/extensions/pdf/skill/reference.md +612 -0
  54. package/examples/extensions/pdf/skill/scripts/check_bounding_boxes.py +65 -0
  55. package/examples/extensions/pdf/skill/scripts/check_fillable_fields.py +11 -0
  56. package/examples/extensions/pdf/skill/scripts/convert_pdf_to_images.py +33 -0
  57. package/examples/extensions/pdf/skill/scripts/create_validation_image.py +37 -0
  58. package/examples/extensions/pdf/skill/scripts/extract_form_field_info.py +122 -0
  59. package/examples/extensions/pdf/skill/scripts/extract_form_structure.py +115 -0
  60. package/examples/extensions/pdf/skill/scripts/fill_fillable_fields.py +98 -0
  61. package/examples/extensions/pdf/skill/scripts/fill_pdf_form_with_annotations.py +107 -0
  62. package/examples/extensions/permission-guard/index.ts +65 -0
  63. package/examples/extensions/pinchtab/index.ts +199 -0
  64. package/examples/extensions/pinchtab/lib/session-injector.ts +144 -0
  65. package/examples/extensions/pinchtab/skill/SKILL.md +224 -0
  66. package/examples/extensions/pinchtab/skill/TRUST.md +69 -0
  67. package/examples/extensions/pinchtab/skill/references/api.md +297 -0
  68. package/examples/extensions/pinchtab/skill/references/env.md +45 -0
  69. package/examples/extensions/pinchtab/skill/references/profiles.md +107 -0
  70. package/examples/extensions/tradestation/host/refresh.ts +102 -0
  71. package/examples/extensions/tradestation/index.ts +153 -0
  72. package/examples/extensions/tradestation/skill/SKILL.md +67 -0
  73. package/examples/extensions/tradestation/skill/scripts/ts-cli.ts +111 -0
  74. package/examples/extensions/voice-synth/index.ts +94 -0
  75. package/examples/extensions/voice-synth/skill/SKILL.md +38 -0
  76. package/examples/extensions/voice-transcribe/index.ts +381 -0
  77. package/examples/extensions/voice-transcribe/requirements.txt +8 -0
  78. package/examples/extensions/voice-transcribe/scripts/transcribe.py +179 -0
  79. package/examples/extensions/voice-transcribe/skill/SKILL.md +53 -0
  80. package/examples/extensions/web-search/index.ts +22 -0
  81. package/examples/extensions/web-search/skill/SKILL.md +114 -0
  82. package/examples/extensions/web-search/skill/references/apartments.md +178 -0
  83. package/examples/extensions/web-search/skill/references/car-purchase.md +132 -0
  84. package/examples/extensions/web-search/skill/references/car-rental.md +113 -0
  85. package/examples/extensions/web-search/skill/references/flights.md +133 -0
  86. package/examples/extensions/web-search/skill/references/hotels.md +148 -0
  87. package/examples/extensions/yahoo-mail/cli/bun.lock +66 -0
  88. package/examples/extensions/yahoo-mail/cli/package.json +13 -0
  89. package/examples/extensions/yahoo-mail/cli/ymail.mjs +353 -0
  90. package/examples/extensions/yahoo-mail/index.ts +57 -0
  91. package/examples/extensions/yahoo-mail/skill/SKILL.md +78 -0
  92. package/package.json +106 -0
  93. package/resources/agents/explore.md +50 -0
  94. package/resources/agents/worker.md +24 -0
  95. package/resources/builtin-extensions.txt +3 -0
  96. package/resources/connection-env-vars.json +25 -0
  97. package/resources/extensions/.gitkeep +0 -0
  98. package/resources/pi-extensions/subagent/agents.ts +126 -0
  99. package/resources/pi-extensions/subagent/index.ts +964 -0
  100. package/resources/profiles/coding/AGENTS.md +43 -0
  101. package/resources/profiles/coding/mercury-profile.yaml +15 -0
  102. package/resources/profiles/general/AGENTS.md +31 -0
  103. package/resources/profiles/general/mercury-profile.yaml +15 -0
  104. package/resources/profiles/research/AGENTS.md +40 -0
  105. package/resources/profiles/research/mercury-profile.yaml +15 -0
  106. package/resources/skills/config/SKILL.md +25 -0
  107. package/resources/skills/context/SKILL.md +33 -0
  108. package/resources/skills/conversation-recap/SKILL.md +19 -0
  109. package/resources/skills/media/SKILL.md +27 -0
  110. package/resources/skills/mutes/SKILL.md +31 -0
  111. package/resources/skills/permissions/SKILL.md +19 -0
  112. package/resources/skills/preferences/SKILL.md +31 -0
  113. package/resources/skills/recall/SKILL.md +24 -0
  114. package/resources/skills/roles/SKILL.md +18 -0
  115. package/resources/skills/spaces/SKILL.md +18 -0
  116. package/resources/skills/tasks/SKILL.md +45 -0
  117. package/resources/templates/AGENTS.md +157 -0
  118. package/resources/templates/env.template +34 -0
  119. package/resources/templates/mercury.example.yaml +75 -0
  120. package/src/adapters/discord-native.ts +534 -0
  121. package/src/adapters/discord.ts +38 -0
  122. package/src/adapters/setup.ts +89 -0
  123. package/src/adapters/slack.ts +9 -0
  124. package/src/adapters/whatsapp-media.ts +337 -0
  125. package/src/adapters/whatsapp.ts +629 -0
  126. package/src/agent/api-socket.ts +127 -0
  127. package/src/agent/container-entry.ts +967 -0
  128. package/src/agent/container-error.ts +49 -0
  129. package/src/agent/container-runner.ts +1272 -0
  130. package/src/agent/model-capabilities-core.ts +23 -0
  131. package/src/agent/model-capabilities.ts +231 -0
  132. package/src/agent/pi-failure-class.ts +83 -0
  133. package/src/agent/pi-jsonl-parser.ts +306 -0
  134. package/src/agent/preferences-prompt.ts +20 -0
  135. package/src/agent/user-error-messages.ts +78 -0
  136. package/src/bridges/discord.ts +171 -0
  137. package/src/bridges/slack.ts +177 -0
  138. package/src/bridges/teams.ts +160 -0
  139. package/src/bridges/telegram.ts +571 -0
  140. package/src/bridges/whatsapp.ts +290 -0
  141. package/src/chat-shim.ts +259 -0
  142. package/src/cli/mercury.ts +2508 -0
  143. package/src/cli/mrctl-http.ts +27 -0
  144. package/src/cli/mrctl.ts +611 -0
  145. package/src/cli/whatsapp-auth.ts +260 -0
  146. package/src/config-file.ts +397 -0
  147. package/src/config-model-chain.ts +30 -0
  148. package/src/config.ts +316 -0
  149. package/src/core/api-types.ts +58 -0
  150. package/src/core/api.ts +105 -0
  151. package/src/core/commands.ts +76 -0
  152. package/src/core/conversation.ts +47 -0
  153. package/src/core/handler.ts +206 -0
  154. package/src/core/media.ts +200 -0
  155. package/src/core/mute-duration.ts +22 -0
  156. package/src/core/outbox.ts +76 -0
  157. package/src/core/permissions.ts +192 -0
  158. package/src/core/profiles.ts +245 -0
  159. package/src/core/rate-limiter.ts +127 -0
  160. package/src/core/router.ts +191 -0
  161. package/src/core/routes/chat.ts +172 -0
  162. package/src/core/routes/config-builtin.ts +107 -0
  163. package/src/core/routes/config.ts +81 -0
  164. package/src/core/routes/connections.ts +190 -0
  165. package/src/core/routes/console.ts +668 -0
  166. package/src/core/routes/control.ts +46 -0
  167. package/src/core/routes/conversations.ts +66 -0
  168. package/src/core/routes/dashboard.ts +2491 -0
  169. package/src/core/routes/extensions.ts +37 -0
  170. package/src/core/routes/index.ts +14 -0
  171. package/src/core/routes/media.ts +72 -0
  172. package/src/core/routes/messages.ts +37 -0
  173. package/src/core/routes/mutes.ts +89 -0
  174. package/src/core/routes/prefs.ts +95 -0
  175. package/src/core/routes/roles.ts +125 -0
  176. package/src/core/routes/spaces.ts +60 -0
  177. package/src/core/routes/storage.ts +126 -0
  178. package/src/core/routes/tasks.ts +189 -0
  179. package/src/core/routes/tradestation.ts +268 -0
  180. package/src/core/routes/tts.ts +51 -0
  181. package/src/core/runtime.ts +1140 -0
  182. package/src/core/space-queue.ts +103 -0
  183. package/src/core/storage-cleanup.ts +140 -0
  184. package/src/core/storage-guard.ts +24 -0
  185. package/src/core/task-scheduler.ts +132 -0
  186. package/src/core/telegram-format.ts +178 -0
  187. package/src/core/trigger.ts +142 -0
  188. package/src/dashboard/index.html +729 -0
  189. package/src/dashboard/tokens.css +53 -0
  190. package/src/extensions/api.ts +252 -0
  191. package/src/extensions/catalog.ts +117 -0
  192. package/src/extensions/config-registry.ts +83 -0
  193. package/src/extensions/context.ts +36 -0
  194. package/src/extensions/hooks.ts +156 -0
  195. package/src/extensions/image-builder.ts +617 -0
  196. package/src/extensions/installer.ts +306 -0
  197. package/src/extensions/jobs.ts +122 -0
  198. package/src/extensions/loader.ts +271 -0
  199. package/src/extensions/permission-guard.ts +52 -0
  200. package/src/extensions/reserved.ts +28 -0
  201. package/src/extensions/skills.ts +123 -0
  202. package/src/extensions/types.ts +462 -0
  203. package/src/logger.ts +174 -0
  204. package/src/main.ts +586 -0
  205. package/src/server.ts +391 -0
  206. package/src/storage/db.ts +1624 -0
  207. package/src/storage/memory.ts +45 -0
  208. package/src/storage/pi-auth.ts +95 -0
  209. package/src/text/markdown.ts +117 -0
  210. package/src/text/rtl.ts +38 -0
  211. package/src/tradestation/host-api.ts +77 -0
  212. package/src/tradestation/pending-orders.ts +69 -0
  213. package/src/tts/azure.ts +52 -0
  214. package/src/tts/google.ts +128 -0
  215. package/src/tts/index.ts +8 -0
  216. package/src/tts/language.ts +20 -0
  217. package/src/tts/synthesize.ts +133 -0
  218. package/src/types.ts +295 -0
package/src/server.ts ADDED
@@ -0,0 +1,391 @@
1
+ import { timingSafeEqual } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import type { Adapter } from "chat";
6
+ import { Hono } from "hono";
7
+ import type { WhatsAppBaileysAdapter } from "./adapters/whatsapp.js";
8
+ import type { AppConfig } from "./config.js";
9
+ import { resolveProjectPath } from "./config.js";
10
+ import { createApiApp } from "./core/api.js";
11
+ import { createChatRoute } from "./core/routes/chat.js";
12
+ import { createConsoleApp } from "./core/routes/console.js";
13
+ import { createDashboardRoutes } from "./core/routes/dashboard.js";
14
+ import type { MercuryCoreRuntime } from "./core/runtime.js";
15
+ import type { ConfigRegistry } from "./extensions/config-registry.js";
16
+ import { createMercuryExtensionContext } from "./extensions/context.js";
17
+ import { ensureDerivedImage } from "./extensions/image-builder.js";
18
+ import { ExtensionRegistry } from "./extensions/loader.js";
19
+ import { logger } from "./logger.js";
20
+
21
+ const __dirname = dirname(fileURLToPath(import.meta.url));
22
+
23
+ const MAX_BODY_SIZE = 50 * 1024 * 1024; // 50 MB
24
+
25
+ type WaitUntil = (task: Promise<unknown>) => void;
26
+
27
+ type WebhookHandler = (
28
+ request: Request,
29
+ options?: { waitUntil?: WaitUntil },
30
+ ) => Promise<Response>;
31
+
32
+ export interface ServerContext {
33
+ core: MercuryCoreRuntime;
34
+ config: AppConfig;
35
+ adapters: Record<string, Adapter>;
36
+ webhooks: Record<string, WebhookHandler>;
37
+ startTime: number;
38
+ registry: ExtensionRegistry;
39
+ configRegistry: ConfigRegistry;
40
+ /** Current Mercury project directory (usually `process.cwd()`). */
41
+ projectRoot: string;
42
+ /** Root of the mercury-agent package (for bundled `examples/extensions`). */
43
+ packageRoot: string;
44
+ }
45
+
46
+ /**
47
+ * Authorize an infra request against the `MERCURY_API_SECRET` Bearer token —
48
+ * the same secret enforced by `/api/*` and `/api/console/*`. Returns 503 when no
49
+ * secret is configured (a side-effecting endpoint must never silently run
50
+ * unauthenticated), 401 on a missing or mismatched token, `ok` when valid.
51
+ * Length is checked before `timingSafeEqual` (which throws on unequal buffer
52
+ * lengths); the comparison itself is constant-time. Exported for testing.
53
+ */
54
+ export function authorizeApiSecret(
55
+ authHeader: string | undefined,
56
+ secret: string | undefined,
57
+ ): { ok: true } | { ok: false; status: 401 | 503 } {
58
+ if (!secret) return { ok: false, status: 503 };
59
+ const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : "";
60
+ const tokenBuf = Buffer.from(token);
61
+ const secretBuf = Buffer.from(secret);
62
+ if (
63
+ tokenBuf.length !== secretBuf.length ||
64
+ !timingSafeEqual(tokenBuf, secretBuf)
65
+ ) {
66
+ return { ok: false, status: 401 };
67
+ }
68
+ return { ok: true };
69
+ }
70
+
71
+ export function createApp(ctx: ServerContext): Hono {
72
+ const {
73
+ core,
74
+ config,
75
+ adapters,
76
+ webhooks,
77
+ startTime,
78
+ projectRoot,
79
+ packageRoot,
80
+ } = ctx;
81
+
82
+ const waitUntil: WaitUntil = (task) => {
83
+ void task.catch((error) => {
84
+ logger.error(
85
+ "Background task failed",
86
+ error instanceof Error ? error : undefined,
87
+ );
88
+ });
89
+ };
90
+
91
+ const app = new Hono();
92
+
93
+ // ─── Body Size Limit ──────────────────────────────────────────────────
94
+ app.use("*", async (c, next) => {
95
+ const contentLength = c.req.header("content-length");
96
+ if (contentLength && Number.parseInt(contentLength, 10) > MAX_BODY_SIZE) {
97
+ return c.json({ error: "Request body too large" }, 413);
98
+ }
99
+ await next();
100
+ });
101
+
102
+ // ─── Dashboard ──────────────────────────────────────────────────────────
103
+
104
+ // Cache dashboard HTML at startup
105
+ let dashboardHtml: string | null = null;
106
+ try {
107
+ const html = readFileSync(join(__dirname, "dashboard/index.html"), "utf8");
108
+ let version = "unknown";
109
+ try {
110
+ const pkg = JSON.parse(
111
+ readFileSync(join(packageRoot, "package.json"), "utf8"),
112
+ );
113
+ version = `v${pkg.version}`;
114
+ } catch {
115
+ // ignore — version stays "unknown"
116
+ }
117
+ dashboardHtml = html.replace("{{VERSION}}", version);
118
+ } catch {
119
+ // Dashboard not found — will return 404
120
+ }
121
+
122
+ // Cache tokens.css at startup
123
+ let tokensCss: string | null = null;
124
+ try {
125
+ tokensCss = readFileSync(join(__dirname, "dashboard/tokens.css"), "utf8");
126
+ } catch {
127
+ // tokens.css not found — will return 404
128
+ }
129
+
130
+ app.get("/", (c) => {
131
+ if (!dashboardHtml) return c.text("Dashboard not found", 404);
132
+ return c.html(dashboardHtml);
133
+ });
134
+
135
+ app.get("/dashboard", (c) => {
136
+ if (!dashboardHtml) return c.text("Dashboard not found", 404);
137
+ return c.html(dashboardHtml);
138
+ });
139
+
140
+ app.get("/dashboard/tokens.css", (c) => {
141
+ if (!tokensCss) return c.text("tokens.css not found", 404);
142
+ c.header("Content-Type", "text/css; charset=utf-8");
143
+ c.header("Cache-Control", "public, max-age=300");
144
+ return c.body(tokensCss);
145
+ });
146
+
147
+ // Dashboard partials (htmx)
148
+ const adapterStatus: Record<string, boolean> = {};
149
+ for (const name of Object.keys(adapters)) {
150
+ adapterStatus[name] = true;
151
+ }
152
+
153
+ const dashboardRoutes = createDashboardRoutes({
154
+ core,
155
+ adapters: adapterStatus,
156
+ startTime,
157
+ registry: ctx.registry,
158
+ configRegistry: ctx.configRegistry,
159
+ extensionCtx: createMercuryExtensionContext({
160
+ db: core.db,
161
+ config,
162
+ log: logger,
163
+ }),
164
+ projectRoot,
165
+ packageRoot,
166
+ });
167
+
168
+ // Login route — validates token, sets session cookie, redirects to dashboard
169
+ app.get("/dashboard/login", (c) => {
170
+ const secret = config.apiSecret;
171
+ if (!secret) {
172
+ return c.redirect("/dashboard");
173
+ }
174
+ const token = c.req.query("token");
175
+ if (
176
+ !token ||
177
+ token.length !== secret.length ||
178
+ !timingSafeEqual(Buffer.from(token), Buffer.from(secret))
179
+ ) {
180
+ return c.text("Invalid or missing token", 401);
181
+ }
182
+ c.header(
183
+ "Set-Cookie",
184
+ `mercury_token=${token}; Path=/; HttpOnly; SameSite=Strict`,
185
+ );
186
+ return c.redirect("/dashboard");
187
+ });
188
+
189
+ app.use("/dashboard/*", async (c, next) => {
190
+ // Login route handled above — skip auth
191
+ if (c.req.path === "/dashboard/login") return next();
192
+
193
+ const secret = config.apiSecret;
194
+ if (secret) {
195
+ const authHeader = c.req.header("authorization");
196
+ const token = authHeader?.startsWith("Bearer ")
197
+ ? authHeader.slice(7)
198
+ : undefined;
199
+ const cookie = c.req.header("cookie");
200
+ const cookieToken = cookie
201
+ ?.split(";")
202
+ .map((s) => s.trim())
203
+ .find((s) => s.startsWith("mercury_token="))
204
+ ?.split("=")[1];
205
+
206
+ const provided = token || cookieToken;
207
+ if (
208
+ !provided ||
209
+ provided.length !== secret.length ||
210
+ !timingSafeEqual(Buffer.from(provided), Buffer.from(secret))
211
+ ) {
212
+ return c.json({ error: "Unauthorized" }, 401);
213
+ }
214
+ }
215
+ await next();
216
+ });
217
+ app.route("/dashboard", dashboardRoutes);
218
+
219
+ // ─── Health & Auth ──────────────────────────────────────────────────────
220
+
221
+ app.get("/health", (c) => {
222
+ const uptimeSeconds = Math.floor((Date.now() - startTime) / 1000);
223
+ const adapterStatus: Record<string, boolean> = {};
224
+ for (const name of Object.keys(adapters)) {
225
+ adapterStatus[name] = true;
226
+ }
227
+ const currentImage = core.containerRunner.image;
228
+ const extImage = currentImage.startsWith("mercury-agent-ext-")
229
+ ? currentImage
230
+ : null;
231
+ return c.json({
232
+ status: "ok",
233
+ version:
234
+ process.env.MERCURY_VERSION ??
235
+ process.env.npm_package_version ??
236
+ "unknown",
237
+ uptime: uptimeSeconds,
238
+ queue: {
239
+ active: core.queue.activeCount,
240
+ pending: core.queue.pendingCount,
241
+ },
242
+ containers: {
243
+ active: core.containerRunner.activeCount,
244
+ },
245
+ adapters: adapterStatus,
246
+ extImage,
247
+ });
248
+ });
249
+
250
+ // ─── Pre-build ext image (called by the orchestrator during Phase A of rolling deploy) ──
251
+ // Builds the derived ext image for the given base image tag against this agent's
252
+ // current extension set. After this completes the image is cached — the swap
253
+ // container finds it immediately and skips the build, exiting warming in ~1s.
254
+ //
255
+ // Auth: requires the MERCURY_API_SECRET Bearer token (same as /api/* and
256
+ // /api/console/*). Unlike /health this endpoint has side effects — it loads
257
+ // extensions with caller-supplied env and spawns a Docker build — so it must
258
+ // not rely on mercury-net topology alone (the process binds 0.0.0.0:8787).
259
+ // When no secret is configured it refuses with 503 rather than running open,
260
+ // so it can never silently serve unauthenticated.
261
+ app.post("/pre-build-ext-image", async (c) => {
262
+ const auth = authorizeApiSecret(
263
+ c.req.header("authorization"),
264
+ config.apiSecret,
265
+ );
266
+ if (!auth.ok) {
267
+ return c.json(
268
+ auth.status === 503
269
+ ? { error: "MERCURY_API_SECRET must be set for /pre-build-ext-image" }
270
+ : { error: "Unauthorized" },
271
+ auth.status,
272
+ );
273
+ }
274
+ const raw = await c.req.json().catch(() => null);
275
+ const body =
276
+ raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {};
277
+ const targetAgentImage = body.targetAgentImage;
278
+ if (typeof targetAgentImage !== "string" || !targetAgentImage) {
279
+ return c.json({ error: "targetAgentImage is required" }, 400);
280
+ }
281
+
282
+ // When targetEnv is provided, simulate extension loading with the target
283
+ // container's env so the pre-built image hash matches the -next container.
284
+ let extensions = ctx.registry.list();
285
+ const targetEnv = body.targetEnv;
286
+ if (
287
+ targetEnv &&
288
+ typeof targetEnv === "object" &&
289
+ !Array.isArray(targetEnv)
290
+ ) {
291
+ // Coerce all values to strings to avoid null/number values slipping past
292
+ // the credential gate check (!envToCheck[credVar]).
293
+ const envOverride = Object.fromEntries(
294
+ Object.entries(targetEnv as Record<string, unknown>)
295
+ .filter(([, v]) => typeof v === "string")
296
+ .map(([k, v]) => [k, v as string]),
297
+ );
298
+ const tempRegistry = new ExtensionRegistry();
299
+ const extensionsDir = resolveProjectPath(`${config.dataDir}/extensions`);
300
+ const builtinExtDir = join(packageRoot, "resources/extensions");
301
+ try {
302
+ await tempRegistry.loadAll(
303
+ extensionsDir,
304
+ core.db,
305
+ logger,
306
+ null,
307
+ [builtinExtDir],
308
+ envOverride,
309
+ );
310
+ extensions = tempRegistry.list();
311
+ } catch (err) {
312
+ logger.warn(
313
+ "pre-build: failed to load extensions with targetEnv, falling back to current registry",
314
+ {
315
+ error: err instanceof Error ? err.message : String(err),
316
+ },
317
+ );
318
+ }
319
+ }
320
+
321
+ const derivedImage = await ensureDerivedImage(
322
+ targetAgentImage,
323
+ extensions,
324
+ logger,
325
+ process.env.MERCURY_AGENT_ID,
326
+ );
327
+ return c.json({ status: "ok", derivedImage });
328
+ });
329
+
330
+ app.get("/auth/whatsapp", (c) => {
331
+ const whatsappAdapter = adapters.whatsapp as
332
+ | WhatsAppBaileysAdapter
333
+ | undefined;
334
+ if (!whatsappAdapter) {
335
+ return c.json({ error: "WhatsApp adapter not enabled" }, 400);
336
+ }
337
+ const status = whatsappAdapter.getQrStatus();
338
+ return c.json(status);
339
+ });
340
+
341
+ // ─── Control plane JSON API (Bearer MERCURY_API_SECRET) ─────────────────
342
+ const consoleApp = createConsoleApp({
343
+ projectRoot,
344
+ packageRoot,
345
+ apiSecret: config.apiSecret,
346
+ db: core.db,
347
+ spacesDir: config.spacesDir,
348
+ dbPath: config.dbPath,
349
+ whatsappAuthDir: config.whatsappAuthDir,
350
+ registry: ctx.registry,
351
+ config,
352
+ });
353
+ app.route("/api/console", consoleApp);
354
+
355
+ // ─── Internal API ───────────────────────────────────────────────────────
356
+
357
+ const apiApp = createApiApp({
358
+ db: core.db,
359
+ config,
360
+ containerRunner: core.containerRunner,
361
+ queue: core.queue,
362
+ scheduler: core.scheduler,
363
+ registry: ctx.registry,
364
+ configRegistry: ctx.configRegistry,
365
+ });
366
+
367
+ app.route("/api", apiApp);
368
+ app.route("/chat", createChatRoute(core));
369
+
370
+ // ─── Webhooks ───────────────────────────────────────────────────────────
371
+
372
+ app.all("/webhooks/:platform", async (c) => {
373
+ const platform = c.req.param("platform");
374
+ logger.info("Webhook dispatch", { platform });
375
+
376
+ const handler = webhooks[platform];
377
+ if (!handler) {
378
+ return c.text(`Unknown platform: ${platform}`, 404);
379
+ }
380
+
381
+ return handler(c.req.raw, { waitUntil });
382
+ });
383
+
384
+ // ─── Fallback ───────────────────────────────────────────────────────────
385
+
386
+ app.all("*", (c) => {
387
+ return c.text("Not found", 404);
388
+ });
389
+
390
+ return app;
391
+ }