@rubytech/create-maxy 1.0.463 → 1.0.464

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.
@@ -2485,7 +2485,7 @@ var responseViaResponseObject = async (res, outgoing, options = {}) => {
2485
2485
  });
2486
2486
  if (!chunk) {
2487
2487
  if (i === 1) {
2488
- await new Promise((resolve17) => setTimeout(resolve17));
2488
+ await new Promise((resolve18) => setTimeout(resolve18));
2489
2489
  maxReadCount = 3;
2490
2490
  continue;
2491
2491
  }
@@ -2852,7 +2852,7 @@ var serveStatic = (options = { root: "" }) => {
2852
2852
 
2853
2853
  // server/index.ts
2854
2854
  import { readFileSync as readFileSync19, existsSync as existsSync19, watchFile } from "fs";
2855
- import { resolve as resolve16, join as join9, basename as basename2 } from "path";
2855
+ import { resolve as resolve17, join as join9, basename as basename4 } from "path";
2856
2856
  import { homedir as homedir3 } from "os";
2857
2857
 
2858
2858
  // app/api/health/route.ts
@@ -3091,20 +3091,20 @@ async function probeApiKey() {
3091
3091
  }
3092
3092
  }
3093
3093
  function checkPort(port2, timeoutMs = 500) {
3094
- return new Promise((resolve17) => {
3094
+ return new Promise((resolve18) => {
3095
3095
  const socket = createConnection(port2, "127.0.0.1");
3096
3096
  socket.setTimeout(timeoutMs);
3097
3097
  socket.once("connect", () => {
3098
3098
  socket.destroy();
3099
- resolve17(true);
3099
+ resolve18(true);
3100
3100
  });
3101
3101
  socket.once("error", () => {
3102
3102
  socket.destroy();
3103
- resolve17(false);
3103
+ resolve18(false);
3104
3104
  });
3105
3105
  socket.once("timeout", () => {
3106
3106
  socket.destroy();
3107
- resolve17(false);
3107
+ resolve18(false);
3108
3108
  });
3109
3109
  });
3110
3110
  }
@@ -3668,7 +3668,7 @@ ${userContent}`;
3668
3668
  "dontAsk",
3669
3669
  prompt
3670
3670
  ];
3671
- return new Promise((resolve17) => {
3671
+ return new Promise((resolve18) => {
3672
3672
  let stdout = "";
3673
3673
  let stderr = "";
3674
3674
  const spawnFn = _spawnOverride ?? spawn;
@@ -3686,35 +3686,35 @@ ${userContent}`;
3686
3686
  const timer = setTimeout(() => {
3687
3687
  proc.kill("SIGTERM");
3688
3688
  console.error("[persist] autoLabel: haiku subprocess timed out");
3689
- resolve17(null);
3689
+ resolve18(null);
3690
3690
  }, SESSION_LABEL_TIMEOUT_MS);
3691
3691
  proc.on("error", (err) => {
3692
3692
  clearTimeout(timer);
3693
3693
  console.error(`[persist] autoLabel: subprocess error \u2014 ${err.message}`);
3694
- resolve17(null);
3694
+ resolve18(null);
3695
3695
  });
3696
3696
  proc.on("close", (code) => {
3697
3697
  clearTimeout(timer);
3698
3698
  if (code !== 0) {
3699
3699
  console.error(`[persist] autoLabel: subprocess exited code=${code}${stderr ? ` stderr=${stderr.trim().slice(0, 200)}` : ""}`);
3700
- resolve17(null);
3700
+ resolve18(null);
3701
3701
  return;
3702
3702
  }
3703
3703
  const text = stdout.trim();
3704
3704
  if (!text) {
3705
3705
  console.error("[persist] autoLabel: haiku returned empty response");
3706
- resolve17(null);
3706
+ resolve18(null);
3707
3707
  return;
3708
3708
  }
3709
3709
  if (text === "SKIP") {
3710
3710
  console.error("[persist] autoLabel: haiku returned SKIP \u2014 messages too vague");
3711
- resolve17(null);
3711
+ resolve18(null);
3712
3712
  return;
3713
3713
  }
3714
3714
  const words = text.split(/\s+/).slice(0, SESSION_LABEL_MAX_WORDS);
3715
3715
  const label = words.join(" ");
3716
3716
  console.error(`[persist] autoLabel: haiku response="${label}"`);
3717
- resolve17(label);
3717
+ resolve18(label);
3718
3718
  });
3719
3719
  });
3720
3720
  }
@@ -4808,8 +4808,8 @@ async function buildPluginManifest(enabledPlugins) {
4808
4808
  for (const entry of readdirSync(current)) {
4809
4809
  const full = resolve4(current, entry);
4810
4810
  try {
4811
- const stat = statSync2(full);
4812
- if (stat.isDirectory()) {
4811
+ const stat3 = statSync2(full);
4812
+ if (stat3.isDirectory()) {
4813
4813
  walk(full, `${rel}${entry}/`);
4814
4814
  } else if (entry.endsWith(".md")) {
4815
4815
  target.push(`${prefix}${rel}${entry}`);
@@ -5102,7 +5102,7 @@ function getMcpServers(accountId, enabledPlugins) {
5102
5102
  // the always-on Chromium instance started by vnc.sh on display :99.
5103
5103
  "plugin_playwright_playwright": {
5104
5104
  command: "npx",
5105
- args: ["-y", "@playwright/mcp@latest", "--cdp-endpoint", "http://127.0.0.1:9222", "--caps", "pdf"]
5105
+ args: ["-y", "@playwright/mcp@latest", "--cdp-endpoint", "http://127.0.0.1:9222"]
5106
5106
  }
5107
5107
  };
5108
5108
  if (process.env.TELEGRAM_PUBLIC_BOT_TOKEN) {
@@ -5194,6 +5194,7 @@ var ADMIN_CORE_TOOLS = [
5194
5194
  "mcp__whatsapp__whatsapp-status",
5195
5195
  "mcp__whatsapp__whatsapp-disconnect",
5196
5196
  "mcp__whatsapp__whatsapp-send",
5197
+ "mcp__whatsapp__whatsapp-send-document",
5197
5198
  "mcp__whatsapp__whatsapp-config",
5198
5199
  "mcp__admin__system-status",
5199
5200
  "mcp__admin__brand-settings",
@@ -5206,6 +5207,7 @@ var ADMIN_CORE_TOOLS = [
5206
5207
  "mcp__admin__session-resume",
5207
5208
  "mcp__admin__api-key-store",
5208
5209
  "mcp__admin__api-key-verify",
5210
+ "mcp__admin__file-attach",
5209
5211
  "mcp__cloudflare__tunnel-status",
5210
5212
  "mcp__cloudflare__tunnel-install",
5211
5213
  "mcp__cloudflare__tunnel-login",
@@ -5343,7 +5345,7 @@ async function fetchMemoryContext(accountId, query, sessionKey, options) {
5343
5345
  return null;
5344
5346
  }
5345
5347
  const startMs = Date.now();
5346
- return new Promise((resolve17) => {
5348
+ return new Promise((resolve18) => {
5347
5349
  const proc = spawn2(process.execPath, [serverPath], {
5348
5350
  env: {
5349
5351
  ...process.env,
@@ -5372,7 +5374,7 @@ async function fetchMemoryContext(accountId, query, sessionKey, options) {
5372
5374
  } else {
5373
5375
  console.error(`[fetchMemoryContext] failed: ${reason} (${elapsed}ms)${stderrBuf ? ` stderr: ${stderrBuf.slice(0, 500)}` : ""}`);
5374
5376
  }
5375
- resolve17(value);
5377
+ resolve18(value);
5376
5378
  };
5377
5379
  proc.stdout.on("data", (chunk) => {
5378
5380
  buffer += chunk.toString();
@@ -7572,8 +7574,9 @@ ${raw2}`;
7572
7574
 
7573
7575
  // app/lib/attachments.ts
7574
7576
  import { randomUUID as randomUUID2 } from "crypto";
7575
- import { mkdir, writeFile } from "fs/promises";
7576
- import { resolve as resolve6, extname } from "path";
7577
+ import { mkdir, readFile, stat, writeFile } from "fs/promises";
7578
+ import { realpathSync } from "fs";
7579
+ import { resolve as resolve6, extname, basename } from "path";
7577
7580
  var PLATFORM_ROOT4 = process.env.MAXY_PLATFORM_ROOT ?? resolve6(process.cwd(), "../platform");
7578
7581
  var ATTACHMENTS_ROOT = resolve6(PLATFORM_ROOT4, "data/attachments");
7579
7582
  var SUPPORTED_MIME_TYPES = /* @__PURE__ */ new Set([
@@ -7633,6 +7636,44 @@ async function storePublicAttachment(sessionId, file2, buffer) {
7633
7636
  }
7634
7637
  return writeAttachment(`public/${sessionId}`, file2.name, file2.type, file2.size, buffer);
7635
7638
  }
7639
+ var MIME_BY_EXT = {
7640
+ ".pdf": "application/pdf",
7641
+ ".png": "image/png",
7642
+ ".jpg": "image/jpeg",
7643
+ ".jpeg": "image/jpeg",
7644
+ ".gif": "image/gif",
7645
+ ".webp": "image/webp",
7646
+ ".svg": "image/svg+xml",
7647
+ ".html": "text/html",
7648
+ ".htm": "text/html",
7649
+ ".css": "text/css",
7650
+ ".js": "application/javascript",
7651
+ ".json": "application/json",
7652
+ ".csv": "text/csv",
7653
+ ".txt": "text/plain",
7654
+ ".md": "text/markdown",
7655
+ ".xml": "text/xml",
7656
+ ".zip": "application/zip",
7657
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
7658
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
7659
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
7660
+ };
7661
+ function detectMimeType(filePath) {
7662
+ const ext = extname(filePath).toLowerCase();
7663
+ return MIME_BY_EXT[ext] ?? "application/octet-stream";
7664
+ }
7665
+ async function storeGeneratedFile(accountId, filePath) {
7666
+ const fileStat = await stat(filePath);
7667
+ if (fileStat.size > MAX_FILE_SIZE_BYTES) {
7668
+ throw new Error(
7669
+ `File exceeds the 20 MB limit (${(fileStat.size / 1024 / 1024).toFixed(1)} MB).`
7670
+ );
7671
+ }
7672
+ const filename = basename(filePath);
7673
+ const mimeType = detectMimeType(filePath);
7674
+ const buffer = await readFile(filePath);
7675
+ return writeAttachment(accountId, filename, mimeType, fileStat.size, buffer);
7676
+ }
7636
7677
 
7637
7678
  // app/api/chat/route.ts
7638
7679
  function isLocalRequest(req) {
@@ -7844,10 +7885,10 @@ var SCRYPT_R = 8;
7844
7885
  var SCRYPT_P = 1;
7845
7886
  var SCRYPT_KEYLEN = 64;
7846
7887
  function scryptAsync(password, salt) {
7847
- return new Promise((resolve17, reject) => {
7888
+ return new Promise((resolve18, reject) => {
7848
7889
  scrypt(password, salt, SCRYPT_KEYLEN, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P }, (err, key) => {
7849
7890
  if (err) reject(err);
7850
- else resolve17(key);
7891
+ else resolve18(key);
7851
7892
  });
7852
7893
  });
7853
7894
  }
@@ -9400,7 +9441,7 @@ var credsSaveQueue = Promise.resolve();
9400
9441
  async function drainCredsSaveQueue(timeoutMs = 5e3) {
9401
9442
  console.error(`${TAG2} draining credential save queue\u2026`);
9402
9443
  const timer = new Promise(
9403
- (resolve17) => setTimeout(() => resolve17("timeout"), timeoutMs)
9444
+ (resolve18) => setTimeout(() => resolve18("timeout"), timeoutMs)
9404
9445
  );
9405
9446
  const result = await Promise.race([
9406
9447
  credsSaveQueue.then(() => "drained"),
@@ -9541,11 +9582,11 @@ async function createWaSocket(opts) {
9541
9582
  return sock;
9542
9583
  }
9543
9584
  async function waitForConnection(sock) {
9544
- return new Promise((resolve17, reject) => {
9585
+ return new Promise((resolve18, reject) => {
9545
9586
  const handler = (update) => {
9546
9587
  if (update.connection === "open") {
9547
9588
  sock.ev.off("connection.update", handler);
9548
- resolve17();
9589
+ resolve18();
9549
9590
  }
9550
9591
  if (update.connection === "close") {
9551
9592
  sock.ev.off("connection.update", handler);
@@ -9730,8 +9771,8 @@ async function startLogin(opts) {
9730
9771
  resetActiveLogin(accountId);
9731
9772
  let resolveQr = null;
9732
9773
  let rejectQr = null;
9733
- const qrPromise = new Promise((resolve17, reject) => {
9734
- resolveQr = resolve17;
9774
+ const qrPromise = new Promise((resolve18, reject) => {
9775
+ resolveQr = resolve18;
9735
9776
  rejectQr = reject;
9736
9777
  });
9737
9778
  const qrTimer = setTimeout(
@@ -23656,7 +23697,7 @@ var WhatsAppAccountSchema = external_exports.object({
23656
23697
  direct: external_exports.boolean().optional().default(true),
23657
23698
  group: external_exports.enum(["always", "mentions", "never"]).optional().default("mentions")
23658
23699
  }).strict().optional().describe("React to incoming messages with an emoji to acknowledge receipt before the agent responds."),
23659
- debounceMs: external_exports.number().int().nonnegative().optional().default(0).describe("Wait this many milliseconds after the last message before processing, to batch rapid multi-message input into a single agent turn. 0 means process each message immediately."),
23700
+ debounceMs: external_exports.number().int().nonnegative().optional().default(2e3).describe("Wait this many milliseconds after the last message before processing, to batch rapid multi-message input into a single agent turn. Default 2000ms catches most multi-message bursts. 0 means process each message immediately."),
23660
23701
  publicAgent: external_exports.string().optional().describe("Slug of the public agent that handles WhatsApp inbound from non-admin phones for this account. Overrides the top-level publicAgent when set.")
23661
23702
  }).strict().superRefine((value, ctx) => {
23662
23703
  if (value.dmPolicy !== "open") return;
@@ -23678,7 +23719,7 @@ var WhatsAppConfigSchema = external_exports.object({
23678
23719
  sendReadReceipts: external_exports.boolean().optional().default(true).describe("Whether to send read receipts (blue ticks) when messages are received. Disabling may feel less responsive but preserves privacy."),
23679
23720
  mediaMaxMb: external_exports.number().int().positive().optional().default(50).describe("Maximum file size in MB for media the agent will download and process."),
23680
23721
  textChunkLimit: external_exports.number().int().positive().optional().default(4e3).describe("Maximum characters per outbound message. Longer replies are split into multiple messages."),
23681
- debounceMs: external_exports.number().int().nonnegative().optional().default(0).describe("Wait this many milliseconds after the last message before processing, to batch rapid multi-message input into a single agent turn. 0 means process each message immediately."),
23722
+ debounceMs: external_exports.number().int().nonnegative().optional().default(2e3).describe("Wait this many milliseconds after the last message before processing, to batch rapid multi-message input into a single agent turn. Default 2000ms catches most multi-message bursts. 0 means process each message immediately."),
23682
23723
  publicAgent: external_exports.string().optional().describe("Slug of the public agent that handles WhatsApp inbound from non-admin phones. Per-account overrides take precedence.")
23683
23724
  }).strict().superRefine((value, ctx) => {
23684
23725
  if (value.dmPolicy !== "open") return;
@@ -24284,6 +24325,53 @@ async function sendReadReceipt(sock, chatJid, messageIds, participant) {
24284
24325
  } catch {
24285
24326
  }
24286
24327
  }
24328
+ async function sendMediaMessage(sock, to, media) {
24329
+ try {
24330
+ const jid = to.includes("@") ? to : toWhatsappJid(to);
24331
+ let payload;
24332
+ switch (media.type) {
24333
+ case "image":
24334
+ payload = {
24335
+ image: media.buffer,
24336
+ mimetype: media.mimetype,
24337
+ caption: media.caption
24338
+ };
24339
+ break;
24340
+ case "audio":
24341
+ payload = {
24342
+ audio: media.buffer,
24343
+ mimetype: media.mimetype,
24344
+ ptt: media.ptt ?? false
24345
+ };
24346
+ break;
24347
+ case "video":
24348
+ payload = {
24349
+ video: media.buffer,
24350
+ mimetype: media.mimetype,
24351
+ caption: media.caption
24352
+ };
24353
+ break;
24354
+ case "document":
24355
+ payload = {
24356
+ document: media.buffer,
24357
+ mimetype: media.mimetype,
24358
+ fileName: media.filename ?? "file",
24359
+ caption: media.caption
24360
+ };
24361
+ break;
24362
+ }
24363
+ const result = await sock.sendMessage(jid, payload);
24364
+ const messageId = result?.key?.id;
24365
+ if (messageId) {
24366
+ trackAgentSentMessage(messageId);
24367
+ console.error(`${TAG5} sent ${media.type} to=${jid} id=${messageId}`);
24368
+ }
24369
+ return { success: true, messageId: messageId ?? void 0 };
24370
+ } catch (err) {
24371
+ console.error(`${TAG5} send media failed to=${to}: ${String(err)}`);
24372
+ return { success: false, error: String(err) };
24373
+ }
24374
+ }
24287
24375
 
24288
24376
  // app/lib/whatsapp/inbound/media.ts
24289
24377
  import { randomUUID as randomUUID6 } from "crypto";
@@ -24667,11 +24755,11 @@ async function connectWithReconnect(conn) {
24667
24755
  }
24668
24756
  const delay = computeBackoff(conn.reconnectAttempts);
24669
24757
  console.error(`${TAG8} reconnecting account=${conn.accountId} in ${delay}ms (attempt ${conn.reconnectAttempts}/${maxAttempts})`);
24670
- await new Promise((resolve17) => {
24671
- const timer = setTimeout(resolve17, delay);
24758
+ await new Promise((resolve18) => {
24759
+ const timer = setTimeout(resolve18, delay);
24672
24760
  conn.abortController.signal.addEventListener("abort", () => {
24673
24761
  clearTimeout(timer);
24674
- resolve17();
24762
+ resolve18();
24675
24763
  }, { once: true });
24676
24764
  });
24677
24765
  if (conn.abortController.signal.aborted) return;
@@ -24679,14 +24767,14 @@ async function connectWithReconnect(conn) {
24679
24767
  }
24680
24768
  }
24681
24769
  function waitForDisconnectEvent(conn) {
24682
- return new Promise((resolve17) => {
24770
+ return new Promise((resolve18) => {
24683
24771
  if (!conn.sock) {
24684
- resolve17();
24772
+ resolve18();
24685
24773
  return;
24686
24774
  }
24687
24775
  conn.sock.ev.on("connection.update", (update) => {
24688
24776
  if (update.connection === "close") {
24689
- resolve17();
24777
+ resolve18();
24690
24778
  }
24691
24779
  });
24692
24780
  });
@@ -24978,45 +25066,56 @@ async function POST13(req) {
24978
25066
  }
24979
25067
 
24980
25068
  // app/lib/whatsapp/schema-serialize.ts
25069
+ function zodTypeName(def) {
25070
+ if (typeof def?.type === "string") return def.type;
25071
+ if (typeof def?.typeName === "string") return def.typeName;
25072
+ return "unknown";
25073
+ }
25074
+ function isType(name, ...candidates) {
25075
+ return candidates.includes(name);
25076
+ }
24981
25077
  function resolveZodType(def) {
24982
- const typeName = def?.typeName ?? "unknown";
24983
- if (typeName === "ZodDefault") {
25078
+ const tn = zodTypeName(def);
25079
+ if (isType(tn, "ZodDefault", "default")) {
24984
25080
  const inner = resolveZodType(def.innerType?._def);
24985
25081
  const defaultVal = typeof def.defaultValue === "function" ? def.defaultValue() : def.defaultValue;
24986
25082
  return { ...inner, default: JSON.stringify(defaultVal) };
24987
25083
  }
24988
- if (typeName === "ZodOptional") {
25084
+ if (isType(tn, "ZodOptional", "optional")) {
24989
25085
  const inner = resolveZodType(def.innerType?._def);
24990
25086
  return { ...inner, required: false };
24991
25087
  }
24992
- if (typeName === "ZodString") return { type: "string", required: true };
24993
- if (typeName === "ZodNumber") return { type: "number", required: true };
24994
- if (typeName === "ZodBoolean") return { type: "boolean", required: true };
24995
- if (typeName === "ZodEnum") {
25088
+ if (isType(tn, "ZodString", "string")) return { type: "string", required: true };
25089
+ if (isType(tn, "ZodNumber", "number")) return { type: "number", required: true };
25090
+ if (isType(tn, "ZodBoolean", "boolean")) return { type: "boolean", required: true };
25091
+ if (isType(tn, "ZodEnum", "enum")) {
24996
25092
  const values = def.values ?? [];
24997
25093
  return { type: `enum(${values.join(", ")})`, required: true };
24998
25094
  }
24999
- if (typeName === "ZodArray") {
25000
- const inner = resolveZodType(def.type?._def);
25095
+ if (isType(tn, "ZodArray", "array")) {
25096
+ const elementDef = def.element?._def ?? def.type?._def;
25097
+ const inner = resolveZodType(elementDef);
25001
25098
  return { type: `${inner.type}[]`, required: true };
25002
25099
  }
25003
- if (typeName === "ZodRecord") {
25100
+ if (isType(tn, "ZodRecord", "record")) {
25004
25101
  return { type: "Record<string, ...>", required: true };
25005
25102
  }
25006
- if (typeName === "ZodObject") {
25103
+ if (isType(tn, "ZodObject", "object")) {
25007
25104
  return { type: "object", required: true };
25008
25105
  }
25009
25106
  return { type: "unknown", required: true };
25010
25107
  }
25011
- function extractDescription(def) {
25012
- if (!def) return void 0;
25013
- if (typeof def.description === "string") return def.description;
25014
- if (def.innerType?._def) return extractDescription(def.innerType._def);
25108
+ function extractDescription(schema, def) {
25109
+ if (typeof schema?.description === "string") return schema.description;
25110
+ const d = def ?? schema?._def ?? schema;
25111
+ if (typeof d?.description === "string") return d.description;
25112
+ if (d?.innerType) return extractDescription(d.innerType, d.innerType?._def);
25015
25113
  return void 0;
25016
25114
  }
25017
25115
  function unwrapToShape(schema) {
25018
25116
  if (schema?.shape && typeof schema.shape === "object") return schema.shape;
25019
- if (schema?._def?.typeName === "ZodEffects" && schema._def.schema) {
25117
+ const tn = zodTypeName(schema?._def);
25118
+ if (isType(tn, "ZodEffects", "effects") && schema._def.schema) {
25020
25119
  return unwrapToShape(schema._def.schema);
25021
25120
  }
25022
25121
  if (typeof schema?._def?.shape === "function") return schema._def.shape();
@@ -25028,7 +25127,7 @@ function extractFields(schema) {
25028
25127
  for (const [name, fieldSchema] of Object.entries(shape)) {
25029
25128
  const def = fieldSchema?._def;
25030
25129
  const resolved = resolveZodType(def);
25031
- const description = extractDescription(def);
25130
+ const description = extractDescription(fieldSchema, def);
25032
25131
  fields.push({
25033
25132
  name,
25034
25133
  type: resolved.type,
@@ -25162,6 +25261,107 @@ async function POST14(req) {
25162
25261
  }
25163
25262
  }
25164
25263
 
25264
+ // app/api/whatsapp/send-document/route.ts
25265
+ import { readFile as readFile2, stat as stat2 } from "fs/promises";
25266
+ import { realpathSync as realpathSync2 } from "fs";
25267
+ import { resolve as resolve10, basename as basename2 } from "path";
25268
+ var TAG9 = "[whatsapp:api]";
25269
+ var PLATFORM_ROOT6 = process.env.MAXY_PLATFORM_ROOT || "";
25270
+ async function POST15(req) {
25271
+ try {
25272
+ const body = await req.json();
25273
+ const { to, filePath, caption, maxyAccountId } = body;
25274
+ const accountId = validateAccountId(body.accountId);
25275
+ if (!to || !filePath) {
25276
+ return Response.json({ error: "Missing required fields: to, filePath" }, { status: 400 });
25277
+ }
25278
+ if (!maxyAccountId || !PLATFORM_ROOT6) {
25279
+ return Response.json(
25280
+ { error: "Cannot validate file path: missing account or platform context" },
25281
+ { status: 400 }
25282
+ );
25283
+ }
25284
+ const accountDir = resolve10(PLATFORM_ROOT6, "config/accounts", maxyAccountId);
25285
+ let resolvedPath;
25286
+ try {
25287
+ resolvedPath = realpathSync2(filePath);
25288
+ const accountResolved = realpathSync2(accountDir);
25289
+ if (!resolvedPath.startsWith(accountResolved + "/")) {
25290
+ const sanitised = filePath.replace(accountDir, "<account>/");
25291
+ console.error(`${TAG9} send-document REJECTED path=${sanitised} reason=outside_account_directory`);
25292
+ return Response.json({ error: "Access denied: file is outside the account directory" }, { status: 403 });
25293
+ }
25294
+ } catch (err) {
25295
+ const code = err.code;
25296
+ if (code === "ENOENT") {
25297
+ console.error(`${TAG9} send-document ENOENT path=${filePath}`);
25298
+ return Response.json({ error: `File not found: ${filePath}` }, { status: 404 });
25299
+ }
25300
+ console.error(`${TAG9} send-document path error: ${String(err)}`);
25301
+ return Response.json({ error: String(err) }, { status: 500 });
25302
+ }
25303
+ const fileStat = await stat2(resolvedPath);
25304
+ if (fileStat.size > MAX_FILE_SIZE_BYTES) {
25305
+ return Response.json(
25306
+ { error: `File exceeds 20 MB limit (${(fileStat.size / 1024 / 1024).toFixed(1)} MB)` },
25307
+ { status: 400 }
25308
+ );
25309
+ }
25310
+ const buffer = Buffer.from(await readFile2(resolvedPath));
25311
+ const filename = basename2(resolvedPath);
25312
+ const mimetype = detectMimeType(resolvedPath);
25313
+ const sock = getSocket(accountId);
25314
+ if (!sock) {
25315
+ return Response.json(
25316
+ { error: `WhatsApp account "${accountId}" is not connected` },
25317
+ { status: 503 }
25318
+ );
25319
+ }
25320
+ const result = await sendMediaMessage(sock, to, {
25321
+ type: "document",
25322
+ buffer,
25323
+ mimetype,
25324
+ filename,
25325
+ caption
25326
+ });
25327
+ console.error(
25328
+ `${TAG9} send-document to=${to} size=${fileStat.size} mime=${mimetype} ok=${result.success}` + (result.messageId ? ` id=${result.messageId}` : "")
25329
+ );
25330
+ return Response.json(result);
25331
+ } catch (err) {
25332
+ console.error(`${TAG9} send-document error: ${String(err)}`);
25333
+ return Response.json({ error: String(err) }, { status: 500 });
25334
+ }
25335
+ }
25336
+
25337
+ // app/api/admin/file-attach/route.ts
25338
+ async function POST16(req) {
25339
+ try {
25340
+ const body = await req.json();
25341
+ const { filePath, accountId } = body;
25342
+ if (!filePath || !accountId) {
25343
+ return Response.json(
25344
+ { error: "Missing required fields: filePath, accountId" },
25345
+ { status: 400 }
25346
+ );
25347
+ }
25348
+ const attachment = await storeGeneratedFile(accountId, filePath);
25349
+ console.error(
25350
+ `[admin:file-attach] stored path=${filePath} id=${attachment.attachmentId} size=${attachment.sizeBytes} mime=${attachment.mimeType}`
25351
+ );
25352
+ return Response.json({
25353
+ attachmentId: attachment.attachmentId,
25354
+ filename: attachment.filename,
25355
+ sizeBytes: attachment.sizeBytes,
25356
+ mimeType: attachment.mimeType
25357
+ });
25358
+ } catch (err) {
25359
+ const message = err instanceof Error ? err.message : String(err);
25360
+ console.error(`[admin:file-attach] error: ${message}`);
25361
+ return Response.json({ error: message }, { status: 500 });
25362
+ }
25363
+ }
25364
+
25165
25365
  // app/api/onboarding/claude-auth/route.ts
25166
25366
  import { spawn as spawn3, execFileSync as execFileSync2 } from "child_process";
25167
25367
  import { openSync, closeSync, writeFileSync as writeFileSync7, writeSync } from "fs";
@@ -25192,7 +25392,7 @@ async function waitForAuthPage(timeoutMs = 2e4) {
25192
25392
  async function GET3() {
25193
25393
  return Response.json({ authenticated: checkAuthStatus() });
25194
25394
  }
25195
- async function POST15(req) {
25395
+ async function POST17(req) {
25196
25396
  let body = {};
25197
25397
  try {
25198
25398
  body = await req.json();
@@ -25249,7 +25449,7 @@ import { createHash } from "crypto";
25249
25449
  function hashPin(pin) {
25250
25450
  return createHash("sha256").update(pin).digest("hex");
25251
25451
  }
25252
- async function POST16(req) {
25452
+ async function POST18(req) {
25253
25453
  if (existsSync11(PIN_FILE)) {
25254
25454
  return Response.json(
25255
25455
  { error: "PIN is already configured." },
@@ -25307,7 +25507,7 @@ function getStoredPinHash() {
25307
25507
  }
25308
25508
  return null;
25309
25509
  }
25310
- async function POST17(req) {
25510
+ async function POST19(req) {
25311
25511
  const storedHash = getStoredPinHash();
25312
25512
  if (!storedHash) {
25313
25513
  return Response.json(
@@ -25428,7 +25628,7 @@ function advanceGateIfNeeded(envelope, accountDir, logFn) {
25428
25628
  const pending = Object.entries(gates).filter(([, v]) => !v).map(([k]) => k);
25429
25629
  logFn(`${gateFlag} gate advanced (route-level). Pending: ${pending.length > 0 ? pending.join(", ") : "none"}`);
25430
25630
  }
25431
- async function POST18(req) {
25631
+ async function POST20(req) {
25432
25632
  const contentType = req.headers.get("content-type") ?? "";
25433
25633
  let message;
25434
25634
  let session_key;
@@ -25650,7 +25850,7 @@ async function POST18(req) {
25650
25850
  }
25651
25851
 
25652
25852
  // app/api/admin/compact/route.ts
25653
- async function POST19(req) {
25853
+ async function POST21(req) {
25654
25854
  let body;
25655
25855
  try {
25656
25856
  body = await req.json();
@@ -25692,7 +25892,7 @@ async function POST19(req) {
25692
25892
 
25693
25893
  // app/api/admin/logs/route.ts
25694
25894
  import { existsSync as existsSync14, readdirSync as readdirSync2, readFileSync as readFileSync14, statSync as statSync3 } from "fs";
25695
- import { resolve as resolve10, basename } from "path";
25895
+ import { resolve as resolve11, basename as basename3 } from "path";
25696
25896
  var TAIL_BYTES = 8192;
25697
25897
  async function GET4(request) {
25698
25898
  const { searchParams } = new URL(request.url);
@@ -25700,12 +25900,12 @@ async function GET4(request) {
25700
25900
  const typeParam = searchParams.get("type");
25701
25901
  const download = searchParams.get("download") === "1";
25702
25902
  const account = resolveAccount();
25703
- const accountLogDir = account ? resolve10(account.accountDir, "logs") : null;
25903
+ const accountLogDir = account ? resolve11(account.accountDir, "logs") : null;
25704
25904
  if (fileParam) {
25705
- const safe = basename(fileParam);
25905
+ const safe = basename3(fileParam);
25706
25906
  for (const dir of [accountLogDir, LOG_DIR]) {
25707
25907
  if (!dir) continue;
25708
- const filePath = resolve10(dir, safe);
25908
+ const filePath = resolve11(dir, safe);
25709
25909
  try {
25710
25910
  const content = readFileSync14(filePath, "utf-8");
25711
25911
  const headers = { "Content-Type": "text/plain; charset=utf-8" };
@@ -25731,7 +25931,7 @@ async function GET4(request) {
25731
25931
  }
25732
25932
  for (const dir of [accountLogDir, LOG_DIR]) {
25733
25933
  if (!dir) continue;
25734
- const filePath = resolve10(dir, fileName);
25934
+ const filePath = resolve11(dir, fileName);
25735
25935
  try {
25736
25936
  const content = readFileSync14(filePath, "utf-8");
25737
25937
  const headers = { "Content-Type": "text/plain; charset=utf-8" };
@@ -25752,10 +25952,10 @@ async function GET4(request) {
25752
25952
  } catch {
25753
25953
  continue;
25754
25954
  }
25755
- files.filter((f) => !seen.has(f)).map((f) => ({ name: f, mtime: statSync3(resolve10(dir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime).forEach(({ name }) => {
25955
+ files.filter((f) => !seen.has(f)).map((f) => ({ name: f, mtime: statSync3(resolve11(dir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime).forEach(({ name }) => {
25756
25956
  seen.add(name);
25757
25957
  try {
25758
- const content = readFileSync14(resolve10(dir, name));
25958
+ const content = readFileSync14(resolve11(dir, name));
25759
25959
  const tail = content.length > TAIL_BYTES ? content.subarray(content.length - TAIL_BYTES).toString("utf-8") : content.toString("utf-8");
25760
25960
  logs[name] = tail.trim() || "(empty)";
25761
25961
  } catch {
@@ -25789,9 +25989,9 @@ async function GET5() {
25789
25989
  }
25790
25990
 
25791
25991
  // app/api/admin/attachment/[attachmentId]/route.ts
25792
- import { readFile, readdir } from "fs/promises";
25992
+ import { readFile as readFile3, readdir } from "fs/promises";
25793
25993
  import { existsSync as existsSync15 } from "fs";
25794
- import { resolve as resolve11 } from "path";
25994
+ import { resolve as resolve12 } from "path";
25795
25995
  async function GET6(req, attachmentId) {
25796
25996
  const sessionKey = new URL(req.url).searchParams.get("session_key") ?? "";
25797
25997
  if (!validateSession(sessionKey, "admin")) {
@@ -25804,17 +26004,17 @@ async function GET6(req, attachmentId) {
25804
26004
  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(attachmentId)) {
25805
26005
  return new Response("Not found", { status: 404 });
25806
26006
  }
25807
- const dir = resolve11(ATTACHMENTS_ROOT, accountId, attachmentId);
26007
+ const dir = resolve12(ATTACHMENTS_ROOT, accountId, attachmentId);
25808
26008
  if (!existsSync15(dir)) {
25809
26009
  return new Response("Not found", { status: 404 });
25810
26010
  }
25811
- const metaPath = resolve11(dir, `${attachmentId}.meta.json`);
26011
+ const metaPath = resolve12(dir, `${attachmentId}.meta.json`);
25812
26012
  if (!existsSync15(metaPath)) {
25813
26013
  return new Response("Not found", { status: 404 });
25814
26014
  }
25815
26015
  let meta3;
25816
26016
  try {
25817
- meta3 = JSON.parse(await readFile(metaPath, "utf-8"));
26017
+ meta3 = JSON.parse(await readFile3(metaPath, "utf-8"));
25818
26018
  } catch {
25819
26019
  return new Response("Not found", { status: 404 });
25820
26020
  }
@@ -25823,8 +26023,8 @@ async function GET6(req, attachmentId) {
25823
26023
  if (!dataFile) {
25824
26024
  return new Response("Not found", { status: 404 });
25825
26025
  }
25826
- const filePath = resolve11(dir, dataFile);
25827
- const buffer = await readFile(filePath);
26026
+ const filePath = resolve12(dir, dataFile);
26027
+ const buffer = await readFile3(filePath);
25828
26028
  return new Response(buffer, {
25829
26029
  headers: {
25830
26030
  "Content-Type": meta3.mimeType,
@@ -25836,7 +26036,7 @@ async function GET6(req, attachmentId) {
25836
26036
 
25837
26037
  // app/api/admin/account/route.ts
25838
26038
  import { readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "fs";
25839
- import { resolve as resolve12 } from "path";
26039
+ import { resolve as resolve13 } from "path";
25840
26040
  var VALID_CONTEXT_MODES = ["managed", "claude-code"];
25841
26041
  async function PATCH(req) {
25842
26042
  let body;
@@ -25859,7 +26059,7 @@ async function PATCH(req) {
25859
26059
  if (!account) {
25860
26060
  return Response.json({ error: "No account configured" }, { status: 500 });
25861
26061
  }
25862
- const configPath2 = resolve12(account.accountDir, "account.json");
26062
+ const configPath2 = resolve13(account.accountDir, "account.json");
25863
26063
  try {
25864
26064
  const raw2 = readFileSync15(configPath2, "utf-8");
25865
26065
  const config2 = JSON.parse(raw2);
@@ -25874,14 +26074,14 @@ async function PATCH(req) {
25874
26074
  }
25875
26075
 
25876
26076
  // app/api/admin/agents/route.ts
25877
- import { resolve as resolve13 } from "path";
26077
+ import { resolve as resolve14 } from "path";
25878
26078
  import { readdirSync as readdirSync3, readFileSync as readFileSync16, existsSync as existsSync16 } from "fs";
25879
26079
  async function GET7() {
25880
26080
  const account = resolveAccount();
25881
26081
  if (!account) {
25882
26082
  return Response.json({ agents: [] });
25883
26083
  }
25884
- const agentsDir = resolve13(account.accountDir, "agents");
26084
+ const agentsDir = resolve14(account.accountDir, "agents");
25885
26085
  if (!existsSync16(agentsDir)) {
25886
26086
  return Response.json({ agents: [] });
25887
26087
  }
@@ -25891,7 +26091,7 @@ async function GET7() {
25891
26091
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
25892
26092
  if (!entry.isDirectory()) continue;
25893
26093
  if (entry.name === "admin") continue;
25894
- const configPath2 = resolve13(agentsDir, entry.name, "config.json");
26094
+ const configPath2 = resolve14(agentsDir, entry.name, "config.json");
25895
26095
  if (!existsSync16(configPath2)) continue;
25896
26096
  try {
25897
26097
  const config2 = JSON.parse(readFileSync16(configPath2, "utf-8"));
@@ -25913,11 +26113,11 @@ async function GET7() {
25913
26113
 
25914
26114
  // app/api/admin/version/route.ts
25915
26115
  import { readFileSync as readFileSync17, existsSync as existsSync17 } from "fs";
25916
- import { resolve as resolve14, join as join7 } from "path";
25917
- var PLATFORM_ROOT6 = process.env.MAXY_PLATFORM_ROOT ?? resolve14(process.cwd(), "..");
26116
+ import { resolve as resolve15, join as join7 } from "path";
26117
+ var PLATFORM_ROOT7 = process.env.MAXY_PLATFORM_ROOT ?? resolve15(process.cwd(), "..");
25918
26118
  var brandHostname = "maxy";
25919
26119
  var brandNpmPackage = "@rubytech/create-maxy";
25920
- var brandJsonPath = join7(PLATFORM_ROOT6, "config", "brand.json");
26120
+ var brandJsonPath = join7(PLATFORM_ROOT7, "config", "brand.json");
25921
26121
  if (existsSync17(brandJsonPath)) {
25922
26122
  try {
25923
26123
  const brand = JSON.parse(readFileSync17(brandJsonPath, "utf-8"));
@@ -25926,7 +26126,7 @@ if (existsSync17(brandJsonPath)) {
25926
26126
  } catch {
25927
26127
  }
25928
26128
  }
25929
- var VERSION_FILE = resolve14(PLATFORM_ROOT6, `config/.${brandHostname}-version`);
26129
+ var VERSION_FILE = resolve15(PLATFORM_ROOT7, `config/.${brandHostname}-version`);
25930
26130
  var NPM_PACKAGE = brandNpmPackage;
25931
26131
  var REGISTRY_URL = `https://registry.npmjs.org/${NPM_PACKAGE}/latest`;
25932
26132
  var CACHE_TTL_MS = 60 * 60 * 1e3;
@@ -25989,11 +26189,11 @@ async function GET8() {
25989
26189
  // app/api/admin/version/upgrade/route.ts
25990
26190
  import { spawn as spawn4 } from "child_process";
25991
26191
  import { existsSync as existsSync18, statSync as statSync4, writeFileSync as writeFileSync11, readFileSync as readFileSync18, openSync as openSync2, closeSync as closeSync2 } from "fs";
25992
- import { resolve as resolve15, join as join8 } from "path";
25993
- var PLATFORM_ROOT7 = process.env.MAXY_PLATFORM_ROOT ?? resolve15(process.cwd(), "..");
26192
+ import { resolve as resolve16, join as join8 } from "path";
26193
+ var PLATFORM_ROOT8 = process.env.MAXY_PLATFORM_ROOT ?? resolve16(process.cwd(), "..");
25994
26194
  var upgradePkg = "@rubytech/create-maxy";
25995
26195
  var upgradeHostname = "maxy";
25996
- var brandPath = join8(PLATFORM_ROOT7, "config", "brand.json");
26196
+ var brandPath = join8(PLATFORM_ROOT8, "config", "brand.json");
25997
26197
  if (existsSync18(brandPath)) {
25998
26198
  try {
25999
26199
  const brand = JSON.parse(readFileSync18(brandPath, "utf-8"));
@@ -26008,13 +26208,13 @@ var LOCK_MAX_AGE_MS = 3 * 60 * 1e3;
26008
26208
  function isLockFresh() {
26009
26209
  if (!existsSync18(LOCK_FILE)) return false;
26010
26210
  try {
26011
- const stat = statSync4(LOCK_FILE);
26012
- return Date.now() - stat.mtimeMs < LOCK_MAX_AGE_MS;
26211
+ const stat3 = statSync4(LOCK_FILE);
26212
+ return Date.now() - stat3.mtimeMs < LOCK_MAX_AGE_MS;
26013
26213
  } catch {
26014
26214
  return false;
26015
26215
  }
26016
26216
  }
26017
- async function POST20(req) {
26217
+ async function POST22(req) {
26018
26218
  let body;
26019
26219
  try {
26020
26220
  body = await req.json();
@@ -26043,7 +26243,7 @@ async function POST20(req) {
26043
26243
  detached: true,
26044
26244
  stdio: ["ignore", logFd, logFd],
26045
26245
  env: { ...process.env, npm_config_yes: "true" },
26046
- cwd: resolve15(process.cwd(), "..")
26246
+ cwd: resolve16(process.cwd(), "..")
26047
26247
  });
26048
26248
  child.unref();
26049
26249
  closeSync2(logFd);
@@ -26138,7 +26338,7 @@ async function GET10(req, { params }) {
26138
26338
  }
26139
26339
 
26140
26340
  // app/api/admin/browser/launch/route.ts
26141
- async function POST21() {
26341
+ async function POST23() {
26142
26342
  try {
26143
26343
  const vncOk = await ensureVnc();
26144
26344
  if (!vncOk) {
@@ -26159,8 +26359,8 @@ async function POST21() {
26159
26359
  }
26160
26360
 
26161
26361
  // server/index.ts
26162
- var PLATFORM_ROOT8 = process.env.MAXY_PLATFORM_ROOT || "";
26163
- var BRAND_JSON_PATH = PLATFORM_ROOT8 ? join9(PLATFORM_ROOT8, "config", "brand.json") : "";
26362
+ var PLATFORM_ROOT9 = process.env.MAXY_PLATFORM_ROOT || "";
26363
+ var BRAND_JSON_PATH = PLATFORM_ROOT9 ? join9(PLATFORM_ROOT9, "config", "brand.json") : "";
26164
26364
  var BRAND = { productName: "Maxy", hostname: "maxy", configDir: ".maxy", domain: "getmaxy.com" };
26165
26365
  if (BRAND_JSON_PATH && !existsSync19(BRAND_JSON_PATH)) {
26166
26366
  console.error(`[brand] WARNING: brand.json not found at ${BRAND_JSON_PATH} \u2014 using Maxy defaults`);
@@ -26491,13 +26691,14 @@ app.post("/api/whatsapp/disconnect", (c) => POST11(c.req.raw));
26491
26691
  app.post("/api/whatsapp/reconnect", (c) => POST12(c.req.raw));
26492
26692
  app.post("/api/whatsapp/send", (c) => POST13(c.req.raw));
26493
26693
  app.post("/api/whatsapp/config", (c) => POST14(c.req.raw));
26694
+ app.post("/api/whatsapp/send-document", (c) => POST15(c.req.raw));
26494
26695
  app.get("/api/onboarding/claude-auth", () => GET3());
26495
- app.post("/api/onboarding/claude-auth", (c) => POST15(c.req.raw));
26496
- app.post("/api/onboarding/set-pin", (c) => POST16(c.req.raw));
26696
+ app.post("/api/onboarding/claude-auth", (c) => POST17(c.req.raw));
26697
+ app.post("/api/onboarding/set-pin", (c) => POST18(c.req.raw));
26497
26698
  app.delete("/api/onboarding/set-pin", (c) => DELETE(c.req.raw));
26498
- app.post("/api/admin/session", (c) => POST17(c.req.raw));
26499
- app.post("/api/admin/chat", (c) => POST18(c.req.raw));
26500
- app.post("/api/admin/compact", (c) => POST19(c.req.raw));
26699
+ app.post("/api/admin/session", (c) => POST19(c.req.raw));
26700
+ app.post("/api/admin/chat", (c) => POST20(c.req.raw));
26701
+ app.post("/api/admin/compact", (c) => POST21(c.req.raw));
26501
26702
  app.get("/api/admin/logs", (c) => GET4(c.req.raw));
26502
26703
  app.get("/api/admin/claude-info", () => GET5());
26503
26704
  app.patch("/api/admin/account", (c) => PATCH(c.req.raw));
@@ -26505,10 +26706,11 @@ app.get(
26505
26706
  "/api/admin/attachment/:attachmentId",
26506
26707
  (c) => GET6(c.req.raw, c.req.param("attachmentId"))
26507
26708
  );
26709
+ app.post("/api/admin/file-attach", (c) => POST16(c.req.raw));
26508
26710
  app.get("/api/admin/version", () => GET8());
26509
- app.post("/api/admin/version/upgrade", (c) => POST20(c.req.raw));
26711
+ app.post("/api/admin/version/upgrade", (c) => POST22(c.req.raw));
26510
26712
  app.get("/api/admin/agents", () => GET7());
26511
- app.post("/api/admin/browser/launch", () => POST21());
26713
+ app.post("/api/admin/browser/launch", () => POST23());
26512
26714
  app.get("/api/admin/sessions", (c) => GET9(c.req.raw));
26513
26715
  app.delete(
26514
26716
  "/api/admin/sessions/:id",
@@ -26540,7 +26742,7 @@ var brandScript = `<script>window.__BRAND__=${JSON.stringify({
26540
26742
  function cachedHtml(file2) {
26541
26743
  let html = htmlCache.get(file2);
26542
26744
  if (!html) {
26543
- html = readFileSync19(resolve16(process.cwd(), "public", file2), "utf-8");
26745
+ html = readFileSync19(resolve17(process.cwd(), "public", file2), "utf-8");
26544
26746
  html = html.replace("</head>", `${brandScript}
26545
26747
  </head>`);
26546
26748
  htmlCache.set(file2, html);
@@ -26634,7 +26836,7 @@ var port = parseInt(process.env.PORT ?? "19200", 10);
26634
26836
  var hostname3 = process.env.HOSTNAME ?? "0.0.0.0";
26635
26837
  serve({ fetch: app.fetch, port, hostname: hostname3 });
26636
26838
  console.log(`${BRAND.productName} listening on http://${hostname3}:${port}`);
26637
- var configDirForWhatsApp = basename2(MAXY_DIR) || ".maxy";
26839
+ var configDirForWhatsApp = basename4(MAXY_DIR) || ".maxy";
26638
26840
  var bootAccount = resolveAccount();
26639
26841
  var bootAccountConfig = bootAccount?.config;
26640
26842
  var bootPublicAgent = bootAccount ? getPublicAgent(bootAccount.accountDir) : null;
@@ -26645,7 +26847,7 @@ if (bootAccountConfig?.whatsapp) {
26645
26847
  }
26646
26848
  init({
26647
26849
  configDir: configDirForWhatsApp,
26648
- platformRoot: resolve16(process.env.MAXY_PLATFORM_ROOT ?? join9(__dirname, "..")),
26850
+ platformRoot: resolve17(process.env.MAXY_PLATFORM_ROOT ?? join9(__dirname, "..")),
26649
26851
  accountConfig: bootAccountConfig,
26650
26852
  onMessage: async (msg) => {
26651
26853
  try {