@rubytech/create-maxy-code 0.1.281 → 0.1.282

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 (42) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/admin/PLUGIN.md +2 -1
  3. package/payload/platform/plugins/admin/hooks/__tests__/prompt-optimiser-compliance.test.sh +154 -0
  4. package/payload/platform/plugins/admin/hooks/__tests__/prompt-optimiser-directive.test.sh +24 -0
  5. package/payload/platform/plugins/admin/hooks/prompt-optimiser-compliance.sh +174 -0
  6. package/payload/platform/plugins/admin/hooks/prompt-optimiser-directive.sh +22 -2
  7. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +2 -2
  8. package/payload/platform/plugins/business-assistant/skills/e-sign/SKILL.md +378 -56
  9. package/payload/platform/plugins/cloudflare/references/api.md +35 -1
  10. package/payload/platform/plugins/cloudflare/references/d1-data-capture.md +2 -0
  11. package/payload/platform/plugins/docs/references/cloudflare.md +1 -1
  12. package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-resolve.test.js +25 -1
  13. package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-resolve.test.js.map +1 -1
  14. package/payload/platform/plugins/email/mcp/dist/lib/attachment-resolve.d.ts +7 -0
  15. package/payload/platform/plugins/email/mcp/dist/lib/attachment-resolve.d.ts.map +1 -1
  16. package/payload/platform/plugins/email/mcp/dist/lib/attachment-resolve.js +9 -0
  17. package/payload/platform/plugins/email/mcp/dist/lib/attachment-resolve.js.map +1 -1
  18. package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.d.ts.map +1 -1
  19. package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.js +6 -2
  20. package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.js.map +1 -1
  21. package/payload/platform/plugins/prompt-optimiser/skills/prompt-optimiser/SKILL.md +1 -0
  22. package/payload/platform/plugins/whatsapp/.claude-plugin/plugin.json +1 -1
  23. package/payload/platform/plugins/whatsapp/PLUGIN.md +7 -11
  24. package/payload/platform/plugins/whatsapp/mcp/dist/index.js +10 -56
  25. package/payload/platform/plugins/whatsapp/mcp/dist/index.js.map +1 -1
  26. package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +26 -2
  27. package/payload/platform/plugins/whatsapp/skills/manage-whatsapp-config/SKILL.md +9 -14
  28. package/payload/platform/scripts/setup-account.sh +7 -0
  29. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  30. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +0 -2
  31. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  32. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  33. package/payload/platform/services/claude-session-manager/dist/http-server.js +63 -3
  34. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  35. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +6 -0
  36. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
  37. package/payload/platform/services/whatsapp-channel/dist/notification.js +14 -0
  38. package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
  39. package/payload/platform/services/whatsapp-channel/dist/server.js +5 -3
  40. package/payload/platform/services/whatsapp-channel/dist/server.js.map +1 -1
  41. package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
  42. package/payload/server/server.js +55 -236
@@ -1513,24 +1513,14 @@ function getLanIp() {
1513
1513
  // app/lib/whatsapp/schema.ts
1514
1514
  import { z } from "zod";
1515
1515
  var DmPolicySchema = z.enum(["open", "allowlist", "disabled"]);
1516
- var GroupPolicySchema = z.enum(["open", "allowlist", "disabled"]);
1517
1516
  var WhatsAppAccountSchema = z.object({
1518
1517
  name: z.string().optional().describe("Display name for this WhatsApp account (e.g. 'Main', 'Sales')."),
1519
1518
  enabled: z.boolean().optional().describe("If disabled, this account won't connect on startup."),
1520
1519
  dmPolicy: DmPolicySchema.optional().describe("Who can message your public agent via WhatsApp DM. 'Open' lets anyone message. 'Allowlist' restricts to approved phone numbers. 'Disabled' blocks all public messages."),
1521
- groupPolicy: GroupPolicySchema.optional().describe("Whether your agent responds in WhatsApp group chats. 'Open' enables group responses (subject to per-group activation). 'Allowlist' restricts to approved senders. 'Disabled' ignores all group messages."),
1522
1520
  selfChatMode: z.boolean().optional().describe("Enable if the bot uses the owner's personal WhatsApp number (same phone for admin and bot). Changes how self-messages are routed."),
1523
1521
  adminPhones: z.array(z.string()).optional().describe("Phone numbers (E.164) that route to the admin agent. Managed via add-admin-phone / remove-admin-phone."),
1524
1522
  allowFrom: z.array(z.string()).optional().describe("Phone numbers allowed to message the public agent when DM policy is 'allowlist'. Add '*' for open access. Does not affect admin routing \u2014 use adminPhones for that."),
1525
- groupAllowFrom: z.array(z.string()).optional().describe("Phone numbers allowed to trigger the agent in groups when group policy is 'allowlist'."),
1526
1523
  sendReadReceipts: z.boolean().optional().default(true).describe("Whether to send read receipts (blue ticks) when messages are received. Disabling may feel less responsive but preserves privacy."),
1527
- groups: z.record(
1528
- z.string(),
1529
- z.object({
1530
- activation: z.enum(["always", "mention", "off"]).optional(),
1531
- 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.")
1532
- }).strict().optional()
1533
- ).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)."),
1534
1524
  ackReaction: z.object({
1535
1525
  emoji: z.string().optional(),
1536
1526
  direct: z.boolean().optional().default(true),
@@ -1552,10 +1542,8 @@ var WhatsAppAccountSchema = z.object({
1552
1542
  var WhatsAppConfigSchema = z.object({
1553
1543
  accounts: z.record(z.string(), WhatsAppAccountSchema.optional()).optional().describe("Per-account config keyed by account ID."),
1554
1544
  dmPolicy: DmPolicySchema.optional().default("disabled").describe("Who can message your public agent via WhatsApp DM. 'Open' lets anyone message. 'Allowlist' restricts to approved phone numbers. 'Disabled' blocks all public messages."),
1555
- groupPolicy: GroupPolicySchema.optional().default("disabled").describe("Whether your agent responds in WhatsApp group chats. 'Open' enables group responses (subject to per-group activation). 'Allowlist' restricts to approved senders. 'Disabled' ignores all group messages."),
1556
1545
  adminPhones: z.array(z.string()).optional().describe("Phone numbers (E.164) that route to the admin agent. Managed via add-admin-phone / remove-admin-phone."),
1557
1546
  allowFrom: z.array(z.string()).optional().describe("Phone numbers allowed to message the public agent when DM policy is 'allowlist'. Add '*' for open access. Does not affect admin routing \u2014 use adminPhones for that."),
1558
- groupAllowFrom: z.array(z.string()).optional().describe("Phone numbers allowed to trigger the agent in groups when group policy is 'allowlist'."),
1559
1547
  sendReadReceipts: z.boolean().optional().default(true).describe("Whether to send read receipts (blue ticks) when messages are received. Disabling may feel less responsive but preserves privacy."),
1560
1548
  mediaMaxMb: z.number().int().positive().optional().default(50).describe("Maximum file size in MB for media the agent will download and process."),
1561
1549
  textChunkLimit: z.number().int().positive().optional().default(4e3).describe("Maximum characters per outbound message. Longer replies are split into multiple messages."),
@@ -2321,10 +2309,8 @@ function extractMessage(msg) {
2321
2309
  function resolveAccountConfig(config, accountConfig) {
2322
2310
  return {
2323
2311
  dmPolicy: accountConfig?.dmPolicy ?? config.dmPolicy ?? "disabled",
2324
- groupPolicy: accountConfig?.groupPolicy ?? config.groupPolicy ?? "disabled",
2325
2312
  adminPhones: accountConfig?.adminPhones ?? config.adminPhones ?? [],
2326
2313
  allowFrom: accountConfig?.allowFrom ?? config.allowFrom ?? [],
2327
- groupAllowFrom: accountConfig?.groupAllowFrom ?? config.groupAllowFrom ?? [],
2328
2314
  selfChatMode: accountConfig?.selfChatMode ?? false
2329
2315
  };
2330
2316
  }
@@ -2361,36 +2347,8 @@ function checkDmAccess(params) {
2361
2347
  return { allowed: false, reason: "unknown-dm-policy", agentType: "public" };
2362
2348
  }
2363
2349
  }
2364
- function checkGroupAccess(params) {
2365
- const { senderPhone, selfPhone, groupJid, isMentioned } = params;
2366
- const cfg = resolveAccountConfig(params.config, params.accountConfig);
2367
- if (phonesMatch(senderPhone, selfPhone)) {
2368
- return { allowed: true, reason: "self-phone-group", agentType: "admin" };
2369
- }
2370
- switch (cfg.groupPolicy) {
2371
- case "open": {
2372
- const groupConfig = params.accountConfig?.groups?.[groupJid];
2373
- const activation = groupConfig?.activation ?? "mention";
2374
- if (activation === "off") {
2375
- return { allowed: false, reason: "group-activation-off", agentType: "public" };
2376
- }
2377
- if (activation === "mention" && !isMentioned) {
2378
- return { allowed: false, reason: "mention-gating", agentType: "public" };
2379
- }
2380
- return { allowed: true, reason: "group-policy-open", agentType: "public" };
2381
- }
2382
- case "allowlist": {
2383
- const inList = cfg.groupAllowFrom.some((entry) => phonesMatch(entry, senderPhone)) || cfg.adminPhones.some((entry) => phonesMatch(entry, senderPhone));
2384
- if (!inList) {
2385
- return { allowed: false, reason: "not-in-group-allowlist", agentType: "public" };
2386
- }
2387
- return { allowed: true, reason: "group-allowlist-match", agentType: "public" };
2388
- }
2389
- case "disabled":
2390
- return { allowed: false, reason: "group-policy-disabled", agentType: "public" };
2391
- default:
2392
- return { allowed: false, reason: "unknown-group-policy", agentType: "public" };
2393
- }
2350
+ function checkGroupAccess(_params) {
2351
+ return { allowed: false, reason: "group-observe-only", agentType: "public" };
2394
2352
  }
2395
2353
 
2396
2354
  // app/lib/whatsapp/inbound/dedupe.ts
@@ -3275,11 +3233,20 @@ async function transcribe(audioPath, mimetype) {
3275
3233
  }
3276
3234
  }
3277
3235
 
3236
+ // app/lib/whatsapp/reply-error.ts
3237
+ var WaReplyError = class extends Error {
3238
+ terminal;
3239
+ constructor(message, terminal) {
3240
+ super(message);
3241
+ this.name = "WaReplyError";
3242
+ this.terminal = terminal;
3243
+ }
3244
+ };
3245
+
3278
3246
  // app/lib/whatsapp/manager.ts
3279
3247
  var TAG12 = "[whatsapp:manager]";
3280
3248
  var MAX_RECONNECT_ATTEMPTS = 10;
3281
3249
  var MESSAGE_STORE_MAX = 500;
3282
- var GROUP_META_TTL = 5 * 60 * 1e3;
3283
3250
  var connections = /* @__PURE__ */ new Map();
3284
3251
  var configDir = null;
3285
3252
  var whatsAppConfig = {};
@@ -3375,8 +3342,7 @@ async function startConnection(accountId) {
3375
3342
  reconnectAttempts: 0,
3376
3343
  abortController: new AbortController(),
3377
3344
  debouncer: null,
3378
- lidMapping: null,
3379
- groupMetaCache: /* @__PURE__ */ new Map()
3345
+ lidMapping: null
3380
3346
  };
3381
3347
  connections.set(accountId, conn);
3382
3348
  await connectWithReconnect(conn);
@@ -3412,7 +3378,8 @@ function getStatus() {
3412
3378
  reconnectAttempts: conn.reconnectAttempts,
3413
3379
  lastConnectedAt: conn.lastConnectedAt,
3414
3380
  lastError: conn.lastError,
3415
- sessionStuckReason: conn.sessionStuckReason
3381
+ sessionStuckReason: conn.sessionStuckReason,
3382
+ terminalReason: conn.terminalReason
3416
3383
  }));
3417
3384
  }
3418
3385
  function getSocket(accountId) {
@@ -3456,8 +3423,7 @@ async function registerLoginSocket(accountId, sock, authDir) {
3456
3423
  lastConnectedAt: Date.now(),
3457
3424
  abortController: new AbortController(),
3458
3425
  debouncer: null,
3459
- lidMapping,
3460
- groupMetaCache: /* @__PURE__ */ new Map()
3426
+ lidMapping
3461
3427
  };
3462
3428
  connections.set(accountId, conn);
3463
3429
  try {
@@ -3570,6 +3536,7 @@ async function connectWithReconnect(conn) {
3570
3536
  conn.selfLid = selfId.lid;
3571
3537
  conn.lastConnectedAt = connectedAt;
3572
3538
  conn.lastError = void 0;
3539
+ conn.terminalReason = void 0;
3573
3540
  const rawLidMapping = sock.signalRepository?.lidMapping ?? null;
3574
3541
  conn.lidMapping = withSelfLidShortcut(rawLidMapping, selfId.lid, selfId.jid);
3575
3542
  if (selfId.lid) {
@@ -3612,6 +3579,7 @@ async function connectWithReconnect(conn) {
3612
3579
  conn.lastError = classification.message;
3613
3580
  console.error(`${TAG12} disconnect account=${conn.accountId}: ${classification.kind} \u2014 ${classification.message}`);
3614
3581
  if (!classification.shouldRetry) {
3582
+ conn.terminalReason = classification.message;
3615
3583
  console.error(`${TAG12} terminal disconnect for account=${conn.accountId}, stopping reconnection`);
3616
3584
  return;
3617
3585
  }
@@ -3687,37 +3655,6 @@ function watchForDisconnect(conn) {
3687
3655
  }
3688
3656
  });
3689
3657
  }
3690
- async function getGroupMeta(conn, jid) {
3691
- const cached3 = conn.groupMetaCache.get(jid);
3692
- if (cached3 && cached3.expires > Date.now()) return cached3;
3693
- if (!conn.sock) return null;
3694
- console.error(`${TAG12} group metadata cache miss for ${jid}, fetching from Baileys account=${conn.accountId}`);
3695
- try {
3696
- const meta = await conn.sock.groupMetadata(jid);
3697
- const participants = await Promise.all(
3698
- (meta.participants ?? []).map(async (p) => {
3699
- return await resolveJidToE164(p.id, conn.lidMapping) ?? p.id;
3700
- })
3701
- );
3702
- const entry = {
3703
- subject: meta.subject,
3704
- participants,
3705
- expires: Date.now() + GROUP_META_TTL
3706
- };
3707
- conn.groupMetaCache.set(jid, entry);
3708
- console.error(
3709
- `${TAG12} group metadata cached for ${jid}: "${meta.subject}", ${participants.length} participants, expires in ${GROUP_META_TTL}ms account=${conn.accountId}`
3710
- );
3711
- return entry;
3712
- } catch (err) {
3713
- console.error(
3714
- `${TAG12} group metadata fetch failed for ${jid}: ${err instanceof Error ? err.message : String(err)}, caching empty entry for ${GROUP_META_TTL}ms account=${conn.accountId}`
3715
- );
3716
- const emptyEntry = { expires: Date.now() + GROUP_META_TTL };
3717
- conn.groupMetaCache.set(jid, emptyEntry);
3718
- return null;
3719
- }
3720
- }
3721
3658
  function monitorInbound(conn) {
3722
3659
  if (!conn.sock || !onInboundMessage) return;
3723
3660
  const sock = conn.sock;
@@ -3941,24 +3878,7 @@ async function handleInboundMessage(conn, msg) {
3941
3878
  }
3942
3879
  let accessResult;
3943
3880
  if (isGroup) {
3944
- accessResult = checkGroupAccess({
3945
- senderPhone,
3946
- selfPhone,
3947
- groupJid: remoteJid,
3948
- // Three-way mention recognition (Sub-fix 4): legacy phone-JID equality,
3949
- // legacy E.164-via-jidToE164 equality, plus the new self-LID equality so
3950
- // mentions in LID-addressed groups (deliver as `<self-lid>@lid`) match.
3951
- // Pre-LID groups continue matching the first two — additive, never blocks.
3952
- // Each clause guards against null on its right operand; a creds-parse
3953
- // failure leaves all three of selfJid/selfLid/selfPhone null and
3954
- // jidToE164 of a malformed mention also returns null — without the
3955
- // guards, `null === null` would spuriously match every nullified field.
3956
- isMentioned: extracted.mentionedJids.some(
3957
- (jid) => conn.selfJid !== null && jid === conn.selfJid || conn.selfLid !== null && jid === conn.selfLid || conn.selfPhone !== null && jidToE164(jid) === conn.selfPhone
3958
- ),
3959
- config: whatsAppConfig,
3960
- accountConfig: whatsAppConfig.accounts?.[conn.accountId]
3961
- });
3881
+ accessResult = checkGroupAccess({ groupJid: remoteJid });
3962
3882
  } else {
3963
3883
  accessResult = checkDmAccess({
3964
3884
  senderPhone,
@@ -3971,18 +3891,20 @@ async function handleInboundMessage(conn, msg) {
3971
3891
  `${TAG12} inbound account=${conn.accountId} from=${senderPhone} group=${isGroup} access=${accessResult.allowed ? "allowed" : "blocked"}(${accessResult.reason}) agent=${accessResult.agentType}` + (extracted.mediaType ? ` media=${extracted.mediaType}` : "") + (mediaResult ? ` mediaPath=${mediaResult.path}` : "") + (extracted.quotedMessage ? ` replyTo=${extracted.quotedMessage.id}` : "")
3972
3892
  );
3973
3893
  if (!accessResult.allowed) return;
3974
- let groupSubject;
3975
- if (isGroup) {
3976
- const groupMeta = await getGroupMeta(conn, remoteJid);
3977
- groupSubject = groupMeta?.subject;
3978
- }
3979
3894
  const sendReceipts = whatsAppConfig.accounts?.[conn.accountId]?.sendReadReceipts ?? whatsAppConfig.sendReadReceipts ?? true;
3980
3895
  if (sendReceipts && msg.key.id) {
3981
3896
  sendReadReceipt(conn.sock, remoteJid, [msg.key.id], isGroup ? senderJid : void 0);
3982
3897
  }
3983
3898
  const reply = async (text) => {
3899
+ if (isGroupJid(remoteJid)) {
3900
+ console.error(`${TAG12} op=group-reply-blocked jid=${remoteJid}`);
3901
+ throw new WaReplyError("group reply blocked \u2014 observe-only", false);
3902
+ }
3984
3903
  const currentSock = conn.sock;
3985
- if (!currentSock) throw new Error("WhatsApp disconnected \u2014 cannot reply");
3904
+ if (!currentSock) {
3905
+ const reason = conn.terminalReason;
3906
+ throw new WaReplyError(reason ?? "WhatsApp disconnected \u2014 cannot reply", Boolean(reason));
3907
+ }
3986
3908
  await sendTextMessage(currentSock, remoteJid, text, { accountId: conn.accountId });
3987
3909
  };
3988
3910
  const composing = async () => {
@@ -4004,7 +3926,6 @@ async function handleInboundMessage(conn, msg) {
4004
3926
  text: extracted.text,
4005
3927
  isGroup,
4006
3928
  groupJid: isGroup ? remoteJid : void 0,
4007
- groupSubject,
4008
3929
  reply,
4009
3930
  composing,
4010
3931
  cacheKey,
@@ -4383,13 +4304,15 @@ app.get("/", async (c) => {
4383
4304
  selfPhone: a.selfPhone ?? null,
4384
4305
  reconnectAttempts: a.reconnectAttempts,
4385
4306
  lastError: a.lastError ?? null,
4386
- sessionStuckReason: a.sessionStuckReason ?? null
4307
+ sessionStuckReason: a.sessionStuckReason ?? null,
4308
+ terminalReason: a.terminalReason ?? null
4387
4309
  }));
4388
4310
  } catch (err) {
4389
4311
  console.error(`[health] failed to read WhatsApp status: ${err instanceof Error ? err.message : String(err)}`);
4390
4312
  }
4391
4313
  const whatsappAnyConnected = whatsappAccounts.some((a) => a.connected);
4392
4314
  const whatsappAnyStuck = whatsappAccounts.some((a) => Boolean(a.sessionStuckReason));
4315
+ const whatsappAnyTerminal = whatsappAccounts.some((a) => Boolean(a.terminalReason));
4393
4316
  const account = resolveAccount();
4394
4317
  const missingPlugins = findMissingPlugins(account?.config.enabledPlugins);
4395
4318
  return c.json({
@@ -4404,6 +4327,7 @@ app.get("/", async (c) => {
4404
4327
  whatsapp: {
4405
4328
  any_connected: whatsappAnyConnected,
4406
4329
  any_stuck: whatsappAnyStuck,
4330
+ any_terminal: whatsappAnyTerminal,
4407
4331
  accounts: whatsappAccounts
4408
4332
  }
4409
4333
  });
@@ -4677,7 +4601,10 @@ async function managerRcSpawn(opts) {
4677
4601
  sliceToken: opts.sliceToken,
4678
4602
  personId: opts.personId,
4679
4603
  userId: opts.userId,
4680
- waChannel: opts.waChannel
4604
+ waChannel: opts.waChannel,
4605
+ role: opts.role,
4606
+ channel: opts.channel,
4607
+ senderId: opts.senderId
4681
4608
  })
4682
4609
  }).catch((err) => ({ __throw: err instanceof Error ? err.message : String(err) }));
4683
4610
  if ("__throw" in res) {
@@ -5286,6 +5213,13 @@ async function ensureEntry(input) {
5286
5213
  spawned = await managerRcSpawn({
5287
5214
  sessionId: adminSessionId,
5288
5215
  name: nameValue ?? void 0,
5216
+ // Task 724 — pass the real channel + role + senderId so the manager writes
5217
+ // the classification sidecar for this admin channel (webchat, email, …),
5218
+ // not just WhatsApp. Without these the live session was left sidecar-less
5219
+ // and the sidebar channel badge / archive classification mis-read it.
5220
+ role: "admin",
5221
+ channel: input.channel,
5222
+ senderId: input.senderId,
5289
5223
  logContext: `channel=${input.channel} senderId=${input.senderId}`
5290
5224
  });
5291
5225
  } else {
@@ -5987,16 +5921,6 @@ function resolvePublicAgent(accountDir, opts) {
5987
5921
  const config = readConfig(accountDir);
5988
5922
  const wa = config.whatsapp;
5989
5923
  if (!wa) return null;
5990
- if (opts.groupJid) {
5991
- const accounts2 = wa.accounts;
5992
- const account2 = accounts2?.[opts.accountId];
5993
- const groups = account2?.groups;
5994
- const group = groups?.[opts.groupJid];
5995
- const groupSlug = group?.publicAgent;
5996
- if (typeof groupSlug === "string" && groupSlug.trim()) {
5997
- return { slug: groupSlug.trim(), source: "group" };
5998
- }
5999
- }
6000
5924
  const accounts = wa.accounts;
6001
5925
  const account = accounts?.[opts.accountId];
6002
5926
  const accountSlug = account?.publicAgent;
@@ -6012,77 +5936,6 @@ function resolvePublicAgent(accountDir, opts) {
6012
5936
  return null;
6013
5937
  }
6014
5938
  }
6015
- function setGroupPublicAgent(accountDir, accountId, groupJid, slug) {
6016
- const trimmedSlug = slug.trim();
6017
- const trimmedGroup = groupJid.trim();
6018
- const trimmedAccount = accountId.trim();
6019
- if (!trimmedSlug) return { ok: false, error: "Agent slug cannot be empty." };
6020
- if (!trimmedGroup) return { ok: false, error: "Group JID cannot be empty." };
6021
- if (!trimmedAccount) return { ok: false, error: "Account ID cannot be empty." };
6022
- const agentConfigPath = join4(accountDir, "agents", trimmedSlug, "config.json");
6023
- if (!existsSync4(agentConfigPath)) {
6024
- return { ok: false, error: `Agent "${trimmedSlug}" not found \u2014 no config.json at ${agentConfigPath}. Check the agent slug and try again.` };
6025
- }
6026
- try {
6027
- const config = readConfig(accountDir);
6028
- if (!config.whatsapp || typeof config.whatsapp !== "object") config.whatsapp = {};
6029
- const wa = config.whatsapp;
6030
- if (!wa.accounts || typeof wa.accounts !== "object") wa.accounts = {};
6031
- const accounts = wa.accounts;
6032
- if (!accounts[trimmedAccount] || typeof accounts[trimmedAccount] !== "object") accounts[trimmedAccount] = {};
6033
- const account = accounts[trimmedAccount];
6034
- if (!account.groups || typeof account.groups !== "object") account.groups = {};
6035
- const groups = account.groups;
6036
- const existing = groups[trimmedGroup] && typeof groups[trimmedGroup] === "object" ? groups[trimmedGroup] : {};
6037
- groups[trimmedGroup] = { ...existing, publicAgent: trimmedSlug };
6038
- const parsed = WhatsAppConfigSchema.safeParse(wa);
6039
- if (!parsed.success) {
6040
- const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
6041
- return { ok: false, error: `Validation failed: ${msg}` };
6042
- }
6043
- config.whatsapp = parsed.data;
6044
- writeConfig(accountDir, config);
6045
- console.error(`${TAG17} setGroupPublicAgent account=${trimmedAccount} groupJid=${trimmedGroup} slug=${trimmedSlug}`);
6046
- reloadManagerConfig(accountDir);
6047
- return { ok: true, message: `Per-group public agent set to "${trimmedSlug}" for group ${trimmedGroup}.` };
6048
- } catch (err) {
6049
- const msg = err instanceof Error ? err.message : String(err);
6050
- console.error(`${TAG17} setGroupPublicAgent failed: ${msg}`);
6051
- return { ok: false, error: msg };
6052
- }
6053
- }
6054
- function unsetGroupPublicAgent(accountDir, accountId, groupJid) {
6055
- const trimmedGroup = groupJid.trim();
6056
- const trimmedAccount = accountId.trim();
6057
- if (!trimmedGroup) return { ok: false, error: "Group JID cannot be empty." };
6058
- if (!trimmedAccount) return { ok: false, error: "Account ID cannot be empty." };
6059
- try {
6060
- const config = readConfig(accountDir);
6061
- const wa = config.whatsapp ?? {};
6062
- const accounts = wa.accounts;
6063
- const account = accounts?.[trimmedAccount];
6064
- const groups = account?.groups;
6065
- const group = groups?.[trimmedGroup];
6066
- if (!group || typeof group.publicAgent !== "string") {
6067
- return { ok: true, message: `No per-group publicAgent override for group ${trimmedGroup}; nothing to remove.` };
6068
- }
6069
- delete group.publicAgent;
6070
- const parsed = WhatsAppConfigSchema.safeParse(wa);
6071
- if (!parsed.success) {
6072
- const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
6073
- return { ok: false, error: `Validation failed: ${msg}` };
6074
- }
6075
- config.whatsapp = parsed.data;
6076
- writeConfig(accountDir, config);
6077
- console.error(`${TAG17} unsetGroupPublicAgent account=${trimmedAccount} groupJid=${trimmedGroup}`);
6078
- reloadManagerConfig(accountDir);
6079
- return { ok: true, message: `Per-group public agent override removed for group ${trimmedGroup}.` };
6080
- } catch (err) {
6081
- const msg = err instanceof Error ? err.message : String(err);
6082
- console.error(`${TAG17} unsetGroupPublicAgent failed: ${msg}`);
6083
- return { ok: false, error: msg };
6084
- }
6085
- }
6086
5939
  function updateConfig(accountDir, fields) {
6087
5940
  const fieldNames = Object.keys(fields);
6088
5941
  if (fieldNames.length === 0) {
@@ -6615,29 +6468,10 @@ app3.post("/reconnect", async (c) => {
6615
6468
  return c.json({ error: String(err) }, 500);
6616
6469
  }
6617
6470
  });
6618
- app3.post("/send", async (c) => {
6619
- try {
6620
- const body = await c.req.json();
6621
- const { to, text } = body;
6622
- const accountId = validateAccountId(body.accountId);
6623
- if (!to || !text) {
6624
- return c.json({ error: "Missing required fields: to, text" }, 400);
6625
- }
6626
- const sock = getSocket(accountId);
6627
- if (!sock) {
6628
- return c.json({ error: `WhatsApp account "${accountId}" is not connected` }, 503);
6629
- }
6630
- const result = await sendTextMessage(sock, to, text, { accountId });
6631
- return c.json(result);
6632
- } catch (err) {
6633
- console.error(`${TAG20} send error: ${String(err)}`);
6634
- return c.json({ error: String(err) }, 500);
6635
- }
6636
- });
6637
6471
  app3.post("/config", async (c) => {
6638
6472
  try {
6639
6473
  const body = await c.req.json().catch(() => ({}));
6640
- const { action, phone, slug, groupJid, fields, accountId } = body;
6474
+ const { action, phone, slug, fields, accountId } = body;
6641
6475
  if (!action || typeof action !== "string") {
6642
6476
  return c.json({ ok: false, error: 'Missing required field "action".' }, 400);
6643
6477
  }
@@ -6677,32 +6511,10 @@ app3.post("/config", async (c) => {
6677
6511
  }
6678
6512
  case "get-public-agent": {
6679
6513
  const targetAccount = typeof accountId === "string" && accountId.trim() ? accountId.trim() : "default";
6680
- const targetGroup = typeof groupJid === "string" && groupJid.trim() ? groupJid.trim() : void 0;
6681
- const resolved = resolvePublicAgent(account.accountDir, { accountId: targetAccount, groupJid: targetGroup });
6682
- console.error(`${TAG20} config action=get-public-agent accountId=${targetAccount} groupJid=${targetGroup ?? "none"} slug=${resolved?.slug ?? "none"} source=${resolved?.source ?? "none"}`);
6514
+ const resolved = resolvePublicAgent(account.accountDir, { accountId: targetAccount });
6515
+ console.error(`${TAG20} config action=get-public-agent accountId=${targetAccount} slug=${resolved?.slug ?? "none"} source=${resolved?.source ?? "none"}`);
6683
6516
  return c.json({ ok: true, slug: resolved?.slug ?? null, source: resolved?.source ?? null });
6684
6517
  }
6685
- case "set-group-public-agent": {
6686
- if (!slug || typeof slug !== "string") {
6687
- return c.json({ ok: false, error: 'Missing required field "slug" (the public-agent directory name, e.g. "my-agent").' }, 400);
6688
- }
6689
- if (!groupJid || typeof groupJid !== "string") {
6690
- return c.json({ ok: false, error: 'Missing required field "groupJid" (group JID ending in @g.us).' }, 400);
6691
- }
6692
- const targetAccount = typeof accountId === "string" && accountId.trim() ? accountId.trim() : "default";
6693
- const result = setGroupPublicAgent(account.accountDir, targetAccount, groupJid, slug);
6694
- console.error(`${TAG20} config action=set-group-public-agent accountId=${targetAccount} groupJid=${groupJid} slug=${slug} ok=${result.ok}`);
6695
- return c.json(result, result.ok ? 200 : 400);
6696
- }
6697
- case "unset-group-public-agent": {
6698
- if (!groupJid || typeof groupJid !== "string") {
6699
- return c.json({ ok: false, error: 'Missing required field "groupJid" (group JID ending in @g.us).' }, 400);
6700
- }
6701
- const targetAccount = typeof accountId === "string" && accountId.trim() ? accountId.trim() : "default";
6702
- const result = unsetGroupPublicAgent(account.accountDir, targetAccount, groupJid);
6703
- console.error(`${TAG20} config action=unset-group-public-agent accountId=${targetAccount} groupJid=${groupJid} ok=${result.ok}`);
6704
- return c.json(result, result.ok ? 200 : 400);
6705
- }
6706
6518
  case "list-public-agents": {
6707
6519
  const agentsDir = resolve7(account.accountDir, "agents");
6708
6520
  const agents = [];
@@ -6769,7 +6581,7 @@ app3.post("/config", async (c) => {
6769
6581
  }
6770
6582
  default:
6771
6583
  return c.json(
6772
- { 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.` },
6584
+ { 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.` },
6773
6585
  400
6774
6586
  );
6775
6587
  }
@@ -15830,10 +15642,12 @@ function createWaChannelRoutes(deps) {
15830
15642
  try {
15831
15643
  await deps.sendOutbound(senderId, text);
15832
15644
  } catch (err) {
15645
+ const message = err instanceof Error ? err.message : String(err);
15646
+ const terminal = err instanceof WaReplyError ? err.terminal : false;
15833
15647
  console.error(
15834
- `[whatsapp-native] op=reply-failed senderId=${senderId} bytes=${bytes} error=${err instanceof Error ? err.message : String(err)}`
15648
+ `[whatsapp-native] op=reply-failed senderId=${senderId} bytes=${bytes} terminal=${terminal} error=${message}`
15835
15649
  );
15836
- return c.json({ error: "send-failed" }, 500);
15650
+ return c.json({ error: message, terminal }, 500);
15837
15651
  }
15838
15652
  console.error(`[whatsapp-native] op=reply-dispatch senderId=${senderId} bytes=${bytes}`);
15839
15653
  return c.json({ ok: true });
@@ -16353,6 +16167,11 @@ var waGateway = new WaGateway({
16353
16167
  const result = await managerRcSpawn({
16354
16168
  sessionId: adminSessionIdFor(accountId, senderId),
16355
16169
  userId,
16170
+ // Task 724 — supply the channel so /rc-spawn writes the classification
16171
+ // sidecar from the body, the same path every other admin channel uses.
16172
+ // senderId for the sidecar continues to come from the waChannel binding.
16173
+ role: "admin",
16174
+ channel: "whatsapp",
16356
16175
  waChannel: { senderId, gatewayUrl, serverPath },
16357
16176
  logContext: `channel=whatsapp-native senderId=${senderId}`
16358
16177
  });