@rubytech/create-realagent 1.0.795 → 1.0.798

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 (62) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/neo4j/migrations/003-person-name-eradicate.cypher +24 -0
  3. package/payload/platform/neo4j/schema.cypher +34 -0
  4. package/payload/platform/plugins/admin/PLUGIN.md +4 -0
  5. package/payload/platform/plugins/admin/hooks/pre-tool-use.sh +9 -5
  6. package/payload/platform/plugins/admin/mcp/dist/index.js +54 -18
  7. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  8. package/payload/platform/plugins/docs/references/internals.md +8 -0
  9. package/payload/platform/plugins/docs/references/settings.md +6 -0
  10. package/payload/platform/plugins/memory/PLUGIN.md +4 -2
  11. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.js +11 -0
  12. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.js.map +1 -1
  13. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-validator.test.js +124 -1
  14. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-validator.test.js.map +1 -1
  15. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.d.ts +12 -0
  16. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.d.ts.map +1 -1
  17. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.js +41 -2
  18. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.js.map +1 -1
  19. package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.d.ts +9 -0
  20. package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.d.ts.map +1 -1
  21. package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.js +44 -0
  22. package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.js.map +1 -1
  23. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-archive-write.test.js +20 -2
  24. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-archive-write.test.js.map +1 -1
  25. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts +1 -1
  26. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts.map +1 -1
  27. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js +0 -1
  28. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js.map +1 -1
  29. package/payload/platform/plugins/memory/references/schema-base.md +11 -1
  30. package/payload/platform/plugins/whatsapp/mcp/dist/call-api.d.ts +14 -0
  31. package/payload/platform/plugins/whatsapp/mcp/dist/call-api.d.ts.map +1 -0
  32. package/payload/platform/plugins/whatsapp/mcp/dist/call-api.js +42 -0
  33. package/payload/platform/plugins/whatsapp/mcp/dist/call-api.js.map +1 -0
  34. package/payload/platform/plugins/whatsapp/mcp/dist/index.js +50 -19
  35. package/payload/platform/plugins/whatsapp/mcp/dist/index.js.map +1 -1
  36. package/payload/platform/scripts/logs-read.sh +17 -12
  37. package/payload/platform/templates/agents/admin/IDENTITY.md +6 -0
  38. package/payload/server/chunk-BURNRCKP.js +3405 -0
  39. package/payload/server/chunk-JSBRDJBE.js +30 -0
  40. package/payload/server/chunk-KM23Y7SY.js +9896 -0
  41. package/payload/server/chunk-SBHI2NMF.js +9910 -0
  42. package/payload/server/chunk-WHF6YXJ5.js +3456 -0
  43. package/payload/server/client-pool-4YDRTKAT.js +29 -0
  44. package/payload/server/client-pool-PV45NUTN.js +29 -0
  45. package/payload/server/maxy-edge.js +3 -2
  46. package/payload/server/neo4j-migrations-IUSBODOP.js +51 -0
  47. package/payload/server/public/assets/admin-C0lKk6WM.js +352 -0
  48. package/payload/server/public/assets/data-BvV94XHO.js +1 -0
  49. package/payload/server/public/assets/graph-CBu0rtrP.js +1 -0
  50. package/payload/server/public/assets/page-BNM63zsb.js +50 -0
  51. package/payload/server/public/assets/page-DM19J3ur.js +1 -0
  52. package/payload/server/public/assets/useAdminFetch-iYCQ9lT0.js +1 -0
  53. package/payload/server/public/data.html +3 -3
  54. package/payload/server/public/graph.html +3 -3
  55. package/payload/server/public/index.html +4 -4
  56. package/payload/server/server.js +470 -137
  57. package/payload/server/public/assets/admin-D8wbpnrW.js +0 -352
  58. package/payload/server/public/assets/data-BhrQjgR5.js +0 -1
  59. package/payload/server/public/assets/graph-Jj7seS-w.js +0 -1
  60. package/payload/server/public/assets/page-DIG7s5Jp.js +0 -1
  61. package/payload/server/public/assets/page-sZb3wcOM.js +0 -50
  62. package/payload/server/public/assets/share-2-BndjMKeG.js +0 -1
@@ -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,14 +50,12 @@ import {
49
50
  vncLog,
50
51
  waitForExit,
51
52
  writeChromiumWrapper
52
- } from "./chunk-3SQJW5Y5.js";
53
+ } from "./chunk-SBHI2NMF.js";
53
54
  import {
54
55
  ACCOUNTS_DIR,
55
56
  GREETING_DIRECTIVE,
56
57
  HAIKU_MODEL,
57
58
  PLATFORM_ROOT,
58
- __commonJS,
59
- __toESM,
60
59
  agentLogStream,
61
60
  backfillNullUserIdConversations,
62
61
  bindVisitorToGroup,
@@ -103,6 +102,7 @@ import {
103
102
  resolveAgentConfig,
104
103
  resolveDefaultAgentSlug,
105
104
  resolveUserAccounts,
105
+ runBootMigrations,
106
106
  setAgentSessionId,
107
107
  setConversationIdForSession,
108
108
  setGroupContextForSession,
@@ -113,7 +113,11 @@ import {
113
113
  verifyAndGetConversationUpdatedAt,
114
114
  verifyConversationOwnership,
115
115
  writeAdminUserAndPerson
116
- } from "./chunk-2N7XJW6Q.js";
116
+ } from "./chunk-WHF6YXJ5.js";
117
+ import {
118
+ __commonJS,
119
+ __toESM
120
+ } from "./chunk-JSBRDJBE.js";
117
121
 
118
122
  // ../lib/graph-trash/dist/index.js
119
123
  var require_dist = __commonJS({
@@ -612,8 +616,8 @@ var serveStatic = (options = { root: "" }) => {
612
616
  };
613
617
 
614
618
  // server/index.ts
615
- import { readFileSync as readFileSync16, existsSync as existsSync22, watchFile } from "fs";
616
- 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";
617
621
  import { homedir as homedir2 } from "os";
618
622
 
619
623
  // app/lib/agent-slug-pattern.ts
@@ -2193,9 +2197,10 @@ var WhatsAppAccountSchema = z.object({
2193
2197
  groups: z.record(
2194
2198
  z.string(),
2195
2199
  z.object({
2196
- 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.")
2197
2202
  }).strict().optional()
2198
- ).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)."),
2199
2204
  ackReaction: z.object({
2200
2205
  emoji: z.string().optional(),
2201
2206
  direct: z.boolean().optional().default(true),
@@ -2449,20 +2454,107 @@ function setPublicAgent(accountDir, slug) {
2449
2454
  return { ok: false, error: msg };
2450
2455
  }
2451
2456
  }
2452
- function getPublicAgent(accountDir) {
2457
+ function resolvePublicAgent(accountDir, opts) {
2453
2458
  try {
2454
2459
  const config = readConfig(accountDir);
2455
2460
  const wa = config.whatsapp;
2456
2461
  if (!wa) return null;
2457
- const slug = wa.publicAgent;
2458
- if (typeof slug === "string" && slug.trim()) {
2459
- 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" };
2460
2481
  }
2461
2482
  return null;
2462
2483
  } catch {
2463
2484
  return null;
2464
2485
  }
2465
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
+ }
2466
2558
  function updateConfig(accountDir, fields) {
2467
2559
  const fieldNames = Object.keys(fields);
2468
2560
  if (fieldNames.length === 0) {
@@ -2698,7 +2790,7 @@ var credsSaveQueue = Promise.resolve();
2698
2790
  async function drainCredsSaveQueue(timeoutMs = 5e3) {
2699
2791
  console.error(`${TAG3} draining credential save queue\u2026`);
2700
2792
  const timer2 = new Promise(
2701
- (resolve24) => setTimeout(() => resolve24("timeout"), timeoutMs)
2793
+ (resolve25) => setTimeout(() => resolve25("timeout"), timeoutMs)
2702
2794
  );
2703
2795
  const result = await Promise.race([
2704
2796
  credsSaveQueue.then(() => "drained"),
@@ -2826,11 +2918,11 @@ async function createWaSocket(opts) {
2826
2918
  return sock;
2827
2919
  }
2828
2920
  async function waitForConnection(sock) {
2829
- return new Promise((resolve24, reject) => {
2921
+ return new Promise((resolve25, reject) => {
2830
2922
  const handler = (update) => {
2831
2923
  if (update.connection === "open") {
2832
2924
  sock.ev.off("connection.update", handler);
2833
- resolve24();
2925
+ resolve25();
2834
2926
  }
2835
2927
  if (update.connection === "close") {
2836
2928
  sock.ev.off("connection.update", handler);
@@ -2944,14 +3036,14 @@ ${inspected}`;
2944
3036
  return inspect2(err, INSPECT_OPTS2);
2945
3037
  }
2946
3038
  function withTimeout(label, promise, timeoutMs) {
2947
- return new Promise((resolve24, reject) => {
3039
+ return new Promise((resolve25, reject) => {
2948
3040
  const timer2 = setTimeout(() => {
2949
3041
  reject(new Error(`${label} timed out after ${timeoutMs}ms`));
2950
3042
  }, timeoutMs);
2951
3043
  promise.then(
2952
3044
  (value) => {
2953
3045
  clearTimeout(timer2);
2954
- resolve24(value);
3046
+ resolve25(value);
2955
3047
  },
2956
3048
  (err) => {
2957
3049
  clearTimeout(timer2);
@@ -4165,11 +4257,11 @@ async function connectWithReconnect(conn) {
4165
4257
  console.error(
4166
4258
  `${TAG11} reconnecting account=${conn.accountId} in ${delay}ms (attempt ${decision.nextAttempts}/${maxAttempts})`
4167
4259
  );
4168
- await new Promise((resolve24) => {
4169
- const timer2 = setTimeout(resolve24, delay);
4260
+ await new Promise((resolve25) => {
4261
+ const timer2 = setTimeout(resolve25, delay);
4170
4262
  conn.abortController.signal.addEventListener("abort", () => {
4171
4263
  clearTimeout(timer2);
4172
- resolve24();
4264
+ resolve25();
4173
4265
  }, { once: true });
4174
4266
  });
4175
4267
  }
@@ -4177,16 +4269,16 @@ async function connectWithReconnect(conn) {
4177
4269
  }
4178
4270
  }
4179
4271
  function waitForDisconnectEvent(conn) {
4180
- return new Promise((resolve24) => {
4272
+ return new Promise((resolve25) => {
4181
4273
  if (!conn.sock) {
4182
- resolve24();
4274
+ resolve25();
4183
4275
  return;
4184
4276
  }
4185
4277
  const sock = conn.sock;
4186
4278
  const handler = (update) => {
4187
4279
  if (update.connection === "close") {
4188
4280
  sock.ev.off("connection.update", handler);
4189
- resolve24();
4281
+ resolve25();
4190
4282
  }
4191
4283
  };
4192
4284
  sock.ev.on("connection.update", handler);
@@ -4403,8 +4495,8 @@ async function handleInboundMessage(conn, msg) {
4403
4495
  const conversationKey = isGroup ? remoteJid : senderPhone;
4404
4496
  const debounceKey = `${conn.accountId}:${conversationKey}:${senderPhone}`;
4405
4497
  let resolvePending;
4406
- const sttPending = new Promise((resolve24) => {
4407
- resolvePending = resolve24;
4498
+ const sttPending = new Promise((resolve25) => {
4499
+ resolvePending = resolve25;
4408
4500
  });
4409
4501
  if (conn.debouncer) conn.debouncer.registerPending(debounceKey, sttPending);
4410
4502
  try {
@@ -4517,20 +4609,20 @@ async function probeApiKey() {
4517
4609
  return result.status;
4518
4610
  }
4519
4611
  function checkPort(port2, timeoutMs = 500) {
4520
- return new Promise((resolve24) => {
4612
+ return new Promise((resolve25) => {
4521
4613
  const socket = createConnection(port2, "127.0.0.1");
4522
4614
  socket.setTimeout(timeoutMs);
4523
4615
  socket.once("connect", () => {
4524
4616
  socket.destroy();
4525
- resolve24(true);
4617
+ resolve25(true);
4526
4618
  });
4527
4619
  socket.once("error", () => {
4528
4620
  socket.destroy();
4529
- resolve24(false);
4621
+ resolve25(false);
4530
4622
  });
4531
4623
  socket.once("timeout", () => {
4532
4624
  socket.destroy();
4533
- resolve24(false);
4625
+ resolve25(false);
4534
4626
  });
4535
4627
  });
4536
4628
  }
@@ -4726,7 +4818,7 @@ app2.post("/", async (c) => {
4726
4818
  }
4727
4819
  const cookieHeader = c.req.header("cookie");
4728
4820
  if (body.session_key && typeof body.session_key === "string") {
4729
- if (validateSession(body.session_key, "public")) {
4821
+ if (validateSession(body.session_key, "public").ok) {
4730
4822
  const storedAgent = getAgentNameForSession(body.session_key);
4731
4823
  if (storedAgent && storedAgent !== agentSlug) {
4732
4824
  console.log(`[session] hot-resume agent mismatch: session=${body.session_key.slice(0, 8)}\u2026 has=${storedAgent} requested=${agentSlug} \u2014 creating new session`);
@@ -5414,7 +5506,7 @@ app3.post("/", async (c) => {
5414
5506
  if (!session_key) {
5415
5507
  return c.json({ error: "session_key required" }, 400);
5416
5508
  }
5417
- if (!validateSession(session_key, "public")) {
5509
+ if (!validateSession(session_key, "public").ok) {
5418
5510
  return c.json({ error: "Invalid or expired session" }, 401);
5419
5511
  }
5420
5512
  const files = formData.getAll("attachments");
@@ -5483,7 +5575,7 @@ app3.post("/", async (c) => {
5483
5575
  if (!message) {
5484
5576
  return c.json({ error: "message required" }, 400);
5485
5577
  }
5486
- if (!validateSession(session_key, "public")) {
5578
+ if (!validateSession(session_key, "public").ok) {
5487
5579
  return c.json({ error: "Invalid or expired session" }, 401);
5488
5580
  }
5489
5581
  }
@@ -5623,7 +5715,7 @@ app4.get("/messages", async (c) => {
5623
5715
  if (isNaN(sinceDate.getTime())) {
5624
5716
  return c.json({ error: "Invalid since parameter \u2014 expected ISO 8601" }, 400);
5625
5717
  }
5626
- if (!validateSession(sessionKey, "public")) {
5718
+ if (!validateSession(sessionKey, "public").ok) {
5627
5719
  return c.json({ error: "Session expired" }, 401);
5628
5720
  }
5629
5721
  const groupSlug = getGroupSlugForSession(sessionKey);
@@ -6247,7 +6339,7 @@ app5.post("/create-credentials", async (c) => {
6247
6339
  const { session_key, password } = body;
6248
6340
  if (!session_key || typeof session_key !== "string") return c.json({ error: "session_key required" }, 400);
6249
6341
  if (!password || typeof password !== "string") return c.json({ error: "password required" }, 400);
6250
- if (!validateSession(session_key, "public")) {
6342
+ if (!validateSession(session_key, "public").ok) {
6251
6343
  return c.json({ error: "Invalid or expired session" }, 401);
6252
6344
  }
6253
6345
  const grant = getGrantForSession(session_key);
@@ -6743,8 +6835,8 @@ async function startLogin(opts) {
6743
6835
  resetActiveLogin(accountId);
6744
6836
  let resolveQr = null;
6745
6837
  let rejectQr = null;
6746
- const qrPromise = new Promise((resolve24, reject) => {
6747
- resolveQr = resolve24;
6838
+ const qrPromise = new Promise((resolve25, reject) => {
6839
+ resolveQr = resolve25;
6748
6840
  rejectQr = reject;
6749
6841
  });
6750
6842
  const qrTimer = setTimeout(
@@ -7071,7 +7163,7 @@ app7.post("/send", async (c) => {
7071
7163
  app7.post("/config", async (c) => {
7072
7164
  try {
7073
7165
  const body = await c.req.json().catch(() => ({}));
7074
- const { action, phone, slug, fields, accountId } = body;
7166
+ const { action, phone, slug, groupJid, fields, accountId } = body;
7075
7167
  if (!action || typeof action !== "string") {
7076
7168
  return c.json({ ok: false, error: 'Missing required field "action".' }, 400);
7077
7169
  }
@@ -7110,9 +7202,32 @@ app7.post("/config", async (c) => {
7110
7202
  return c.json(result, result.ok ? 200 : 400);
7111
7203
  }
7112
7204
  case "get-public-agent": {
7113
- const currentSlug = getPublicAgent(account.accountDir);
7114
- console.error(`${TAG16} config action=get-public-agent slug=${currentSlug ?? "none"}`);
7115
- 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);
7116
7231
  }
7117
7232
  case "list-public-agents": {
7118
7233
  const agentsDir = resolve10(account.accountDir, "agents");
@@ -7180,7 +7295,7 @@ app7.post("/config", async (c) => {
7180
7295
  }
7181
7296
  default:
7182
7297
  return c.json(
7183
- { 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.` },
7184
7299
  400
7185
7300
  );
7186
7301
  }
@@ -7813,7 +7928,7 @@ async function createAdminSession(accountId, thinkingView, userId, userName, rol
7813
7928
  var app10 = new Hono();
7814
7929
  app10.get("/", async (c) => {
7815
7930
  const sessionKey = c.req.query("session_key");
7816
- if (!sessionKey || !validateSession(sessionKey, "admin")) {
7931
+ if (!sessionKey || !validateSession(sessionKey, "admin").ok) {
7817
7932
  return c.json({ error: "Invalid or expired admin session" }, 401);
7818
7933
  }
7819
7934
  const accountId = getAccountIdForSession(sessionKey);
@@ -8210,7 +8325,7 @@ var app11 = new Hono();
8210
8325
  app11.post("/cancel", requireAdminSession, async (c) => {
8211
8326
  const session_key = c.var.sessionKey;
8212
8327
  try {
8213
- const { interruptClient: interruptClient2 } = await import("./client-pool-CTMWNDMO.js");
8328
+ const { interruptClient: interruptClient2 } = await import("./client-pool-4YDRTKAT.js");
8214
8329
  await interruptClient2(session_key);
8215
8330
  return c.json({ ok: true });
8216
8331
  } catch (err) {
@@ -8661,10 +8776,15 @@ app13.get("/", async (c) => {
8661
8776
  const filePath = resolve13(dir, safe);
8662
8777
  searched.push(filePath);
8663
8778
  try {
8664
- const content = readFileSync12(filePath, "utf-8");
8665
- const headers = { "Content-Type": "text/plain; charset=utf-8" };
8779
+ const buffer = readFileSync12(filePath);
8780
+ const onDiskBytes = statSync7(filePath).size;
8781
+ const headers = {
8782
+ "Content-Type": "text/plain; charset=utf-8",
8783
+ "Content-Length": String(buffer.byteLength)
8784
+ };
8666
8785
  if (download) headers["Content-Disposition"] = `attachment; filename="${safe}"`;
8667
- return new Response(content, { headers });
8786
+ console.info(`[stream-log-download] file=${safe} bytesServed=${buffer.byteLength} onDiskBytes=${onDiskBytes}`);
8787
+ return new Response(buffer, { headers });
8668
8788
  } catch (err) {
8669
8789
  const reason = err instanceof Error ? err.message : String(err);
8670
8790
  console.debug(`[admin/logs] miss dir=${dir} name=${safe} reason=${reason}`);
@@ -8729,9 +8849,9 @@ app13.get("/", async (c) => {
8729
8849
  const hit = hits[0];
8730
8850
  console.info(`[admin/logs] resolved sessionKey=${sessionKeySlice} conversationId=${conversationIdSlice} shape=${hit.shape} stalePreflushCount=${stalePreflushCount}`);
8731
8851
  try {
8732
- const content = readFileSync12(hit.path, "utf-8");
8733
8852
  const filename = basename5(hit.path);
8734
8853
  if (stalePreflushCount > 0 && !download) {
8854
+ const content = readFileSync12(hit.path, "utf-8");
8735
8855
  return c.json({
8736
8856
  log: content,
8737
8857
  filename,
@@ -8739,9 +8859,15 @@ app13.get("/", async (c) => {
8739
8859
  warnings: stalePreflushPaths.map((path2) => ({ kind: "stale-preflush", path: path2 }))
8740
8860
  });
8741
8861
  }
8742
- const headers = { "Content-Type": "text/plain; charset=utf-8" };
8862
+ const buffer = readFileSync12(hit.path);
8863
+ const onDiskBytes = statSync7(hit.path).size;
8864
+ const headers = {
8865
+ "Content-Type": "text/plain; charset=utf-8",
8866
+ "Content-Length": String(buffer.byteLength)
8867
+ };
8743
8868
  if (download) headers["Content-Disposition"] = `attachment; filename="${filename}"`;
8744
- return new Response(content, { headers });
8869
+ console.info(`[stream-log-download] file=${filename} bytesServed=${buffer.byteLength} onDiskBytes=${onDiskBytes}`);
8870
+ return new Response(buffer, { headers });
8745
8871
  } catch (err) {
8746
8872
  const reason2 = err instanceof Error ? err.message : String(err);
8747
8873
  console.warn(`[admin/logs] read-fail path=${hit.path} reason=${reason2}`);
@@ -8953,7 +9079,35 @@ var agents_default = app16;
8953
9079
  // server/routes/admin/sessions.ts
8954
9080
  import crypto2 from "crypto";
8955
9081
  import { resolve as resolvePath } from "path";
8956
- 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
+ }
8957
9111
  function reconstructAssistantEvents(content, components, conversationId, messageId, streamLogPath) {
8958
9112
  if (components.length === 0) {
8959
9113
  return {
@@ -9106,7 +9260,17 @@ app17.get("/:id/messages", requireAdminSession, async (c) => {
9106
9260
  if (!owned) return c.json({ error: "Conversation not found" }, 404);
9107
9261
  try {
9108
9262
  const messages = await getRecentMessages(conversationId, 50);
9109
- 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 });
9110
9274
  } catch (err) {
9111
9275
  console.error(`[sessions-messages] Failed: ${err instanceof Error ? err.message : String(err)}`);
9112
9276
  return c.json({ error: "Failed to fetch messages" }, 500);
@@ -9139,10 +9303,23 @@ app17.post("/:id/resume", requireAdminSession, async (c) => {
9139
9303
  let totalValid = 0;
9140
9304
  let totalInvalid = 0;
9141
9305
  let totalComponents = 0;
9306
+ let totalAttachments = 0;
9307
+ let totalAttachmentInvalid = 0;
9142
9308
  const rehydrated = messages.map((m) => {
9143
9309
  const components = m.components ?? [];
9310
+ const rawAttachments = m.attachments ?? [];
9144
9311
  if (m.role !== "assistant") {
9145
- 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
+ };
9146
9323
  }
9147
9324
  const { events, valid, invalid, submittedEventIndices } = reconstructAssistantEvents(m.content, components, conversationId, m.messageId, streamLogPath);
9148
9325
  totalValid += valid;
@@ -9159,17 +9336,22 @@ app17.post("/:id/resume", requireAdminSession, async (c) => {
9159
9336
  };
9160
9337
  });
9161
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;
9162
9340
  try {
9163
- 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}
9164
9342
  `);
9165
9343
  if (totalComponents > 0) {
9166
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}
9167
9349
  `);
9168
9350
  }
9169
9351
  } catch {
9170
9352
  }
9171
9353
  const age = formatAge(updatedAt);
9172
- 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`);
9173
9355
  return c.json({ conversationId, messages: rehydrated });
9174
9356
  });
9175
9357
  app17.post("/:id/label", requireAdminSession, async (c) => {
@@ -9444,12 +9626,12 @@ function isValidDomain(value) {
9444
9626
  }
9445
9627
 
9446
9628
  // app/lib/alias-domains.ts
9447
- 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";
9448
9630
  import { dirname as dirname7 } from "path";
9449
9631
  import { resolve as resolve16 } from "path";
9450
9632
  var ALIAS_DOMAINS_PATH = resolve16(MAXY_DIR, "alias-domains.json");
9451
9633
  function readExisting() {
9452
- if (!existsSync18(ALIAS_DOMAINS_PATH)) return /* @__PURE__ */ new Set();
9634
+ if (!existsSync19(ALIAS_DOMAINS_PATH)) return /* @__PURE__ */ new Set();
9453
9635
  try {
9454
9636
  const parsed = JSON.parse(readFileSync14(ALIAS_DOMAINS_PATH, "utf-8"));
9455
9637
  if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
@@ -11786,7 +11968,7 @@ var adherence_default = app30;
11786
11968
  import neo4j3 from "neo4j-driver";
11787
11969
  import { readFile as readFile5, readdir as readdir3, stat as stat5 } from "fs/promises";
11788
11970
  import { resolve as resolve20, relative as relative2, isAbsolute } from "path";
11789
- import { existsSync as existsSync19 } from "fs";
11971
+ import { existsSync as existsSync20 } from "fs";
11790
11972
  var LIMIT = 50;
11791
11973
  var TEXT_MIME_PREFIXES = ["text/", "application/json", "application/markdown"];
11792
11974
  var ADMIN_AGENT_FILES = ["IDENTITY.md", "SOUL.md", "KNOWLEDGE.md"];
@@ -11934,7 +12116,7 @@ async function fetchAgentTemplateRows(accountDir) {
11934
12116
  async function unionSpecialistFilenames(overrideDir, bundledDir) {
11935
12117
  const names = /* @__PURE__ */ new Set();
11936
12118
  for (const dir of [overrideDir, bundledDir]) {
11937
- if (!existsSync19(dir)) continue;
12119
+ if (!existsSync20(dir)) continue;
11938
12120
  try {
11939
12121
  const entries = await readdir3(dir);
11940
12122
  for (const entry of entries) {
@@ -11949,7 +12131,7 @@ async function unionSpecialistFilenames(overrideDir, bundledDir) {
11949
12131
  }
11950
12132
  async function readAgentTemplateRow(inp) {
11951
12133
  let chosenPath = null;
11952
- if (existsSync19(inp.overridePath)) {
12134
+ if (existsSync20(inp.overridePath)) {
11953
12135
  try {
11954
12136
  validateFilePathInAccount(inp.overridePath, inp.overrideRoot);
11955
12137
  chosenPath = inp.overridePath;
@@ -11960,7 +12142,7 @@ async function readAgentTemplateRow(inp) {
11960
12142
  );
11961
12143
  return null;
11962
12144
  }
11963
- } else if (existsSync19(inp.bundledPath)) {
12145
+ } else if (existsSync20(inp.bundledPath)) {
11964
12146
  if (!isWithin(inp.bundledPath, inp.bundledRoot)) {
11965
12147
  console.error(
11966
12148
  `[admin/sidebar-artefacts] agent-template-read-failed agent=${inp.displayName} kind=${inp.logName} error="bundled path outside PLATFORM_ROOT"`
@@ -12002,7 +12184,7 @@ var sidebar_artefacts_default = app31;
12002
12184
  // server/routes/admin/sidebar-artefact-save.ts
12003
12185
  import { mkdir as mkdir4, readdir as readdir4, stat as stat6, writeFile as writeFile5 } from "fs/promises";
12004
12186
  import { resolve as resolve21 } from "path";
12005
- import { existsSync as existsSync20 } from "fs";
12187
+ import { existsSync as existsSync21 } from "fs";
12006
12188
  var ADMIN_AGENT_FILES2 = /* @__PURE__ */ new Set(["IDENTITY.md", "SOUL.md", "KNOWLEDGE.md"]);
12007
12189
  var UUID_RE4 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
12008
12190
  var app32 = new Hono();
@@ -12066,7 +12248,7 @@ async function resolveSavePath(id, accountId, accountDir) {
12066
12248
  }
12067
12249
  if (UUID_RE4.test(id)) {
12068
12250
  const dir = resolve21(ATTACHMENTS_ROOT, accountId, id);
12069
- if (!existsSync20(dir)) {
12251
+ if (!existsSync21(dir)) {
12070
12252
  return { kind: "reject", status: 400, reason: "not-found" };
12071
12253
  }
12072
12254
  try {
@@ -12090,7 +12272,7 @@ var sidebar_artefact_save_default = app32;
12090
12272
 
12091
12273
  // server/routes/admin/sidebar-artefact-content.ts
12092
12274
  import { readFile as readFile6, readdir as readdir5 } from "fs/promises";
12093
- import { existsSync as existsSync21 } from "fs";
12275
+ import { existsSync as existsSync22 } from "fs";
12094
12276
  import { resolve as resolve22 } from "path";
12095
12277
  var UUID_RE5 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
12096
12278
  var app33 = new Hono();
@@ -12104,7 +12286,7 @@ app33.get("/", requireAdminSession, async (c) => {
12104
12286
  return new Response("Not found", { status: 404 });
12105
12287
  }
12106
12288
  const dir = resolve22(ATTACHMENTS_ROOT, accountId, id);
12107
- if (!existsSync21(dir)) {
12289
+ if (!existsSync22(dir)) {
12108
12290
  console.error(`[admin/sidebar-artefact-content] not-found id=${id.slice(0, 8)}`);
12109
12291
  return new Response("Not found", { status: 404 });
12110
12292
  }
@@ -12165,6 +12347,140 @@ app34.route("/sidebar-artefact-save", sidebar_artefact_save_default);
12165
12347
  app34.route("/sidebar-artefact-content", sidebar_artefact_content_default);
12166
12348
  var admin_default = app34;
12167
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
+
12168
12484
  // app/lib/graph-health.ts
12169
12485
  var HOUR_MS = 60 * 60 * 1e3;
12170
12486
  var timer = null;
@@ -12265,12 +12581,12 @@ function clientFrom(c) {
12265
12581
  var PLATFORM_ROOT7 = process.env.MAXY_PLATFORM_ROOT || "";
12266
12582
  var BRAND_JSON_PATH = PLATFORM_ROOT7 ? join10(PLATFORM_ROOT7, "config", "brand.json") : "";
12267
12583
  var BRAND = { productName: "Maxy", hostname: "maxy", configDir: ".maxy", domain: "getmaxy.com" };
12268
- if (BRAND_JSON_PATH && !existsSync22(BRAND_JSON_PATH)) {
12584
+ if (BRAND_JSON_PATH && !existsSync24(BRAND_JSON_PATH)) {
12269
12585
  console.error(`[brand] WARNING: brand.json not found at ${BRAND_JSON_PATH} \u2014 using Maxy defaults`);
12270
12586
  }
12271
- if (BRAND_JSON_PATH && existsSync22(BRAND_JSON_PATH)) {
12587
+ if (BRAND_JSON_PATH && existsSync24(BRAND_JSON_PATH)) {
12272
12588
  try {
12273
- const parsed = JSON.parse(readFileSync16(BRAND_JSON_PATH, "utf-8"));
12589
+ const parsed = JSON.parse(readFileSync17(BRAND_JSON_PATH, "utf-8"));
12274
12590
  BRAND = { ...BRAND, ...parsed };
12275
12591
  } catch (err) {
12276
12592
  console.error(`[brand] Failed to parse brand.json: ${err.message}`);
@@ -12292,8 +12608,8 @@ var brandLoginOpts = {
12292
12608
  var ALIAS_DOMAINS_PATH2 = join10(homedir2(), BRAND.configDir, "alias-domains.json");
12293
12609
  function loadAliasDomains() {
12294
12610
  try {
12295
- if (!existsSync22(ALIAS_DOMAINS_PATH2)) return null;
12296
- 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"));
12297
12613
  if (!Array.isArray(parsed)) {
12298
12614
  console.error("[alias-domains] malformed alias-domains.json \u2014 expected array");
12299
12615
  return null;
@@ -12317,9 +12633,9 @@ watchFile(ALIAS_DOMAINS_PATH2, { interval: 2e3 }, () => {
12317
12633
  function isPublicHost(host) {
12318
12634
  return host.startsWith("public.") || aliasDomains.has(host);
12319
12635
  }
12320
- var app35 = new Hono();
12321
- app35.use("*", clientIpMiddleware);
12322
- app35.use("*", async (c, next) => {
12636
+ var app36 = new Hono();
12637
+ app36.use("*", clientIpMiddleware);
12638
+ app36.use("*", async (c, next) => {
12323
12639
  await next();
12324
12640
  c.header("X-Content-Type-Options", "nosniff");
12325
12641
  c.header("Referrer-Policy", "strict-origin-when-cross-origin");
@@ -12339,10 +12655,11 @@ var PUBLIC_ALLOWED_PREFIXES = [
12339
12655
  "/brand/",
12340
12656
  "/agent-assets/",
12341
12657
  "/generated/",
12342
- "/g/"
12658
+ "/g/",
12659
+ "/sites/"
12343
12660
  ];
12344
12661
  var PUBLIC_ALLOWED_EXACT = ["/favicon.ico"];
12345
- app35.use("*", async (c, next) => {
12662
+ app36.use("*", async (c, next) => {
12346
12663
  const host = (c.req.header("host") ?? "").split(":")[0];
12347
12664
  if (!isPublicHost(host)) {
12348
12665
  await next();
@@ -12382,7 +12699,7 @@ function resolveRemoteAuthOpts() {
12382
12699
  return brandLoginOpts;
12383
12700
  }
12384
12701
  var MAX_LOGIN_BODY = 8 * 1024;
12385
- app35.post("/__remote-auth/login", async (c) => {
12702
+ app36.post("/__remote-auth/login", async (c) => {
12386
12703
  const client = clientFrom(c);
12387
12704
  const clientIp = client.ip || "unknown";
12388
12705
  if (!requestIsTlsTerminated(c)) {
@@ -12426,7 +12743,7 @@ app35.post("/__remote-auth/login", async (c) => {
12426
12743
  }
12427
12744
  });
12428
12745
  });
12429
- app35.get("/__remote-auth/logout", (c) => {
12746
+ app36.get("/__remote-auth/logout", (c) => {
12430
12747
  return new Response(null, {
12431
12748
  status: 302,
12432
12749
  headers: {
@@ -12436,7 +12753,7 @@ app35.get("/__remote-auth/logout", (c) => {
12436
12753
  }
12437
12754
  });
12438
12755
  });
12439
- app35.post("/__remote-auth/change-password", async (c) => {
12756
+ app36.post("/__remote-auth/change-password", async (c) => {
12440
12757
  const client = clientFrom(c);
12441
12758
  const clientIp = client.ip || "unknown";
12442
12759
  const rateLimited = checkRateLimit(client);
@@ -12486,13 +12803,13 @@ app35.post("/__remote-auth/change-password", async (c) => {
12486
12803
  return c.html(renderLoginPage({ ...resolveRemoteAuthOpts(), mode: "change", changeError: "Failed to save password", redirect }), 200);
12487
12804
  }
12488
12805
  });
12489
- app35.get("/__remote-auth/setup", (c) => {
12806
+ app36.get("/__remote-auth/setup", (c) => {
12490
12807
  if (isRemoteAuthConfigured()) {
12491
12808
  return c.redirect("/");
12492
12809
  }
12493
12810
  return c.html(renderLoginPage({ ...resolveRemoteAuthOpts(), mode: "setup" }), 200);
12494
12811
  });
12495
- app35.post("/__remote-auth/set-initial-password", async (c) => {
12812
+ app36.post("/__remote-auth/set-initial-password", async (c) => {
12496
12813
  if (isRemoteAuthConfigured()) {
12497
12814
  return c.redirect("/");
12498
12815
  }
@@ -12528,17 +12845,17 @@ app35.post("/__remote-auth/set-initial-password", async (c) => {
12528
12845
  return c.html(renderLoginPage({ ...resolveRemoteAuthOpts(), mode: "setup", setupError: "Failed to save password. Please try again." }), 200);
12529
12846
  }
12530
12847
  });
12531
- app35.get("/api/remote-auth/status", (c) => {
12848
+ app36.get("/api/remote-auth/status", (c) => {
12532
12849
  return c.json({ configured: isRemoteAuthConfigured() });
12533
12850
  });
12534
- app35.post("/api/remote-auth/set-password", async (c) => {
12851
+ app36.post("/api/remote-auth/set-password", async (c) => {
12535
12852
  let body;
12536
12853
  try {
12537
12854
  body = await c.req.json();
12538
12855
  } catch {
12539
12856
  return c.json({ error: "Invalid request" }, 400);
12540
12857
  }
12541
- if (!body.session_key || !validateSession(body.session_key, "admin")) {
12858
+ if (!body.session_key || !validateSession(body.session_key, "admin").ok) {
12542
12859
  return c.json({ error: "Unauthorized" }, 401);
12543
12860
  }
12544
12861
  if (!body.password) {
@@ -12561,9 +12878,9 @@ app35.post("/api/remote-auth/set-password", async (c) => {
12561
12878
  return c.json({ error: "Failed to save password" }, 500);
12562
12879
  }
12563
12880
  });
12564
- app35.route("/api/_client-error", client_error_default);
12881
+ app36.route("/api/_client-error", client_error_default);
12565
12882
  console.log("[client-error-route] mounted");
12566
- app35.use("*", async (c, next) => {
12883
+ app36.use("*", async (c, next) => {
12567
12884
  const host = (c.req.header("host") ?? "").split(":")[0];
12568
12885
  const path2 = c.req.path;
12569
12886
  if (path2 === "/favicon.ico" || path2.startsWith("/assets/") || path2.startsWith("/brand/")) {
@@ -12596,15 +12913,15 @@ app35.use("*", async (c, next) => {
12596
12913
  console.error(`[remote-auth] login required ip=${clientIp} path=${path2} ${disambig}`);
12597
12914
  return c.html(renderLoginPage({ ...resolveRemoteAuthOpts(), redirect: path2 }), 200);
12598
12915
  });
12599
- app35.route("/api/health", health_default);
12600
- app35.route("/api/session", session_default);
12601
- app35.route("/api/chat", chat_default);
12602
- app35.route("/api/group", group_default);
12603
- app35.route("/api/access", access_default);
12604
- app35.route("/api/telegram", telegram_default);
12605
- app35.route("/api/whatsapp", whatsapp_default);
12606
- app35.route("/api/onboarding", onboarding_default);
12607
- 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);
12608
12925
  var SAFE_SLUG_RE = /^[a-z][a-z0-9-]{2,49}$/;
12609
12926
  var SAFE_FILENAME_RE = /^[a-z0-9_][a-z0-9_.-]{0,99}$/i;
12610
12927
  var IMAGE_MIME = {
@@ -12616,7 +12933,7 @@ var IMAGE_MIME = {
12616
12933
  ".svg": "image/svg+xml",
12617
12934
  ".ico": "image/x-icon"
12618
12935
  };
12619
- app35.get("/agent-assets/:slug/:filename", (c) => {
12936
+ app36.get("/agent-assets/:slug/:filename", (c) => {
12620
12937
  const slug = c.req.param("slug");
12621
12938
  const filename = c.req.param("filename");
12622
12939
  if (!SAFE_SLUG_RE.test(slug)) {
@@ -12632,26 +12949,26 @@ app35.get("/agent-assets/:slug/:filename", (c) => {
12632
12949
  console.error(`[agent-assets] no-account slug=${slug} file=${filename}`);
12633
12950
  return c.text("Not found", 404);
12634
12951
  }
12635
- const filePath = resolve23(account.accountDir, "agents", slug, "assets", filename);
12636
- 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");
12637
12954
  if (!filePath.startsWith(expectedDir + "/")) {
12638
12955
  console.error(`[agent-assets] path-traversal-rejected slug=${slug} file=${filename}`);
12639
12956
  return c.text("Forbidden", 403);
12640
12957
  }
12641
- if (!existsSync22(filePath)) {
12958
+ if (!existsSync24(filePath)) {
12642
12959
  console.error(`[agent-assets] serve slug=${slug} file=${filename} status=404`);
12643
12960
  return c.text("Not found", 404);
12644
12961
  }
12645
12962
  const ext = "." + filename.split(".").pop()?.toLowerCase();
12646
12963
  const contentType = IMAGE_MIME[ext] || "application/octet-stream";
12647
12964
  console.log(`[agent-assets] serve slug=${slug} file=${filename} status=200`);
12648
- const body = readFileSync16(filePath);
12965
+ const body = readFileSync17(filePath);
12649
12966
  return c.body(body, 200, {
12650
12967
  "Content-Type": contentType,
12651
12968
  "Cache-Control": "public, max-age=3600"
12652
12969
  });
12653
12970
  });
12654
- app35.get("/generated/:filename", (c) => {
12971
+ app36.get("/generated/:filename", (c) => {
12655
12972
  const filename = c.req.param("filename");
12656
12973
  if (!SAFE_FILENAME_RE.test(filename) || filename.includes("..")) {
12657
12974
  console.error(`[generated] serve file=${filename} status=403`);
@@ -12662,31 +12979,32 @@ app35.get("/generated/:filename", (c) => {
12662
12979
  console.error(`[generated] serve file=${filename} status=404`);
12663
12980
  return c.text("Not found", 404);
12664
12981
  }
12665
- const filePath = resolve23(account.accountDir, "generated", filename);
12666
- const expectedDir = resolve23(account.accountDir, "generated");
12982
+ const filePath = resolve24(account.accountDir, "generated", filename);
12983
+ const expectedDir = resolve24(account.accountDir, "generated");
12667
12984
  if (!filePath.startsWith(expectedDir + "/")) {
12668
12985
  console.error(`[generated] serve file=${filename} status=403`);
12669
12986
  return c.text("Forbidden", 403);
12670
12987
  }
12671
- if (!existsSync22(filePath)) {
12988
+ if (!existsSync24(filePath)) {
12672
12989
  console.error(`[generated] serve file=${filename} status=404`);
12673
12990
  return c.text("Not found", 404);
12674
12991
  }
12675
12992
  const ext = "." + filename.split(".").pop()?.toLowerCase();
12676
12993
  const contentType = IMAGE_MIME[ext] || "application/octet-stream";
12677
12994
  console.log(`[generated] serve file=${filename} status=200`);
12678
- const body = readFileSync16(filePath);
12995
+ const body = readFileSync17(filePath);
12679
12996
  return c.body(body, 200, {
12680
12997
  "Content-Type": contentType,
12681
12998
  "Cache-Control": "public, max-age=86400"
12682
12999
  });
12683
13000
  });
13001
+ app36.route("/sites", sites_default);
12684
13002
  var htmlCache = /* @__PURE__ */ new Map();
12685
13003
  var brandLogoPath = "/brand/maxy-monochrome.png";
12686
13004
  var brandIconPath = "/brand/maxy-monochrome.png";
12687
- if (BRAND_JSON_PATH && existsSync22(BRAND_JSON_PATH)) {
13005
+ if (BRAND_JSON_PATH && existsSync24(BRAND_JSON_PATH)) {
12688
13006
  try {
12689
- const fullBrand = JSON.parse(readFileSync16(BRAND_JSON_PATH, "utf-8"));
13007
+ const fullBrand = JSON.parse(readFileSync17(BRAND_JSON_PATH, "utf-8"));
12690
13008
  if (fullBrand.assets?.logo) brandLogoPath = `/brand/${fullBrand.assets.logo}`;
12691
13009
  brandIconPath = fullBrand.assets?.icon ? `/brand/${fullBrand.assets.icon}` : brandLogoPath;
12692
13010
  } catch {
@@ -12704,8 +13022,8 @@ function readInstalledVersion() {
12704
13022
  try {
12705
13023
  if (!PLATFORM_ROOT7) return "unknown";
12706
13024
  const versionFile = join10(PLATFORM_ROOT7, "config", `.${BRAND.hostname}-version`);
12707
- if (!existsSync22(versionFile)) return "unknown";
12708
- const content = readFileSync16(versionFile, "utf-8").trim();
13025
+ if (!existsSync24(versionFile)) return "unknown";
13026
+ const content = readFileSync17(versionFile, "utf-8").trim();
12709
13027
  return content || "unknown";
12710
13028
  } catch {
12711
13029
  return "unknown";
@@ -12746,7 +13064,7 @@ var clientErrorReporterScript = `<script>
12746
13064
  function cachedHtml(file) {
12747
13065
  let html = htmlCache.get(file);
12748
13066
  if (!html) {
12749
- html = readFileSync16(resolve23(process.cwd(), "public", file), "utf-8");
13067
+ html = readFileSync17(resolve24(process.cwd(), "public", file), "utf-8");
12750
13068
  const productNameEsc = escapeHtml(BRAND.productName);
12751
13069
  html = html.replace(/<title>([^<]*)<\/title>/, (_match, inner) => `<title>${escapeHtml(inner).replace(/Maxy/g, productNameEsc)}</title>`);
12752
13070
  html = html.replace('href="/favicon.ico"', `href="${escapeHtml(brandFaviconPath)}"`);
@@ -12765,13 +13083,13 @@ function loadBrandingCache(agentSlug) {
12765
13083
  const configDir2 = join10(homedir2(), BRAND.configDir);
12766
13084
  try {
12767
13085
  const accountJsonPath = join10(configDir2, "account.json");
12768
- if (!existsSync22(accountJsonPath)) return null;
12769
- const account = JSON.parse(readFileSync16(accountJsonPath, "utf-8"));
13086
+ if (!existsSync24(accountJsonPath)) return null;
13087
+ const account = JSON.parse(readFileSync17(accountJsonPath, "utf-8"));
12770
13088
  const accountId = account.accountId;
12771
13089
  if (!accountId) return null;
12772
13090
  const cachePath = join10(configDir2, "branding-cache", accountId, `${agentSlug}.json`);
12773
- if (!existsSync22(cachePath)) return null;
12774
- return JSON.parse(readFileSync16(cachePath, "utf-8"));
13091
+ if (!existsSync24(cachePath)) return null;
13092
+ return JSON.parse(readFileSync17(cachePath, "utf-8"));
12775
13093
  } catch {
12776
13094
  return null;
12777
13095
  }
@@ -12780,8 +13098,8 @@ function resolveDefaultSlug() {
12780
13098
  try {
12781
13099
  const configDir2 = join10(homedir2(), BRAND.configDir);
12782
13100
  const accountJsonPath = join10(configDir2, "account.json");
12783
- if (!existsSync22(accountJsonPath)) return null;
12784
- const account = JSON.parse(readFileSync16(accountJsonPath, "utf-8"));
13101
+ if (!existsSync24(accountJsonPath)) return null;
13102
+ const account = JSON.parse(readFileSync17(accountJsonPath, "utf-8"));
12785
13103
  return account.defaultAgent || null;
12786
13104
  } catch {
12787
13105
  return null;
@@ -12817,7 +13135,7 @@ function brandedPublicHtml(agentSlug) {
12817
13135
  function escapeHtml(s) {
12818
13136
  return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
12819
13137
  }
12820
- app35.get("/", (c) => {
13138
+ app36.get("/", (c) => {
12821
13139
  const host = (c.req.header("host") ?? "").split(":")[0];
12822
13140
  if (isPublicHost(host)) {
12823
13141
  const defaultSlug = resolveDefaultSlug();
@@ -12825,12 +13143,12 @@ app35.get("/", (c) => {
12825
13143
  }
12826
13144
  return c.html(cachedHtml("index.html"));
12827
13145
  });
12828
- app35.get("/public", (c) => {
13146
+ app36.get("/public", (c) => {
12829
13147
  const host = (c.req.header("host") ?? "").split(":")[0];
12830
13148
  if (isPublicHost(host)) return c.text("Not found", 404);
12831
13149
  return c.html(cachedHtml("public.html"));
12832
13150
  });
12833
- app35.get("/chat", (c) => {
13151
+ app36.get("/chat", (c) => {
12834
13152
  const host = (c.req.header("host") ?? "").split(":")[0];
12835
13153
  if (isPublicHost(host)) return c.text("Not found", 404);
12836
13154
  return c.html(cachedHtml("public.html"));
@@ -12849,12 +13167,12 @@ async function logViewerFetch(c, next) {
12849
13167
  duration_ms: Date.now() - start
12850
13168
  });
12851
13169
  }
12852
- app35.use("/vnc-viewer.html", logViewerFetch);
12853
- app35.use("/vnc-popout.html", logViewerFetch);
12854
- 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) => {
12855
13173
  let html = htmlCache.get("vnc-popout.html");
12856
13174
  if (!html) {
12857
- html = readFileSync16(resolve23(process.cwd(), "public", "vnc-popout.html"), "utf-8");
13175
+ html = readFileSync17(resolve24(process.cwd(), "public", "vnc-popout.html"), "utf-8");
12858
13176
  const name = escapeHtml(BRAND.productName);
12859
13177
  html = html.replace("<title>Browser \u2014 Maxy</title>", `<title>${name}</title>`);
12860
13178
  html = html.replace("</head>", ` ${brandScript}
@@ -12864,7 +13182,7 @@ app35.get("/vnc-popout.html", (c) => {
12864
13182
  }
12865
13183
  return c.html(html);
12866
13184
  });
12867
- app35.post("/api/vnc/client-event", async (c) => {
13185
+ app36.post("/api/vnc/client-event", async (c) => {
12868
13186
  let body;
12869
13187
  try {
12870
13188
  body = await c.req.json();
@@ -12885,20 +13203,20 @@ app35.post("/api/vnc/client-event", async (c) => {
12885
13203
  });
12886
13204
  return c.json({ ok: true });
12887
13205
  });
12888
- app35.get("/g/:slug", (c) => {
13206
+ app36.get("/g/:slug", (c) => {
12889
13207
  return c.html(brandedPublicHtml());
12890
13208
  });
12891
- app35.get("/graph", (c) => {
13209
+ app36.get("/graph", (c) => {
12892
13210
  const host = (c.req.header("host") ?? "").split(":")[0];
12893
13211
  if (isPublicHost(host)) return c.text("Not found", 404);
12894
13212
  return c.html(cachedHtml("graph.html"));
12895
13213
  });
12896
- app35.get("/data", (c) => {
13214
+ app36.get("/data", (c) => {
12897
13215
  const host = (c.req.header("host") ?? "").split(":")[0];
12898
13216
  if (isPublicHost(host)) return c.text("Not found", 404);
12899
13217
  return c.html(cachedHtml("data.html"));
12900
13218
  });
12901
- app35.get("/:slug", async (c, next) => {
13219
+ app36.get("/:slug", async (c, next) => {
12902
13220
  const slug = c.req.param("slug");
12903
13221
  if (AGENT_SLUG_PATTERN.test(`/${slug}`)) {
12904
13222
  const branding = loadBrandingCache(slug);
@@ -12907,10 +13225,10 @@ app35.get("/:slug", async (c, next) => {
12907
13225
  }
12908
13226
  await next();
12909
13227
  });
12910
- app35.use("/*", serveStatic({ root: "./public" }));
13228
+ app36.use("/*", serveStatic({ root: "./public" }));
12911
13229
  var port = parseInt(process.env.MAXY_UI_INTERNAL_PORT ?? process.env.PORT ?? "19199", 10);
12912
13230
  var hostname = process.env.HOSTNAME ?? "127.0.0.1";
12913
- var httpServer = serve({ fetch: app35.fetch, port, hostname });
13231
+ var httpServer = serve({ fetch: app36.fetch, port, hostname });
12914
13232
  console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
12915
13233
  var SUBAPP_MANIFEST = [
12916
13234
  { prefix: "/api/health", file: "server/routes/health.ts", subapp: health_default },
@@ -12930,7 +13248,7 @@ for (const m of SUBAPP_MANIFEST) {
12930
13248
  }
12931
13249
  try {
12932
13250
  const registered = [];
12933
- for (const r of app35.routes ?? []) {
13251
+ for (const r of app36.routes ?? []) {
12934
13252
  if (typeof r.path !== "string" || r.path.includes(":") || r.path.includes("*")) continue;
12935
13253
  if (AGENT_SLUG_PATTERN.test(r.path)) {
12936
13254
  registered.push({ method: (r.method ?? "ALL").toUpperCase(), path: r.path });
@@ -12944,8 +13262,8 @@ try {
12944
13262
  (async () => {
12945
13263
  try {
12946
13264
  let userId = "";
12947
- if (existsSync22(USERS_FILE)) {
12948
- 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() || "[]");
12949
13267
  userId = users[0]?.userId ?? "";
12950
13268
  }
12951
13269
  await backfillNullUserIdConversations(userId);
@@ -12953,6 +13271,13 @@ try {
12953
13271
  console.error(`[session] backfill startup failed: ${err instanceof Error ? err.message : String(err)}`);
12954
13272
  }
12955
13273
  })();
13274
+ (async () => {
13275
+ try {
13276
+ await runBootMigrations();
13277
+ } catch (err) {
13278
+ console.error(`[migration] runBootMigrations rejected: ${err instanceof Error ? err.message : String(err)}`);
13279
+ }
13280
+ })();
12956
13281
  (async () => {
12957
13282
  try {
12958
13283
  await startReviewDetector();
@@ -12964,7 +13289,7 @@ startGraphHealthTimer();
12964
13289
  var configDirForWhatsApp = basename7(MAXY_DIR) || ".maxy";
12965
13290
  var bootAccount = resolveAccount();
12966
13291
  var bootAccountConfig = bootAccount?.config;
12967
- var bootPublicAgent = bootAccount ? getPublicAgent(bootAccount.accountDir) : null;
13292
+ var bootPublicAgent = bootAccount ? resolvePublicAgent(bootAccount.accountDir, { accountId: bootAccount.accountId })?.slug ?? null : null;
12968
13293
  var bootEntitlement = bootAccountConfig ? resolveEntitlement(
12969
13294
  { configDir: MAXY_DIR, platformRoot: PLATFORM_ROOT, commercialMode: COMMERCIAL_MODE },
12970
13295
  {
@@ -13005,7 +13330,7 @@ if (bootAccountConfig?.whatsapp) {
13005
13330
  }
13006
13331
  init({
13007
13332
  configDir: configDirForWhatsApp,
13008
- platformRoot: resolve23(process.env.MAXY_PLATFORM_ROOT ?? join10(__dirname, "..")),
13333
+ platformRoot: resolve24(process.env.MAXY_PLATFORM_ROOT ?? join10(__dirname, "..")),
13009
13334
  accountConfig: bootAccountConfig,
13010
13335
  onMessage: async (msg) => {
13011
13336
  try {
@@ -13029,12 +13354,20 @@ init({
13029
13354
  if (msg.agentType === "admin") {
13030
13355
  agentName = "admin";
13031
13356
  } else {
13032
- const publicAgentSlug = bootAccount ? getPublicAgent(bootAccount.accountDir) : null;
13033
- if (!publicAgentSlug) {
13034
- 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}`);
13035
13361
  return;
13036
13362
  }
13037
- 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;
13038
13371
  }
13039
13372
  let enrichedText = msg.text || msg.mediaPath || msg.mediaType || msg.replyContext ? buildEnrichedText2(msg.text ?? "") : "";
13040
13373
  if (msg.sessionKey) {