@rubytech/create-maxy 1.0.797 → 1.0.799

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.
@@ -42,6 +42,7 @@ import {
42
42
  setRemotePassword,
43
43
  sleep,
44
44
  streamLogPathFor,
45
+ stripAttachmentMetaSuffix,
45
46
  validateKey,
46
47
  validatePasswordStrength,
47
48
  verifyPassword,
@@ -49,7 +50,7 @@ import {
49
50
  vncLog,
50
51
  waitForExit,
51
52
  writeChromiumWrapper
52
- } from "./chunk-KM23Y7SY.js";
53
+ } from "./chunk-SBHI2NMF.js";
53
54
  import {
54
55
  ACCOUNTS_DIR,
55
56
  GREETING_DIRECTIVE,
@@ -112,7 +113,7 @@ import {
112
113
  verifyAndGetConversationUpdatedAt,
113
114
  verifyConversationOwnership,
114
115
  writeAdminUserAndPerson
115
- } from "./chunk-BURNRCKP.js";
116
+ } from "./chunk-WHF6YXJ5.js";
116
117
  import {
117
118
  __commonJS,
118
119
  __toESM
@@ -615,8 +616,8 @@ var serveStatic = (options = { root: "" }) => {
615
616
  };
616
617
 
617
618
  // server/index.ts
618
- import { readFileSync as readFileSync16, existsSync as existsSync22, watchFile } from "fs";
619
- import { resolve as resolve23, join as join10, basename as basename7 } from "path";
619
+ import { readFileSync as readFileSync17, existsSync as existsSync24, watchFile } from "fs";
620
+ import { resolve as resolve24, join as join10, basename as basename7 } from "path";
620
621
  import { homedir as homedir2 } from "os";
621
622
 
622
623
  // app/lib/agent-slug-pattern.ts
@@ -2196,9 +2197,10 @@ var WhatsAppAccountSchema = z.object({
2196
2197
  groups: z.record(
2197
2198
  z.string(),
2198
2199
  z.object({
2199
- activation: z.enum(["always", "mention", "off"]).optional()
2200
+ activation: z.enum(["always", "mention", "off"]).optional(),
2201
+ publicAgent: z.string().optional().describe("Per-group public-agent slug override. When set, inbound messages in this group route to this agent instead of the per-account or top-level publicAgent.")
2200
2202
  }).strict().optional()
2201
- ).optional().describe("Per-group settings controlling when the agent responds: 'always' (every message), 'mention' (only when @mentioned), 'off' (silent)."),
2203
+ ).optional().describe("Per-group settings: 'activation' controls when the agent responds ('always', 'mention', 'off'); 'publicAgent' optionally overrides the public-agent slug for this group (precedence: group \u2192 account \u2192 top-level)."),
2202
2204
  ackReaction: z.object({
2203
2205
  emoji: z.string().optional(),
2204
2206
  direct: z.boolean().optional().default(true),
@@ -2452,20 +2454,107 @@ function setPublicAgent(accountDir, slug) {
2452
2454
  return { ok: false, error: msg };
2453
2455
  }
2454
2456
  }
2455
- function getPublicAgent(accountDir) {
2457
+ function resolvePublicAgent(accountDir, opts) {
2456
2458
  try {
2457
2459
  const config = readConfig(accountDir);
2458
2460
  const wa = config.whatsapp;
2459
2461
  if (!wa) return null;
2460
- const slug = wa.publicAgent;
2461
- if (typeof slug === "string" && slug.trim()) {
2462
- return slug.trim();
2462
+ if (opts.groupJid) {
2463
+ const accounts2 = wa.accounts;
2464
+ const account2 = accounts2?.[opts.accountId];
2465
+ const groups = account2?.groups;
2466
+ const group = groups?.[opts.groupJid];
2467
+ const groupSlug = group?.publicAgent;
2468
+ if (typeof groupSlug === "string" && groupSlug.trim()) {
2469
+ return { slug: groupSlug.trim(), source: "group" };
2470
+ }
2471
+ }
2472
+ const accounts = wa.accounts;
2473
+ const account = accounts?.[opts.accountId];
2474
+ const accountSlug = account?.publicAgent;
2475
+ if (typeof accountSlug === "string" && accountSlug.trim()) {
2476
+ return { slug: accountSlug.trim(), source: "account" };
2477
+ }
2478
+ const topSlug = wa.publicAgent;
2479
+ if (typeof topSlug === "string" && topSlug.trim()) {
2480
+ return { slug: topSlug.trim(), source: "top-level" };
2463
2481
  }
2464
2482
  return null;
2465
2483
  } catch {
2466
2484
  return null;
2467
2485
  }
2468
2486
  }
2487
+ function setGroupPublicAgent(accountDir, accountId, groupJid, slug) {
2488
+ const trimmedSlug = slug.trim();
2489
+ const trimmedGroup = groupJid.trim();
2490
+ const trimmedAccount = accountId.trim();
2491
+ if (!trimmedSlug) return { ok: false, error: "Agent slug cannot be empty." };
2492
+ if (!trimmedGroup) return { ok: false, error: "Group JID cannot be empty." };
2493
+ if (!trimmedAccount) return { ok: false, error: "Account ID cannot be empty." };
2494
+ const agentConfigPath = join3(accountDir, "agents", trimmedSlug, "config.json");
2495
+ if (!existsSync5(agentConfigPath)) {
2496
+ return { ok: false, error: `Agent "${trimmedSlug}" not found \u2014 no config.json at ${agentConfigPath}. Check the agent slug and try again.` };
2497
+ }
2498
+ try {
2499
+ const config = readConfig(accountDir);
2500
+ if (!config.whatsapp || typeof config.whatsapp !== "object") config.whatsapp = {};
2501
+ const wa = config.whatsapp;
2502
+ if (!wa.accounts || typeof wa.accounts !== "object") wa.accounts = {};
2503
+ const accounts = wa.accounts;
2504
+ if (!accounts[trimmedAccount] || typeof accounts[trimmedAccount] !== "object") accounts[trimmedAccount] = {};
2505
+ const account = accounts[trimmedAccount];
2506
+ if (!account.groups || typeof account.groups !== "object") account.groups = {};
2507
+ const groups = account.groups;
2508
+ const existing = groups[trimmedGroup] && typeof groups[trimmedGroup] === "object" ? groups[trimmedGroup] : {};
2509
+ groups[trimmedGroup] = { ...existing, publicAgent: trimmedSlug };
2510
+ const parsed = WhatsAppConfigSchema.safeParse(wa);
2511
+ if (!parsed.success) {
2512
+ const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
2513
+ return { ok: false, error: `Validation failed: ${msg}` };
2514
+ }
2515
+ config.whatsapp = parsed.data;
2516
+ writeConfig(accountDir, config);
2517
+ console.error(`${TAG} setGroupPublicAgent account=${trimmedAccount} groupJid=${trimmedGroup} slug=${trimmedSlug}`);
2518
+ reloadManagerConfig(accountDir);
2519
+ return { ok: true, message: `Per-group public agent set to "${trimmedSlug}" for group ${trimmedGroup}.` };
2520
+ } catch (err) {
2521
+ const msg = err instanceof Error ? err.message : String(err);
2522
+ console.error(`${TAG} setGroupPublicAgent failed: ${msg}`);
2523
+ return { ok: false, error: msg };
2524
+ }
2525
+ }
2526
+ function unsetGroupPublicAgent(accountDir, accountId, groupJid) {
2527
+ const trimmedGroup = groupJid.trim();
2528
+ const trimmedAccount = accountId.trim();
2529
+ if (!trimmedGroup) return { ok: false, error: "Group JID cannot be empty." };
2530
+ if (!trimmedAccount) return { ok: false, error: "Account ID cannot be empty." };
2531
+ try {
2532
+ const config = readConfig(accountDir);
2533
+ const wa = config.whatsapp ?? {};
2534
+ const accounts = wa.accounts;
2535
+ const account = accounts?.[trimmedAccount];
2536
+ const groups = account?.groups;
2537
+ const group = groups?.[trimmedGroup];
2538
+ if (!group || typeof group.publicAgent !== "string") {
2539
+ return { ok: true, message: `No per-group publicAgent override for group ${trimmedGroup}; nothing to remove.` };
2540
+ }
2541
+ delete group.publicAgent;
2542
+ const parsed = WhatsAppConfigSchema.safeParse(wa);
2543
+ if (!parsed.success) {
2544
+ const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
2545
+ return { ok: false, error: `Validation failed: ${msg}` };
2546
+ }
2547
+ config.whatsapp = parsed.data;
2548
+ writeConfig(accountDir, config);
2549
+ console.error(`${TAG} unsetGroupPublicAgent account=${trimmedAccount} groupJid=${trimmedGroup}`);
2550
+ reloadManagerConfig(accountDir);
2551
+ return { ok: true, message: `Per-group public agent override removed for group ${trimmedGroup}.` };
2552
+ } catch (err) {
2553
+ const msg = err instanceof Error ? err.message : String(err);
2554
+ console.error(`${TAG} unsetGroupPublicAgent failed: ${msg}`);
2555
+ return { ok: false, error: msg };
2556
+ }
2557
+ }
2469
2558
  function updateConfig(accountDir, fields) {
2470
2559
  const fieldNames = Object.keys(fields);
2471
2560
  if (fieldNames.length === 0) {
@@ -2701,7 +2790,7 @@ var credsSaveQueue = Promise.resolve();
2701
2790
  async function drainCredsSaveQueue(timeoutMs = 5e3) {
2702
2791
  console.error(`${TAG3} draining credential save queue\u2026`);
2703
2792
  const timer2 = new Promise(
2704
- (resolve24) => setTimeout(() => resolve24("timeout"), timeoutMs)
2793
+ (resolve25) => setTimeout(() => resolve25("timeout"), timeoutMs)
2705
2794
  );
2706
2795
  const result = await Promise.race([
2707
2796
  credsSaveQueue.then(() => "drained"),
@@ -2829,11 +2918,11 @@ async function createWaSocket(opts) {
2829
2918
  return sock;
2830
2919
  }
2831
2920
  async function waitForConnection(sock) {
2832
- return new Promise((resolve24, reject) => {
2921
+ return new Promise((resolve25, reject) => {
2833
2922
  const handler = (update) => {
2834
2923
  if (update.connection === "open") {
2835
2924
  sock.ev.off("connection.update", handler);
2836
- resolve24();
2925
+ resolve25();
2837
2926
  }
2838
2927
  if (update.connection === "close") {
2839
2928
  sock.ev.off("connection.update", handler);
@@ -2947,14 +3036,14 @@ ${inspected}`;
2947
3036
  return inspect2(err, INSPECT_OPTS2);
2948
3037
  }
2949
3038
  function withTimeout(label, promise, timeoutMs) {
2950
- return new Promise((resolve24, reject) => {
3039
+ return new Promise((resolve25, reject) => {
2951
3040
  const timer2 = setTimeout(() => {
2952
3041
  reject(new Error(`${label} timed out after ${timeoutMs}ms`));
2953
3042
  }, timeoutMs);
2954
3043
  promise.then(
2955
3044
  (value) => {
2956
3045
  clearTimeout(timer2);
2957
- resolve24(value);
3046
+ resolve25(value);
2958
3047
  },
2959
3048
  (err) => {
2960
3049
  clearTimeout(timer2);
@@ -4168,11 +4257,11 @@ async function connectWithReconnect(conn) {
4168
4257
  console.error(
4169
4258
  `${TAG11} reconnecting account=${conn.accountId} in ${delay}ms (attempt ${decision.nextAttempts}/${maxAttempts})`
4170
4259
  );
4171
- await new Promise((resolve24) => {
4172
- const timer2 = setTimeout(resolve24, delay);
4260
+ await new Promise((resolve25) => {
4261
+ const timer2 = setTimeout(resolve25, delay);
4173
4262
  conn.abortController.signal.addEventListener("abort", () => {
4174
4263
  clearTimeout(timer2);
4175
- resolve24();
4264
+ resolve25();
4176
4265
  }, { once: true });
4177
4266
  });
4178
4267
  }
@@ -4180,16 +4269,16 @@ async function connectWithReconnect(conn) {
4180
4269
  }
4181
4270
  }
4182
4271
  function waitForDisconnectEvent(conn) {
4183
- return new Promise((resolve24) => {
4272
+ return new Promise((resolve25) => {
4184
4273
  if (!conn.sock) {
4185
- resolve24();
4274
+ resolve25();
4186
4275
  return;
4187
4276
  }
4188
4277
  const sock = conn.sock;
4189
4278
  const handler = (update) => {
4190
4279
  if (update.connection === "close") {
4191
4280
  sock.ev.off("connection.update", handler);
4192
- resolve24();
4281
+ resolve25();
4193
4282
  }
4194
4283
  };
4195
4284
  sock.ev.on("connection.update", handler);
@@ -4406,8 +4495,8 @@ async function handleInboundMessage(conn, msg) {
4406
4495
  const conversationKey = isGroup ? remoteJid : senderPhone;
4407
4496
  const debounceKey = `${conn.accountId}:${conversationKey}:${senderPhone}`;
4408
4497
  let resolvePending;
4409
- const sttPending = new Promise((resolve24) => {
4410
- resolvePending = resolve24;
4498
+ const sttPending = new Promise((resolve25) => {
4499
+ resolvePending = resolve25;
4411
4500
  });
4412
4501
  if (conn.debouncer) conn.debouncer.registerPending(debounceKey, sttPending);
4413
4502
  try {
@@ -4520,20 +4609,20 @@ async function probeApiKey() {
4520
4609
  return result.status;
4521
4610
  }
4522
4611
  function checkPort(port2, timeoutMs = 500) {
4523
- return new Promise((resolve24) => {
4612
+ return new Promise((resolve25) => {
4524
4613
  const socket = createConnection(port2, "127.0.0.1");
4525
4614
  socket.setTimeout(timeoutMs);
4526
4615
  socket.once("connect", () => {
4527
4616
  socket.destroy();
4528
- resolve24(true);
4617
+ resolve25(true);
4529
4618
  });
4530
4619
  socket.once("error", () => {
4531
4620
  socket.destroy();
4532
- resolve24(false);
4621
+ resolve25(false);
4533
4622
  });
4534
4623
  socket.once("timeout", () => {
4535
4624
  socket.destroy();
4536
- resolve24(false);
4625
+ resolve25(false);
4537
4626
  });
4538
4627
  });
4539
4628
  }
@@ -6746,8 +6835,8 @@ async function startLogin(opts) {
6746
6835
  resetActiveLogin(accountId);
6747
6836
  let resolveQr = null;
6748
6837
  let rejectQr = null;
6749
- const qrPromise = new Promise((resolve24, reject) => {
6750
- resolveQr = resolve24;
6838
+ const qrPromise = new Promise((resolve25, reject) => {
6839
+ resolveQr = resolve25;
6751
6840
  rejectQr = reject;
6752
6841
  });
6753
6842
  const qrTimer = setTimeout(
@@ -7074,7 +7163,7 @@ app7.post("/send", async (c) => {
7074
7163
  app7.post("/config", async (c) => {
7075
7164
  try {
7076
7165
  const body = await c.req.json().catch(() => ({}));
7077
- const { action, phone, slug, fields, accountId } = body;
7166
+ const { action, phone, slug, groupJid, fields, accountId } = body;
7078
7167
  if (!action || typeof action !== "string") {
7079
7168
  return c.json({ ok: false, error: 'Missing required field "action".' }, 400);
7080
7169
  }
@@ -7113,9 +7202,32 @@ app7.post("/config", async (c) => {
7113
7202
  return c.json(result, result.ok ? 200 : 400);
7114
7203
  }
7115
7204
  case "get-public-agent": {
7116
- const currentSlug = getPublicAgent(account.accountDir);
7117
- console.error(`${TAG16} config action=get-public-agent slug=${currentSlug ?? "none"}`);
7118
- return c.json({ ok: true, slug: currentSlug });
7205
+ const targetAccount = typeof accountId === "string" && accountId.trim() ? accountId.trim() : "default";
7206
+ const targetGroup = typeof groupJid === "string" && groupJid.trim() ? groupJid.trim() : void 0;
7207
+ const resolved = resolvePublicAgent(account.accountDir, { accountId: targetAccount, groupJid: targetGroup });
7208
+ console.error(`${TAG16} config action=get-public-agent accountId=${targetAccount} groupJid=${targetGroup ?? "none"} slug=${resolved?.slug ?? "none"} source=${resolved?.source ?? "none"}`);
7209
+ return c.json({ ok: true, slug: resolved?.slug ?? null, source: resolved?.source ?? null });
7210
+ }
7211
+ case "set-group-public-agent": {
7212
+ if (!slug || typeof slug !== "string") {
7213
+ return c.json({ ok: false, error: 'Missing required field "slug" (the public-agent directory name, e.g. "my-agent").' }, 400);
7214
+ }
7215
+ if (!groupJid || typeof groupJid !== "string") {
7216
+ return c.json({ ok: false, error: 'Missing required field "groupJid" (group JID ending in @g.us).' }, 400);
7217
+ }
7218
+ const targetAccount = typeof accountId === "string" && accountId.trim() ? accountId.trim() : "default";
7219
+ const result = setGroupPublicAgent(account.accountDir, targetAccount, groupJid, slug);
7220
+ console.error(`${TAG16} config action=set-group-public-agent accountId=${targetAccount} groupJid=${groupJid} slug=${slug} ok=${result.ok}`);
7221
+ return c.json(result, result.ok ? 200 : 400);
7222
+ }
7223
+ case "unset-group-public-agent": {
7224
+ if (!groupJid || typeof groupJid !== "string") {
7225
+ return c.json({ ok: false, error: 'Missing required field "groupJid" (group JID ending in @g.us).' }, 400);
7226
+ }
7227
+ const targetAccount = typeof accountId === "string" && accountId.trim() ? accountId.trim() : "default";
7228
+ const result = unsetGroupPublicAgent(account.accountDir, targetAccount, groupJid);
7229
+ console.error(`${TAG16} config action=unset-group-public-agent accountId=${targetAccount} groupJid=${groupJid} ok=${result.ok}`);
7230
+ return c.json(result, result.ok ? 200 : 400);
7119
7231
  }
7120
7232
  case "list-public-agents": {
7121
7233
  const agentsDir = resolve10(account.accountDir, "agents");
@@ -7183,7 +7295,7 @@ app7.post("/config", async (c) => {
7183
7295
  }
7184
7296
  default:
7185
7297
  return c.json(
7186
- { ok: false, error: `Unknown action "${action}". Valid actions: add-admin-phone, remove-admin-phone, list-admin-phones, set-public-agent, get-public-agent, list-public-agents, update-config, get-config, schema, list-groups.` },
7298
+ { ok: false, error: `Unknown action "${action}". Valid actions: add-admin-phone, remove-admin-phone, list-admin-phones, set-public-agent, get-public-agent, set-group-public-agent, unset-group-public-agent, list-public-agents, update-config, get-config, schema, list-groups.` },
7187
7299
  400
7188
7300
  );
7189
7301
  }
@@ -8213,7 +8325,7 @@ var app11 = new Hono();
8213
8325
  app11.post("/cancel", requireAdminSession, async (c) => {
8214
8326
  const session_key = c.var.sessionKey;
8215
8327
  try {
8216
- const { interruptClient: interruptClient2 } = await import("./client-pool-PV45NUTN.js");
8328
+ const { interruptClient: interruptClient2 } = await import("./client-pool-4YDRTKAT.js");
8217
8329
  await interruptClient2(session_key);
8218
8330
  return c.json({ ok: true });
8219
8331
  } catch (err) {
@@ -8967,7 +9079,35 @@ var agents_default = app16;
8967
9079
  // server/routes/admin/sessions.ts
8968
9080
  import crypto2 from "crypto";
8969
9081
  import { resolve as resolvePath } from "path";
8970
- import { appendFileSync as appendFileSync5 } from "fs";
9082
+ import { appendFileSync as appendFileSync5, existsSync as existsSync18 } from "fs";
9083
+ function validateAndShapeAttachments(raws, conversationAccountId, conversationId, messageId, streamLogPath) {
9084
+ const chips = [];
9085
+ let valid = 0;
9086
+ let invalid = 0;
9087
+ for (const a of raws) {
9088
+ let reason = null;
9089
+ if (!a.attachmentId || !a.filename || !a.mimeType || !a.storagePath) reason = "schema-fail";
9090
+ else if (a.accountId !== conversationAccountId) reason = "account-mismatch";
9091
+ else if (!existsSync18(a.storagePath)) reason = "missing-file";
9092
+ if (reason) {
9093
+ invalid++;
9094
+ try {
9095
+ appendFileSync5(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [attachment-rehydrate-invalid] conversationId=${conversationId.slice(0, 8)} messageId=${messageId.slice(0, 8)} attachmentId=${(a.attachmentId || "").slice(0, 8)} reason=${reason}
9096
+ `);
9097
+ } catch {
9098
+ }
9099
+ continue;
9100
+ }
9101
+ valid++;
9102
+ chips.push({
9103
+ attachmentId: a.attachmentId,
9104
+ filename: a.filename,
9105
+ mimeType: a.mimeType,
9106
+ sizeBytes: a.sizeBytes
9107
+ });
9108
+ }
9109
+ return { chips, valid, invalid };
9110
+ }
8971
9111
  function reconstructAssistantEvents(content, components, conversationId, messageId, streamLogPath) {
8972
9112
  if (components.length === 0) {
8973
9113
  return {
@@ -9120,7 +9260,17 @@ app17.get("/:id/messages", requireAdminSession, async (c) => {
9120
9260
  if (!owned) return c.json({ error: "Conversation not found" }, 404);
9121
9261
  try {
9122
9262
  const messages = await getRecentMessages(conversationId, 50);
9123
- return c.json({ messages });
9263
+ const sanitised = messages.map((m) => ({
9264
+ ...m,
9265
+ attachments: m.attachments.map((a) => ({
9266
+ attachmentId: a.attachmentId,
9267
+ filename: a.filename,
9268
+ mimeType: a.mimeType,
9269
+ sizeBytes: a.sizeBytes,
9270
+ ordinal: a.ordinal
9271
+ }))
9272
+ }));
9273
+ return c.json({ messages: sanitised });
9124
9274
  } catch (err) {
9125
9275
  console.error(`[sessions-messages] Failed: ${err instanceof Error ? err.message : String(err)}`);
9126
9276
  return c.json({ error: "Failed to fetch messages" }, 500);
@@ -9153,10 +9303,23 @@ app17.post("/:id/resume", requireAdminSession, async (c) => {
9153
9303
  let totalValid = 0;
9154
9304
  let totalInvalid = 0;
9155
9305
  let totalComponents = 0;
9306
+ let totalAttachments = 0;
9307
+ let totalAttachmentInvalid = 0;
9156
9308
  const rehydrated = messages.map((m) => {
9157
9309
  const components = m.components ?? [];
9310
+ const rawAttachments = m.attachments ?? [];
9158
9311
  if (m.role !== "assistant") {
9159
- return { messageId: m.messageId, role: m.role, content: m.content, createdAt: m.createdAt };
9312
+ const { chips, valid: valid2, invalid: invalid2 } = validateAndShapeAttachments(rawAttachments, accountId, conversationId, m.messageId, streamLogPath);
9313
+ totalAttachments += valid2;
9314
+ totalAttachmentInvalid += invalid2;
9315
+ const cleanedContent = chips.length > 0 ? stripAttachmentMetaSuffix(m.content) : m.content;
9316
+ return {
9317
+ messageId: m.messageId,
9318
+ role: m.role,
9319
+ content: cleanedContent,
9320
+ createdAt: m.createdAt,
9321
+ ...chips.length > 0 ? { attachments: chips } : {}
9322
+ };
9160
9323
  }
9161
9324
  const { events, valid, invalid, submittedEventIndices } = reconstructAssistantEvents(m.content, components, conversationId, m.messageId, streamLogPath);
9162
9325
  totalValid += valid;
@@ -9173,17 +9336,22 @@ app17.post("/:id/resume", requireAdminSession, async (c) => {
9173
9336
  };
9174
9337
  });
9175
9338
  const textRuns = rehydrated.reduce((n, m) => n + (m.events?.filter((e) => e.type === "text").length ?? 0), 0);
9339
+ const userMessageCount = rehydrated.filter((m) => m.role !== "assistant").length;
9176
9340
  try {
9177
- appendFileSync5(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [admin-resume] sessionKey=${sessionKey.slice(0, 8)} conversationId=${conversationId.slice(0, 8)} ${tag} loadedMessages=${messages.length} componentCount=${totalComponents}
9341
+ appendFileSync5(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [admin-resume] sessionKey=${sessionKey.slice(0, 8)} conversationId=${conversationId.slice(0, 8)} ${tag} loadedMessages=${messages.length} componentCount=${totalComponents} userAttachmentCount=${totalAttachments}
9178
9342
  `);
9179
9343
  if (totalComponents > 0) {
9180
9344
  appendFileSync5(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [component-rehydrate] conversationId=${conversationId.slice(0, 8)} count=${totalComponents} valid=${totalValid} invalid=${totalInvalid} textRuns=${textRuns}
9345
+ `);
9346
+ }
9347
+ if (totalAttachments > 0 || totalAttachmentInvalid > 0) {
9348
+ appendFileSync5(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [attachment-rehydrate] conversationId=${conversationId.slice(0, 8)} userMessages=${userMessageCount} attachments=${totalAttachments} invalid=${totalAttachmentInvalid}
9181
9349
  `);
9182
9350
  }
9183
9351
  } catch {
9184
9352
  }
9185
9353
  const age = formatAge(updatedAt);
9186
- console.log(`[admin-resume] ${(/* @__PURE__ */ new Date()).toISOString()} conversationId=${conversationId.slice(0, 8)}\u2026 age=${age} loaded=${messages.length} messages ${tag} components=${totalComponents} sessionKey=${sessionKey.slice(0, 8)}\u2026`);
9354
+ console.log(`[admin-resume] ${(/* @__PURE__ */ new Date()).toISOString()} conversationId=${conversationId.slice(0, 8)}\u2026 age=${age} loaded=${messages.length} messages ${tag} components=${totalComponents} attachments=${totalAttachments} sessionKey=${sessionKey.slice(0, 8)}\u2026`);
9187
9355
  return c.json({ conversationId, messages: rehydrated });
9188
9356
  });
9189
9357
  app17.post("/:id/label", requireAdminSession, async (c) => {
@@ -9458,12 +9626,12 @@ function isValidDomain(value) {
9458
9626
  }
9459
9627
 
9460
9628
  // app/lib/alias-domains.ts
9461
- import { existsSync as existsSync18, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
9629
+ import { existsSync as existsSync19, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
9462
9630
  import { dirname as dirname7 } from "path";
9463
9631
  import { resolve as resolve16 } from "path";
9464
9632
  var ALIAS_DOMAINS_PATH = resolve16(MAXY_DIR, "alias-domains.json");
9465
9633
  function readExisting() {
9466
- if (!existsSync18(ALIAS_DOMAINS_PATH)) return /* @__PURE__ */ new Set();
9634
+ if (!existsSync19(ALIAS_DOMAINS_PATH)) return /* @__PURE__ */ new Set();
9467
9635
  try {
9468
9636
  const parsed = JSON.parse(readFileSync14(ALIAS_DOMAINS_PATH, "utf-8"));
9469
9637
  if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
@@ -11800,7 +11968,7 @@ var adherence_default = app30;
11800
11968
  import neo4j3 from "neo4j-driver";
11801
11969
  import { readFile as readFile5, readdir as readdir3, stat as stat5 } from "fs/promises";
11802
11970
  import { resolve as resolve20, relative as relative2, isAbsolute } from "path";
11803
- import { existsSync as existsSync19 } from "fs";
11971
+ import { existsSync as existsSync20 } from "fs";
11804
11972
  var LIMIT = 50;
11805
11973
  var TEXT_MIME_PREFIXES = ["text/", "application/json", "application/markdown"];
11806
11974
  var ADMIN_AGENT_FILES = ["IDENTITY.md", "SOUL.md", "KNOWLEDGE.md"];
@@ -11948,7 +12116,7 @@ async function fetchAgentTemplateRows(accountDir) {
11948
12116
  async function unionSpecialistFilenames(overrideDir, bundledDir) {
11949
12117
  const names = /* @__PURE__ */ new Set();
11950
12118
  for (const dir of [overrideDir, bundledDir]) {
11951
- if (!existsSync19(dir)) continue;
12119
+ if (!existsSync20(dir)) continue;
11952
12120
  try {
11953
12121
  const entries = await readdir3(dir);
11954
12122
  for (const entry of entries) {
@@ -11963,7 +12131,7 @@ async function unionSpecialistFilenames(overrideDir, bundledDir) {
11963
12131
  }
11964
12132
  async function readAgentTemplateRow(inp) {
11965
12133
  let chosenPath = null;
11966
- if (existsSync19(inp.overridePath)) {
12134
+ if (existsSync20(inp.overridePath)) {
11967
12135
  try {
11968
12136
  validateFilePathInAccount(inp.overridePath, inp.overrideRoot);
11969
12137
  chosenPath = inp.overridePath;
@@ -11974,7 +12142,7 @@ async function readAgentTemplateRow(inp) {
11974
12142
  );
11975
12143
  return null;
11976
12144
  }
11977
- } else if (existsSync19(inp.bundledPath)) {
12145
+ } else if (existsSync20(inp.bundledPath)) {
11978
12146
  if (!isWithin(inp.bundledPath, inp.bundledRoot)) {
11979
12147
  console.error(
11980
12148
  `[admin/sidebar-artefacts] agent-template-read-failed agent=${inp.displayName} kind=${inp.logName} error="bundled path outside PLATFORM_ROOT"`
@@ -12016,7 +12184,7 @@ var sidebar_artefacts_default = app31;
12016
12184
  // server/routes/admin/sidebar-artefact-save.ts
12017
12185
  import { mkdir as mkdir4, readdir as readdir4, stat as stat6, writeFile as writeFile5 } from "fs/promises";
12018
12186
  import { resolve as resolve21 } from "path";
12019
- import { existsSync as existsSync20 } from "fs";
12187
+ import { existsSync as existsSync21 } from "fs";
12020
12188
  var ADMIN_AGENT_FILES2 = /* @__PURE__ */ new Set(["IDENTITY.md", "SOUL.md", "KNOWLEDGE.md"]);
12021
12189
  var UUID_RE4 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
12022
12190
  var app32 = new Hono();
@@ -12080,7 +12248,7 @@ async function resolveSavePath(id, accountId, accountDir) {
12080
12248
  }
12081
12249
  if (UUID_RE4.test(id)) {
12082
12250
  const dir = resolve21(ATTACHMENTS_ROOT, accountId, id);
12083
- if (!existsSync20(dir)) {
12251
+ if (!existsSync21(dir)) {
12084
12252
  return { kind: "reject", status: 400, reason: "not-found" };
12085
12253
  }
12086
12254
  try {
@@ -12104,7 +12272,7 @@ var sidebar_artefact_save_default = app32;
12104
12272
 
12105
12273
  // server/routes/admin/sidebar-artefact-content.ts
12106
12274
  import { readFile as readFile6, readdir as readdir5 } from "fs/promises";
12107
- import { existsSync as existsSync21 } from "fs";
12275
+ import { existsSync as existsSync22 } from "fs";
12108
12276
  import { resolve as resolve22 } from "path";
12109
12277
  var UUID_RE5 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
12110
12278
  var app33 = new Hono();
@@ -12118,7 +12286,7 @@ app33.get("/", requireAdminSession, async (c) => {
12118
12286
  return new Response("Not found", { status: 404 });
12119
12287
  }
12120
12288
  const dir = resolve22(ATTACHMENTS_ROOT, accountId, id);
12121
- if (!existsSync21(dir)) {
12289
+ if (!existsSync22(dir)) {
12122
12290
  console.error(`[admin/sidebar-artefact-content] not-found id=${id.slice(0, 8)}`);
12123
12291
  return new Response("Not found", { status: 404 });
12124
12292
  }
@@ -12179,6 +12347,140 @@ app34.route("/sidebar-artefact-save", sidebar_artefact_save_default);
12179
12347
  app34.route("/sidebar-artefact-content", sidebar_artefact_content_default);
12180
12348
  var admin_default = app34;
12181
12349
 
12350
+ // server/routes/sites.ts
12351
+ import { existsSync as existsSync23, readFileSync as readFileSync16, realpathSync as realpathSync5, statSync as statSync8 } from "fs";
12352
+ import { resolve as resolve23 } from "path";
12353
+ var SAFE_SEG_RE = /^[a-z0-9_][a-z0-9_.-]{0,99}$/i;
12354
+ var MIME = {
12355
+ ".html": "text/html; charset=utf-8",
12356
+ ".htm": "text/html; charset=utf-8",
12357
+ ".css": "text/css; charset=utf-8",
12358
+ ".js": "text/javascript; charset=utf-8",
12359
+ ".mjs": "text/javascript; charset=utf-8",
12360
+ ".json": "application/json; charset=utf-8",
12361
+ ".txt": "text/plain; charset=utf-8",
12362
+ ".md": "text/plain; charset=utf-8",
12363
+ ".png": "image/png",
12364
+ ".jpg": "image/jpeg",
12365
+ ".jpeg": "image/jpeg",
12366
+ ".gif": "image/gif",
12367
+ ".webp": "image/webp",
12368
+ ".svg": "image/svg+xml",
12369
+ ".ico": "image/x-icon",
12370
+ ".woff": "font/woff",
12371
+ ".woff2": "font/woff2",
12372
+ ".ttf": "font/ttf",
12373
+ ".otf": "font/otf"
12374
+ };
12375
+ var HTML_EXTS = /* @__PURE__ */ new Set([".html", ".htm"]);
12376
+ var CSP_HTML = "default-src 'self' https: data:; script-src 'none'";
12377
+ function getExt(p) {
12378
+ const idx = p.lastIndexOf(".");
12379
+ if (idx < 0) return "";
12380
+ if (idx < p.lastIndexOf("/")) return "";
12381
+ return p.slice(idx).toLowerCase();
12382
+ }
12383
+ var app35 = new Hono();
12384
+ app35.get("/:rel{.*}", (c) => {
12385
+ const reqPath = c.req.path;
12386
+ const rawRel = c.req.param("rel") ?? "";
12387
+ const trimmed = rawRel.replace(/^\/+/, "").replace(/\/+$/, "");
12388
+ const isDirRequest = rawRel === "" || rawRel.endsWith("/") || reqPath.endsWith("/");
12389
+ const account = resolveAccount();
12390
+ if (!account) {
12391
+ console.error(`[sites] no-account path=${reqPath} status=404`);
12392
+ return c.text("Not found", 404);
12393
+ }
12394
+ const rawSegments = trimmed === "" ? [] : trimmed.split("/");
12395
+ const segments = [];
12396
+ for (const raw of rawSegments) {
12397
+ let seg;
12398
+ try {
12399
+ seg = decodeURIComponent(raw);
12400
+ } catch {
12401
+ console.error(`[sites] path-traversal-rejected path=${reqPath} reason=segment status=403`);
12402
+ return c.text("Forbidden", 403);
12403
+ }
12404
+ if (seg === ".." || seg === "." || !SAFE_SEG_RE.test(seg)) {
12405
+ console.error(`[sites] path-traversal-rejected path=${reqPath} reason=segment status=403`);
12406
+ return c.text("Forbidden", 403);
12407
+ }
12408
+ segments.push(seg);
12409
+ }
12410
+ const rootDir = resolve23(account.accountDir, "sites");
12411
+ let filePath = segments.length === 0 ? rootDir : resolve23(rootDir, ...segments);
12412
+ if (filePath !== rootDir && !filePath.startsWith(rootDir + "/")) {
12413
+ console.error(`[sites] path-traversal-rejected path=${reqPath} reason=escape status=403`);
12414
+ return c.text("Forbidden", 403);
12415
+ }
12416
+ let stat7;
12417
+ try {
12418
+ stat7 = existsSync23(filePath) ? statSync8(filePath) : null;
12419
+ } catch {
12420
+ stat7 = null;
12421
+ }
12422
+ if (stat7?.isDirectory()) {
12423
+ filePath = resolve23(filePath, "index.html");
12424
+ } else if (stat7 === null && isDirRequest) {
12425
+ filePath = resolve23(filePath, "index.html");
12426
+ }
12427
+ if (!filePath.startsWith(rootDir + "/")) {
12428
+ console.error(`[sites] path-traversal-rejected path=${reqPath} reason=escape status=403`);
12429
+ return c.text("Forbidden", 403);
12430
+ }
12431
+ if (!existsSync23(filePath)) {
12432
+ console.error(`[sites] not-found path=${reqPath} status=404`);
12433
+ return c.text("Not found", 404);
12434
+ }
12435
+ let realPath;
12436
+ let realRoot;
12437
+ try {
12438
+ realPath = realpathSync5(filePath);
12439
+ realRoot = realpathSync5(rootDir);
12440
+ } catch {
12441
+ console.error(`[sites] not-found path=${reqPath} status=404`);
12442
+ return c.text("Not found", 404);
12443
+ }
12444
+ if (realPath !== realRoot && !realPath.startsWith(realRoot + "/")) {
12445
+ console.error(`[sites] symlink-escape-rejected path=${reqPath} real=${realPath} status=403`);
12446
+ return c.text("Forbidden", 403);
12447
+ }
12448
+ let body;
12449
+ try {
12450
+ body = readFileSync16(realPath);
12451
+ } catch (err) {
12452
+ const code = err?.code;
12453
+ if (code === "EISDIR") {
12454
+ console.error(`[sites] not-found path=${reqPath} reason=is-directory status=404`);
12455
+ return c.text("Not found", 404);
12456
+ }
12457
+ if (code === "ENOENT") {
12458
+ console.error(`[sites] not-found path=${reqPath} status=404`);
12459
+ return c.text("Not found", 404);
12460
+ }
12461
+ throw err;
12462
+ }
12463
+ const ext = getExt(realPath);
12464
+ const contentType = MIME[ext] ?? "application/octet-stream";
12465
+ const isHtml = HTML_EXTS.has(ext);
12466
+ console.log(`[sites] serve path=${reqPath} ext=${ext || "(none)"} status=200 bytes=${body.length}`);
12467
+ const view = Uint8Array.from(body);
12468
+ if (isHtml) {
12469
+ return c.body(view, 200, {
12470
+ "Content-Type": contentType,
12471
+ "Cache-Control": "no-cache",
12472
+ "X-Content-Type-Options": "nosniff",
12473
+ "Content-Security-Policy": CSP_HTML
12474
+ });
12475
+ }
12476
+ return c.body(view, 200, {
12477
+ "Content-Type": contentType,
12478
+ "Cache-Control": "public, max-age=3600",
12479
+ "X-Content-Type-Options": "nosniff"
12480
+ });
12481
+ });
12482
+ var sites_default = app35;
12483
+
12182
12484
  // app/lib/graph-health.ts
12183
12485
  var HOUR_MS = 60 * 60 * 1e3;
12184
12486
  var timer = null;
@@ -12279,12 +12581,12 @@ function clientFrom(c) {
12279
12581
  var PLATFORM_ROOT7 = process.env.MAXY_PLATFORM_ROOT || "";
12280
12582
  var BRAND_JSON_PATH = PLATFORM_ROOT7 ? join10(PLATFORM_ROOT7, "config", "brand.json") : "";
12281
12583
  var BRAND = { productName: "Maxy", hostname: "maxy", configDir: ".maxy", domain: "getmaxy.com" };
12282
- if (BRAND_JSON_PATH && !existsSync22(BRAND_JSON_PATH)) {
12584
+ if (BRAND_JSON_PATH && !existsSync24(BRAND_JSON_PATH)) {
12283
12585
  console.error(`[brand] WARNING: brand.json not found at ${BRAND_JSON_PATH} \u2014 using Maxy defaults`);
12284
12586
  }
12285
- if (BRAND_JSON_PATH && existsSync22(BRAND_JSON_PATH)) {
12587
+ if (BRAND_JSON_PATH && existsSync24(BRAND_JSON_PATH)) {
12286
12588
  try {
12287
- const parsed = JSON.parse(readFileSync16(BRAND_JSON_PATH, "utf-8"));
12589
+ const parsed = JSON.parse(readFileSync17(BRAND_JSON_PATH, "utf-8"));
12288
12590
  BRAND = { ...BRAND, ...parsed };
12289
12591
  } catch (err) {
12290
12592
  console.error(`[brand] Failed to parse brand.json: ${err.message}`);
@@ -12306,8 +12608,8 @@ var brandLoginOpts = {
12306
12608
  var ALIAS_DOMAINS_PATH2 = join10(homedir2(), BRAND.configDir, "alias-domains.json");
12307
12609
  function loadAliasDomains() {
12308
12610
  try {
12309
- if (!existsSync22(ALIAS_DOMAINS_PATH2)) return null;
12310
- const parsed = JSON.parse(readFileSync16(ALIAS_DOMAINS_PATH2, "utf-8"));
12611
+ if (!existsSync24(ALIAS_DOMAINS_PATH2)) return null;
12612
+ const parsed = JSON.parse(readFileSync17(ALIAS_DOMAINS_PATH2, "utf-8"));
12311
12613
  if (!Array.isArray(parsed)) {
12312
12614
  console.error("[alias-domains] malformed alias-domains.json \u2014 expected array");
12313
12615
  return null;
@@ -12331,9 +12633,9 @@ watchFile(ALIAS_DOMAINS_PATH2, { interval: 2e3 }, () => {
12331
12633
  function isPublicHost(host) {
12332
12634
  return host.startsWith("public.") || aliasDomains.has(host);
12333
12635
  }
12334
- var app35 = new Hono();
12335
- app35.use("*", clientIpMiddleware);
12336
- app35.use("*", async (c, next) => {
12636
+ var app36 = new Hono();
12637
+ app36.use("*", clientIpMiddleware);
12638
+ app36.use("*", async (c, next) => {
12337
12639
  await next();
12338
12640
  c.header("X-Content-Type-Options", "nosniff");
12339
12641
  c.header("Referrer-Policy", "strict-origin-when-cross-origin");
@@ -12353,10 +12655,11 @@ var PUBLIC_ALLOWED_PREFIXES = [
12353
12655
  "/brand/",
12354
12656
  "/agent-assets/",
12355
12657
  "/generated/",
12356
- "/g/"
12658
+ "/g/",
12659
+ "/sites/"
12357
12660
  ];
12358
12661
  var PUBLIC_ALLOWED_EXACT = ["/favicon.ico"];
12359
- app35.use("*", async (c, next) => {
12662
+ app36.use("*", async (c, next) => {
12360
12663
  const host = (c.req.header("host") ?? "").split(":")[0];
12361
12664
  if (!isPublicHost(host)) {
12362
12665
  await next();
@@ -12396,7 +12699,7 @@ function resolveRemoteAuthOpts() {
12396
12699
  return brandLoginOpts;
12397
12700
  }
12398
12701
  var MAX_LOGIN_BODY = 8 * 1024;
12399
- app35.post("/__remote-auth/login", async (c) => {
12702
+ app36.post("/__remote-auth/login", async (c) => {
12400
12703
  const client = clientFrom(c);
12401
12704
  const clientIp = client.ip || "unknown";
12402
12705
  if (!requestIsTlsTerminated(c)) {
@@ -12440,7 +12743,7 @@ app35.post("/__remote-auth/login", async (c) => {
12440
12743
  }
12441
12744
  });
12442
12745
  });
12443
- app35.get("/__remote-auth/logout", (c) => {
12746
+ app36.get("/__remote-auth/logout", (c) => {
12444
12747
  return new Response(null, {
12445
12748
  status: 302,
12446
12749
  headers: {
@@ -12450,7 +12753,7 @@ app35.get("/__remote-auth/logout", (c) => {
12450
12753
  }
12451
12754
  });
12452
12755
  });
12453
- app35.post("/__remote-auth/change-password", async (c) => {
12756
+ app36.post("/__remote-auth/change-password", async (c) => {
12454
12757
  const client = clientFrom(c);
12455
12758
  const clientIp = client.ip || "unknown";
12456
12759
  const rateLimited = checkRateLimit(client);
@@ -12500,13 +12803,13 @@ app35.post("/__remote-auth/change-password", async (c) => {
12500
12803
  return c.html(renderLoginPage({ ...resolveRemoteAuthOpts(), mode: "change", changeError: "Failed to save password", redirect }), 200);
12501
12804
  }
12502
12805
  });
12503
- app35.get("/__remote-auth/setup", (c) => {
12806
+ app36.get("/__remote-auth/setup", (c) => {
12504
12807
  if (isRemoteAuthConfigured()) {
12505
12808
  return c.redirect("/");
12506
12809
  }
12507
12810
  return c.html(renderLoginPage({ ...resolveRemoteAuthOpts(), mode: "setup" }), 200);
12508
12811
  });
12509
- app35.post("/__remote-auth/set-initial-password", async (c) => {
12812
+ app36.post("/__remote-auth/set-initial-password", async (c) => {
12510
12813
  if (isRemoteAuthConfigured()) {
12511
12814
  return c.redirect("/");
12512
12815
  }
@@ -12542,10 +12845,10 @@ app35.post("/__remote-auth/set-initial-password", async (c) => {
12542
12845
  return c.html(renderLoginPage({ ...resolveRemoteAuthOpts(), mode: "setup", setupError: "Failed to save password. Please try again." }), 200);
12543
12846
  }
12544
12847
  });
12545
- app35.get("/api/remote-auth/status", (c) => {
12848
+ app36.get("/api/remote-auth/status", (c) => {
12546
12849
  return c.json({ configured: isRemoteAuthConfigured() });
12547
12850
  });
12548
- app35.post("/api/remote-auth/set-password", async (c) => {
12851
+ app36.post("/api/remote-auth/set-password", async (c) => {
12549
12852
  let body;
12550
12853
  try {
12551
12854
  body = await c.req.json();
@@ -12575,9 +12878,9 @@ app35.post("/api/remote-auth/set-password", async (c) => {
12575
12878
  return c.json({ error: "Failed to save password" }, 500);
12576
12879
  }
12577
12880
  });
12578
- app35.route("/api/_client-error", client_error_default);
12881
+ app36.route("/api/_client-error", client_error_default);
12579
12882
  console.log("[client-error-route] mounted");
12580
- app35.use("*", async (c, next) => {
12883
+ app36.use("*", async (c, next) => {
12581
12884
  const host = (c.req.header("host") ?? "").split(":")[0];
12582
12885
  const path2 = c.req.path;
12583
12886
  if (path2 === "/favicon.ico" || path2.startsWith("/assets/") || path2.startsWith("/brand/")) {
@@ -12610,15 +12913,15 @@ app35.use("*", async (c, next) => {
12610
12913
  console.error(`[remote-auth] login required ip=${clientIp} path=${path2} ${disambig}`);
12611
12914
  return c.html(renderLoginPage({ ...resolveRemoteAuthOpts(), redirect: path2 }), 200);
12612
12915
  });
12613
- app35.route("/api/health", health_default);
12614
- app35.route("/api/session", session_default);
12615
- app35.route("/api/chat", chat_default);
12616
- app35.route("/api/group", group_default);
12617
- app35.route("/api/access", access_default);
12618
- app35.route("/api/telegram", telegram_default);
12619
- app35.route("/api/whatsapp", whatsapp_default);
12620
- app35.route("/api/onboarding", onboarding_default);
12621
- app35.route("/api/admin", admin_default);
12916
+ app36.route("/api/health", health_default);
12917
+ app36.route("/api/session", session_default);
12918
+ app36.route("/api/chat", chat_default);
12919
+ app36.route("/api/group", group_default);
12920
+ app36.route("/api/access", access_default);
12921
+ app36.route("/api/telegram", telegram_default);
12922
+ app36.route("/api/whatsapp", whatsapp_default);
12923
+ app36.route("/api/onboarding", onboarding_default);
12924
+ app36.route("/api/admin", admin_default);
12622
12925
  var SAFE_SLUG_RE = /^[a-z][a-z0-9-]{2,49}$/;
12623
12926
  var SAFE_FILENAME_RE = /^[a-z0-9_][a-z0-9_.-]{0,99}$/i;
12624
12927
  var IMAGE_MIME = {
@@ -12630,7 +12933,7 @@ var IMAGE_MIME = {
12630
12933
  ".svg": "image/svg+xml",
12631
12934
  ".ico": "image/x-icon"
12632
12935
  };
12633
- app35.get("/agent-assets/:slug/:filename", (c) => {
12936
+ app36.get("/agent-assets/:slug/:filename", (c) => {
12634
12937
  const slug = c.req.param("slug");
12635
12938
  const filename = c.req.param("filename");
12636
12939
  if (!SAFE_SLUG_RE.test(slug)) {
@@ -12646,26 +12949,26 @@ app35.get("/agent-assets/:slug/:filename", (c) => {
12646
12949
  console.error(`[agent-assets] no-account slug=${slug} file=${filename}`);
12647
12950
  return c.text("Not found", 404);
12648
12951
  }
12649
- const filePath = resolve23(account.accountDir, "agents", slug, "assets", filename);
12650
- const expectedDir = resolve23(account.accountDir, "agents", slug, "assets");
12952
+ const filePath = resolve24(account.accountDir, "agents", slug, "assets", filename);
12953
+ const expectedDir = resolve24(account.accountDir, "agents", slug, "assets");
12651
12954
  if (!filePath.startsWith(expectedDir + "/")) {
12652
12955
  console.error(`[agent-assets] path-traversal-rejected slug=${slug} file=${filename}`);
12653
12956
  return c.text("Forbidden", 403);
12654
12957
  }
12655
- if (!existsSync22(filePath)) {
12958
+ if (!existsSync24(filePath)) {
12656
12959
  console.error(`[agent-assets] serve slug=${slug} file=${filename} status=404`);
12657
12960
  return c.text("Not found", 404);
12658
12961
  }
12659
12962
  const ext = "." + filename.split(".").pop()?.toLowerCase();
12660
12963
  const contentType = IMAGE_MIME[ext] || "application/octet-stream";
12661
12964
  console.log(`[agent-assets] serve slug=${slug} file=${filename} status=200`);
12662
- const body = readFileSync16(filePath);
12965
+ const body = readFileSync17(filePath);
12663
12966
  return c.body(body, 200, {
12664
12967
  "Content-Type": contentType,
12665
12968
  "Cache-Control": "public, max-age=3600"
12666
12969
  });
12667
12970
  });
12668
- app35.get("/generated/:filename", (c) => {
12971
+ app36.get("/generated/:filename", (c) => {
12669
12972
  const filename = c.req.param("filename");
12670
12973
  if (!SAFE_FILENAME_RE.test(filename) || filename.includes("..")) {
12671
12974
  console.error(`[generated] serve file=${filename} status=403`);
@@ -12676,31 +12979,32 @@ app35.get("/generated/:filename", (c) => {
12676
12979
  console.error(`[generated] serve file=${filename} status=404`);
12677
12980
  return c.text("Not found", 404);
12678
12981
  }
12679
- const filePath = resolve23(account.accountDir, "generated", filename);
12680
- const expectedDir = resolve23(account.accountDir, "generated");
12982
+ const filePath = resolve24(account.accountDir, "generated", filename);
12983
+ const expectedDir = resolve24(account.accountDir, "generated");
12681
12984
  if (!filePath.startsWith(expectedDir + "/")) {
12682
12985
  console.error(`[generated] serve file=${filename} status=403`);
12683
12986
  return c.text("Forbidden", 403);
12684
12987
  }
12685
- if (!existsSync22(filePath)) {
12988
+ if (!existsSync24(filePath)) {
12686
12989
  console.error(`[generated] serve file=${filename} status=404`);
12687
12990
  return c.text("Not found", 404);
12688
12991
  }
12689
12992
  const ext = "." + filename.split(".").pop()?.toLowerCase();
12690
12993
  const contentType = IMAGE_MIME[ext] || "application/octet-stream";
12691
12994
  console.log(`[generated] serve file=${filename} status=200`);
12692
- const body = readFileSync16(filePath);
12995
+ const body = readFileSync17(filePath);
12693
12996
  return c.body(body, 200, {
12694
12997
  "Content-Type": contentType,
12695
12998
  "Cache-Control": "public, max-age=86400"
12696
12999
  });
12697
13000
  });
13001
+ app36.route("/sites", sites_default);
12698
13002
  var htmlCache = /* @__PURE__ */ new Map();
12699
13003
  var brandLogoPath = "/brand/maxy-monochrome.png";
12700
13004
  var brandIconPath = "/brand/maxy-monochrome.png";
12701
- if (BRAND_JSON_PATH && existsSync22(BRAND_JSON_PATH)) {
13005
+ if (BRAND_JSON_PATH && existsSync24(BRAND_JSON_PATH)) {
12702
13006
  try {
12703
- const fullBrand = JSON.parse(readFileSync16(BRAND_JSON_PATH, "utf-8"));
13007
+ const fullBrand = JSON.parse(readFileSync17(BRAND_JSON_PATH, "utf-8"));
12704
13008
  if (fullBrand.assets?.logo) brandLogoPath = `/brand/${fullBrand.assets.logo}`;
12705
13009
  brandIconPath = fullBrand.assets?.icon ? `/brand/${fullBrand.assets.icon}` : brandLogoPath;
12706
13010
  } catch {
@@ -12718,8 +13022,8 @@ function readInstalledVersion() {
12718
13022
  try {
12719
13023
  if (!PLATFORM_ROOT7) return "unknown";
12720
13024
  const versionFile = join10(PLATFORM_ROOT7, "config", `.${BRAND.hostname}-version`);
12721
- if (!existsSync22(versionFile)) return "unknown";
12722
- const content = readFileSync16(versionFile, "utf-8").trim();
13025
+ if (!existsSync24(versionFile)) return "unknown";
13026
+ const content = readFileSync17(versionFile, "utf-8").trim();
12723
13027
  return content || "unknown";
12724
13028
  } catch {
12725
13029
  return "unknown";
@@ -12760,7 +13064,7 @@ var clientErrorReporterScript = `<script>
12760
13064
  function cachedHtml(file) {
12761
13065
  let html = htmlCache.get(file);
12762
13066
  if (!html) {
12763
- html = readFileSync16(resolve23(process.cwd(), "public", file), "utf-8");
13067
+ html = readFileSync17(resolve24(process.cwd(), "public", file), "utf-8");
12764
13068
  const productNameEsc = escapeHtml(BRAND.productName);
12765
13069
  html = html.replace(/<title>([^<]*)<\/title>/, (_match, inner) => `<title>${escapeHtml(inner).replace(/Maxy/g, productNameEsc)}</title>`);
12766
13070
  html = html.replace('href="/favicon.ico"', `href="${escapeHtml(brandFaviconPath)}"`);
@@ -12779,13 +13083,13 @@ function loadBrandingCache(agentSlug) {
12779
13083
  const configDir2 = join10(homedir2(), BRAND.configDir);
12780
13084
  try {
12781
13085
  const accountJsonPath = join10(configDir2, "account.json");
12782
- if (!existsSync22(accountJsonPath)) return null;
12783
- const account = JSON.parse(readFileSync16(accountJsonPath, "utf-8"));
13086
+ if (!existsSync24(accountJsonPath)) return null;
13087
+ const account = JSON.parse(readFileSync17(accountJsonPath, "utf-8"));
12784
13088
  const accountId = account.accountId;
12785
13089
  if (!accountId) return null;
12786
13090
  const cachePath = join10(configDir2, "branding-cache", accountId, `${agentSlug}.json`);
12787
- if (!existsSync22(cachePath)) return null;
12788
- return JSON.parse(readFileSync16(cachePath, "utf-8"));
13091
+ if (!existsSync24(cachePath)) return null;
13092
+ return JSON.parse(readFileSync17(cachePath, "utf-8"));
12789
13093
  } catch {
12790
13094
  return null;
12791
13095
  }
@@ -12794,8 +13098,8 @@ function resolveDefaultSlug() {
12794
13098
  try {
12795
13099
  const configDir2 = join10(homedir2(), BRAND.configDir);
12796
13100
  const accountJsonPath = join10(configDir2, "account.json");
12797
- if (!existsSync22(accountJsonPath)) return null;
12798
- const account = JSON.parse(readFileSync16(accountJsonPath, "utf-8"));
13101
+ if (!existsSync24(accountJsonPath)) return null;
13102
+ const account = JSON.parse(readFileSync17(accountJsonPath, "utf-8"));
12799
13103
  return account.defaultAgent || null;
12800
13104
  } catch {
12801
13105
  return null;
@@ -12831,7 +13135,7 @@ function brandedPublicHtml(agentSlug) {
12831
13135
  function escapeHtml(s) {
12832
13136
  return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
12833
13137
  }
12834
- app35.get("/", (c) => {
13138
+ app36.get("/", (c) => {
12835
13139
  const host = (c.req.header("host") ?? "").split(":")[0];
12836
13140
  if (isPublicHost(host)) {
12837
13141
  const defaultSlug = resolveDefaultSlug();
@@ -12839,12 +13143,12 @@ app35.get("/", (c) => {
12839
13143
  }
12840
13144
  return c.html(cachedHtml("index.html"));
12841
13145
  });
12842
- app35.get("/public", (c) => {
13146
+ app36.get("/public", (c) => {
12843
13147
  const host = (c.req.header("host") ?? "").split(":")[0];
12844
13148
  if (isPublicHost(host)) return c.text("Not found", 404);
12845
13149
  return c.html(cachedHtml("public.html"));
12846
13150
  });
12847
- app35.get("/chat", (c) => {
13151
+ app36.get("/chat", (c) => {
12848
13152
  const host = (c.req.header("host") ?? "").split(":")[0];
12849
13153
  if (isPublicHost(host)) return c.text("Not found", 404);
12850
13154
  return c.html(cachedHtml("public.html"));
@@ -12863,12 +13167,12 @@ async function logViewerFetch(c, next) {
12863
13167
  duration_ms: Date.now() - start
12864
13168
  });
12865
13169
  }
12866
- app35.use("/vnc-viewer.html", logViewerFetch);
12867
- app35.use("/vnc-popout.html", logViewerFetch);
12868
- app35.get("/vnc-popout.html", (c) => {
13170
+ app36.use("/vnc-viewer.html", logViewerFetch);
13171
+ app36.use("/vnc-popout.html", logViewerFetch);
13172
+ app36.get("/vnc-popout.html", (c) => {
12869
13173
  let html = htmlCache.get("vnc-popout.html");
12870
13174
  if (!html) {
12871
- html = readFileSync16(resolve23(process.cwd(), "public", "vnc-popout.html"), "utf-8");
13175
+ html = readFileSync17(resolve24(process.cwd(), "public", "vnc-popout.html"), "utf-8");
12872
13176
  const name = escapeHtml(BRAND.productName);
12873
13177
  html = html.replace("<title>Browser \u2014 Maxy</title>", `<title>${name}</title>`);
12874
13178
  html = html.replace("</head>", ` ${brandScript}
@@ -12878,7 +13182,7 @@ app35.get("/vnc-popout.html", (c) => {
12878
13182
  }
12879
13183
  return c.html(html);
12880
13184
  });
12881
- app35.post("/api/vnc/client-event", async (c) => {
13185
+ app36.post("/api/vnc/client-event", async (c) => {
12882
13186
  let body;
12883
13187
  try {
12884
13188
  body = await c.req.json();
@@ -12899,20 +13203,20 @@ app35.post("/api/vnc/client-event", async (c) => {
12899
13203
  });
12900
13204
  return c.json({ ok: true });
12901
13205
  });
12902
- app35.get("/g/:slug", (c) => {
13206
+ app36.get("/g/:slug", (c) => {
12903
13207
  return c.html(brandedPublicHtml());
12904
13208
  });
12905
- app35.get("/graph", (c) => {
13209
+ app36.get("/graph", (c) => {
12906
13210
  const host = (c.req.header("host") ?? "").split(":")[0];
12907
13211
  if (isPublicHost(host)) return c.text("Not found", 404);
12908
13212
  return c.html(cachedHtml("graph.html"));
12909
13213
  });
12910
- app35.get("/data", (c) => {
13214
+ app36.get("/data", (c) => {
12911
13215
  const host = (c.req.header("host") ?? "").split(":")[0];
12912
13216
  if (isPublicHost(host)) return c.text("Not found", 404);
12913
13217
  return c.html(cachedHtml("data.html"));
12914
13218
  });
12915
- app35.get("/:slug", async (c, next) => {
13219
+ app36.get("/:slug", async (c, next) => {
12916
13220
  const slug = c.req.param("slug");
12917
13221
  if (AGENT_SLUG_PATTERN.test(`/${slug}`)) {
12918
13222
  const branding = loadBrandingCache(slug);
@@ -12921,10 +13225,10 @@ app35.get("/:slug", async (c, next) => {
12921
13225
  }
12922
13226
  await next();
12923
13227
  });
12924
- app35.use("/*", serveStatic({ root: "./public" }));
13228
+ app36.use("/*", serveStatic({ root: "./public" }));
12925
13229
  var port = parseInt(process.env.MAXY_UI_INTERNAL_PORT ?? process.env.PORT ?? "19199", 10);
12926
13230
  var hostname = process.env.HOSTNAME ?? "127.0.0.1";
12927
- var httpServer = serve({ fetch: app35.fetch, port, hostname });
13231
+ var httpServer = serve({ fetch: app36.fetch, port, hostname });
12928
13232
  console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
12929
13233
  var SUBAPP_MANIFEST = [
12930
13234
  { prefix: "/api/health", file: "server/routes/health.ts", subapp: health_default },
@@ -12944,7 +13248,7 @@ for (const m of SUBAPP_MANIFEST) {
12944
13248
  }
12945
13249
  try {
12946
13250
  const registered = [];
12947
- for (const r of app35.routes ?? []) {
13251
+ for (const r of app36.routes ?? []) {
12948
13252
  if (typeof r.path !== "string" || r.path.includes(":") || r.path.includes("*")) continue;
12949
13253
  if (AGENT_SLUG_PATTERN.test(r.path)) {
12950
13254
  registered.push({ method: (r.method ?? "ALL").toUpperCase(), path: r.path });
@@ -12958,8 +13262,8 @@ try {
12958
13262
  (async () => {
12959
13263
  try {
12960
13264
  let userId = "";
12961
- if (existsSync22(USERS_FILE)) {
12962
- const users = JSON.parse(readFileSync16(USERS_FILE, "utf-8").trim() || "[]");
13265
+ if (existsSync24(USERS_FILE)) {
13266
+ const users = JSON.parse(readFileSync17(USERS_FILE, "utf-8").trim() || "[]");
12963
13267
  userId = users[0]?.userId ?? "";
12964
13268
  }
12965
13269
  await backfillNullUserIdConversations(userId);
@@ -12985,7 +13289,7 @@ startGraphHealthTimer();
12985
13289
  var configDirForWhatsApp = basename7(MAXY_DIR) || ".maxy";
12986
13290
  var bootAccount = resolveAccount();
12987
13291
  var bootAccountConfig = bootAccount?.config;
12988
- var bootPublicAgent = bootAccount ? getPublicAgent(bootAccount.accountDir) : null;
13292
+ var bootPublicAgent = bootAccount ? resolvePublicAgent(bootAccount.accountDir, { accountId: bootAccount.accountId })?.slug ?? null : null;
12989
13293
  var bootEntitlement = bootAccountConfig ? resolveEntitlement(
12990
13294
  { configDir: MAXY_DIR, platformRoot: PLATFORM_ROOT, commercialMode: COMMERCIAL_MODE },
12991
13295
  {
@@ -13026,7 +13330,7 @@ if (bootAccountConfig?.whatsapp) {
13026
13330
  }
13027
13331
  init({
13028
13332
  configDir: configDirForWhatsApp,
13029
- platformRoot: resolve23(process.env.MAXY_PLATFORM_ROOT ?? join10(__dirname, "..")),
13333
+ platformRoot: resolve24(process.env.MAXY_PLATFORM_ROOT ?? join10(__dirname, "..")),
13030
13334
  accountConfig: bootAccountConfig,
13031
13335
  onMessage: async (msg) => {
13032
13336
  try {
@@ -13050,12 +13354,20 @@ init({
13050
13354
  if (msg.agentType === "admin") {
13051
13355
  agentName = "admin";
13052
13356
  } else {
13053
- const publicAgentSlug = bootAccount ? getPublicAgent(bootAccount.accountDir) : null;
13054
- if (!publicAgentSlug) {
13055
- console.error(`[whatsapp:route] rejected: no publicAgent configured account=${msg.accountId} from=${msg.senderPhone}`);
13357
+ const resolved = bootAccount ? resolvePublicAgent(bootAccount.accountDir, { accountId: msg.accountId, groupJid: msg.groupJid }) : null;
13358
+ console.error(`[whatsapp:route] resolved publicAgent account=${msg.accountId} groupJid=${msg.groupJid ?? "none"} source=${resolved?.source ?? "null"} slug=${resolved?.slug ?? "none"}`);
13359
+ if (!resolved) {
13360
+ console.error(`[whatsapp:route] rejected: no publicAgent configured account=${msg.accountId} groupJid=${msg.groupJid ?? "none"} from=${msg.senderPhone}`);
13056
13361
  return;
13057
13362
  }
13058
- agentName = publicAgentSlug;
13363
+ if (bootAccount) {
13364
+ const agentConfigPath = resolveBoot(bootAccount.accountDir, "agents", resolved.slug, "config.json");
13365
+ if (!existsSyncBoot(agentConfigPath)) {
13366
+ console.error(`[whatsapp:route] rejected: slug-missing-agent-dir account=${msg.accountId} groupJid=${msg.groupJid ?? "none"} slug=${resolved.slug} source=${resolved.source} expected=${agentConfigPath}`);
13367
+ return;
13368
+ }
13369
+ }
13370
+ agentName = resolved.slug;
13059
13371
  }
13060
13372
  let enrichedText = msg.text || msg.mediaPath || msg.mediaType || msg.replyContext ? buildEnrichedText2(msg.text ?? "") : "";
13061
13373
  if (msg.sessionKey) {