@rubytech/create-maxy 1.0.458 → 1.0.460

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy",
3
- "version": "1.0.458",
3
+ "version": "1.0.460",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy": "./dist/index.js"
@@ -2852,8 +2852,8 @@ 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 join8, basename as basename2 } from "path";
2856
- import { homedir as homedir2 } from "os";
2855
+ import { resolve as resolve16, join as join9, basename as basename2 } from "path";
2856
+ import { homedir as homedir3 } from "os";
2857
2857
 
2858
2858
  // app/api/health/route.ts
2859
2859
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
@@ -3135,7 +3135,8 @@ async function GET() {
3135
3135
  // app/lib/claude-agent.ts
3136
3136
  import Anthropic from "@anthropic-ai/sdk";
3137
3137
  import { spawn as spawn2 } from "child_process";
3138
- import { resolve as resolve4 } from "path";
3138
+ import { resolve as resolve4, join as join3 } from "path";
3139
+ import { homedir as homedir2 } from "os";
3139
3140
  import { readFileSync as readFileSync5, readdirSync, existsSync as existsSync5, mkdirSync as mkdirSync2, createWriteStream, statSync as statSync2, unlinkSync } from "fs";
3140
3141
 
3141
3142
  // app/lib/vnc.ts
@@ -4679,7 +4680,8 @@ function fetchMcpToolsList(pluginDir) {
4679
4680
  env: {
4680
4681
  ...process.env,
4681
4682
  PLATFORM_ROOT: PLATFORM_ROOT3,
4682
- ACCOUNT_ID: "__toolslist__"
4683
+ ACCOUNT_ID: "__toolslist__",
4684
+ PLATFORM_PORT: process.env.PORT ?? "19200"
4683
4685
  }
4684
4686
  });
4685
4687
  let buffer = "";
@@ -5067,11 +5069,6 @@ function getMcpServers(accountId, enabledPlugins) {
5067
5069
  args: [resolve4(PLATFORM_ROOT3, "plugins/contacts/mcp/dist/index.js")],
5068
5070
  env: { ACCOUNT_ID: accountId, PLATFORM_ROOT: PLATFORM_ROOT3 }
5069
5071
  },
5070
- "telegram": {
5071
- command: "node",
5072
- args: [resolve4(PLATFORM_ROOT3, "plugins/telegram/mcp/dist/index.js")],
5073
- env: { ACCOUNT_ID: accountId, PLATFORM_ROOT: PLATFORM_ROOT3 }
5074
- },
5075
5072
  "whatsapp": {
5076
5073
  command: "node",
5077
5074
  args: [resolve4(PLATFORM_ROOT3, "plugins/whatsapp/mcp/dist/index.js")],
@@ -5082,11 +5079,6 @@ function getMcpServers(accountId, enabledPlugins) {
5082
5079
  args: [resolve4(PLATFORM_ROOT3, "plugins/admin/mcp/dist/index.js")],
5083
5080
  env: { ACCOUNT_ID: accountId, PLATFORM_ROOT: PLATFORM_ROOT3 }
5084
5081
  },
5085
- "cloudflare": {
5086
- command: "node",
5087
- args: [resolve4(PLATFORM_ROOT3, "plugins/cloudflare/mcp/dist/index.js")],
5088
- env: { PLATFORM_ROOT: PLATFORM_ROOT3 }
5089
- },
5090
5082
  "scheduling": {
5091
5083
  command: "node",
5092
5084
  args: [resolve4(PLATFORM_ROOT3, "plugins/scheduling/mcp/dist/index.js")],
@@ -5113,6 +5105,33 @@ function getMcpServers(accountId, enabledPlugins) {
5113
5105
  args: ["-y", "@playwright/mcp@latest", "--cdp-endpoint", "http://127.0.0.1:9222"]
5114
5106
  }
5115
5107
  };
5108
+ if (process.env.TELEGRAM_PUBLIC_BOT_TOKEN) {
5109
+ servers["telegram"] = {
5110
+ command: "node",
5111
+ args: [resolve4(PLATFORM_ROOT3, "plugins/telegram/mcp/dist/index.js")],
5112
+ env: { ACCOUNT_ID: accountId, PLATFORM_ROOT: PLATFORM_ROOT3 }
5113
+ };
5114
+ } else {
5115
+ console.error("[plugins] telegram MCP: skipped (no TELEGRAM_PUBLIC_BOT_TOKEN)");
5116
+ }
5117
+ let tunnelConfigured = false;
5118
+ try {
5119
+ const stateFile = join3(homedir2(), ".cloudflared", "tunnel.state");
5120
+ if (existsSync5(stateFile)) {
5121
+ const state = JSON.parse(readFileSync5(stateFile, "utf-8"));
5122
+ tunnelConfigured = !!state?.tunnelId;
5123
+ }
5124
+ } catch {
5125
+ }
5126
+ if (!tunnelConfigured) {
5127
+ servers["cloudflare"] = {
5128
+ command: "node",
5129
+ args: [resolve4(PLATFORM_ROOT3, "plugins/cloudflare/mcp/dist/index.js")],
5130
+ env: { PLATFORM_ROOT: PLATFORM_ROOT3 }
5131
+ };
5132
+ } else {
5133
+ console.error("[plugins] cloudflare MCP: skipped (tunnel already configured)");
5134
+ }
5116
5135
  if (Array.isArray(enabledPlugins) && enabledPlugins.length > 0) {
5117
5136
  const pluginsDir = resolve4(PLATFORM_ROOT3, "plugins");
5118
5137
  let dirs;
@@ -9249,7 +9268,7 @@ async function POST8(req) {
9249
9268
  }
9250
9269
 
9251
9270
  // app/api/whatsapp/login/start/route.ts
9252
- import { join as join3 } from "path";
9271
+ import { join as join4 } from "path";
9253
9272
 
9254
9273
  // app/lib/whatsapp/login.ts
9255
9274
  import { randomUUID as randomUUID5 } from "crypto";
@@ -9835,7 +9854,7 @@ async function POST9(req) {
9835
9854
  const body = await req.json().catch(() => ({}));
9836
9855
  const accountId = validateAccountId(body.accountId);
9837
9856
  const force = body.force ?? false;
9838
- const authDir = join3(MAXY_DIR, "credentials", "whatsapp", accountId);
9857
+ const authDir = join4(MAXY_DIR, "credentials", "whatsapp", accountId);
9839
9858
  const result = await startLogin({ accountId, authDir, force });
9840
9859
  console.error(`[whatsapp:api] login/start result account=${accountId} hasQr=${!!result.qrRaw}${result.selfPhone ? ` phone=${result.selfPhone}` : ""}`);
9841
9860
  return Response.json(result);
@@ -23622,7 +23641,8 @@ var WhatsAppAccountSchema = external_exports.object({
23622
23641
  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."),
23623
23642
  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."),
23624
23643
  selfChatMode: external_exports.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."),
23625
- allowFrom: external_exports.array(external_exports.string()).optional().describe("Phone numbers (E.164 format, e.g. +44...) allowed to message via DM when policy is 'allowlist'. Add '*' to allow everyone."),
23644
+ adminPhones: external_exports.array(external_exports.string()).optional().describe("Phone numbers (E.164) that route to the admin agent. Managed via add-admin-phone / remove-admin-phone."),
23645
+ allowFrom: external_exports.array(external_exports.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."),
23626
23646
  groupAllowFrom: external_exports.array(external_exports.string()).optional().describe("Phone numbers allowed to trigger the agent in groups when group policy is 'allowlist'."),
23627
23647
  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."),
23628
23648
  groups: external_exports.record(
@@ -23652,7 +23672,8 @@ var WhatsAppConfigSchema = external_exports.object({
23652
23672
  accounts: external_exports.record(external_exports.string(), WhatsAppAccountSchema.optional()).optional().describe("Per-account config keyed by account ID."),
23653
23673
  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."),
23654
23674
  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."),
23655
- allowFrom: external_exports.array(external_exports.string()).optional().describe("Phone numbers (E.164 format, e.g. +44...) allowed to message via DM when policy is 'allowlist'. Add '*' to allow everyone."),
23675
+ adminPhones: external_exports.array(external_exports.string()).optional().describe("Phone numbers (E.164) that route to the admin agent. Managed via add-admin-phone / remove-admin-phone."),
23676
+ allowFrom: external_exports.array(external_exports.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."),
23656
23677
  groupAllowFrom: external_exports.array(external_exports.string()).optional().describe("Phone numbers allowed to trigger the agent in groups when group policy is 'allowlist'."),
23657
23678
  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."),
23658
23679
  mediaMaxMb: external_exports.number().int().positive().optional().default(50).describe("Maximum file size in MB for media the agent will download and process."),
@@ -23833,18 +23854,16 @@ function resolveAccountConfig(config2, accountConfig) {
23833
23854
  return {
23834
23855
  dmPolicy: accountConfig?.dmPolicy ?? config2.dmPolicy ?? "disabled",
23835
23856
  groupPolicy: accountConfig?.groupPolicy ?? config2.groupPolicy ?? "disabled",
23857
+ adminPhones: accountConfig?.adminPhones ?? config2.adminPhones ?? [],
23836
23858
  allowFrom: accountConfig?.allowFrom ?? config2.allowFrom ?? [],
23837
23859
  groupAllowFrom: accountConfig?.groupAllowFrom ?? config2.groupAllowFrom ?? [],
23838
23860
  selfChatMode: accountConfig?.selfChatMode ?? false
23839
23861
  };
23840
23862
  }
23841
- function isAdminPhone(phone, allowFrom) {
23863
+ function isAdminPhone(phone, adminPhones) {
23842
23864
  const normalized = normalizeE164(phone);
23843
23865
  if (!normalized) return false;
23844
- return allowFrom.some((entry) => {
23845
- if (entry === "*") return false;
23846
- return phonesMatch(entry, normalized);
23847
- });
23866
+ return adminPhones.some((entry) => phonesMatch(entry, normalized));
23848
23867
  }
23849
23868
  function checkDmAccess(params) {
23850
23869
  const { senderPhone, selfPhone } = params;
@@ -23852,7 +23871,7 @@ function checkDmAccess(params) {
23852
23871
  if (phonesMatch(senderPhone, selfPhone)) {
23853
23872
  return { allowed: true, reason: "self-phone", agentType: "admin" };
23854
23873
  }
23855
- if (isAdminPhone(senderPhone, cfg.allowFrom)) {
23874
+ if (isAdminPhone(senderPhone, cfg.adminPhones)) {
23856
23875
  return { allowed: true, reason: "admin-binding", agentType: "admin" };
23857
23876
  }
23858
23877
  switch (cfg.dmPolicy) {
@@ -23893,10 +23912,7 @@ function checkGroupAccess(params) {
23893
23912
  return { allowed: true, reason: "group-policy-open", agentType: "public" };
23894
23913
  }
23895
23914
  case "allowlist": {
23896
- const inList = cfg.groupAllowFrom.some((entry) => phonesMatch(entry, senderPhone)) || cfg.allowFrom.some((entry) => {
23897
- if (entry === "*") return false;
23898
- return phonesMatch(entry, senderPhone);
23899
- });
23915
+ const inList = cfg.groupAllowFrom.some((entry) => phonesMatch(entry, senderPhone)) || cfg.adminPhones.some((entry) => phonesMatch(entry, senderPhone));
23900
23916
  if (!inList) {
23901
23917
  return { allowed: false, reason: "not-in-group-allowlist", agentType: "public" };
23902
23918
  }
@@ -23996,7 +24012,7 @@ async function sendReadReceipt(sock, chatJid, messageIds, participant) {
23996
24012
  // app/lib/whatsapp/inbound/media.ts
23997
24013
  import { randomUUID as randomUUID6 } from "crypto";
23998
24014
  import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
23999
- import { join as join4 } from "path";
24015
+ import { join as join5 } from "path";
24000
24016
  import {
24001
24017
  downloadMediaMessage,
24002
24018
  downloadContentFromMessage,
@@ -24082,7 +24098,7 @@ async function downloadInboundMedia(msg, sock, opts) {
24082
24098
  await mkdir2(MEDIA_DIR, { recursive: true });
24083
24099
  const ext = mimeToExt(mimetype ?? "application/octet-stream");
24084
24100
  const filename = `${randomUUID6()}.${ext}`;
24085
- const filePath = join4(MEDIA_DIR, filename);
24101
+ const filePath = join5(MEDIA_DIR, filename);
24086
24102
  await writeFile2(filePath, buffer);
24087
24103
  const sizeKB = (buffer.length / 1024).toFixed(0);
24088
24104
  console.error(`${TAG5} media downloaded type=${mimetype ?? "unknown"} size=${sizeKB}KB path=${filePath}`);
@@ -24580,7 +24596,7 @@ async function handleInboundMessage(conn, msg) {
24580
24596
 
24581
24597
  // app/lib/whatsapp/config-persist.ts
24582
24598
  import { readFileSync as readFileSync10, writeFileSync as writeFileSync6, existsSync as existsSync10 } from "fs";
24583
- import { resolve as resolve9, join as join5 } from "path";
24599
+ import { resolve as resolve9, join as join6 } from "path";
24584
24600
  var TAG8 = "[whatsapp:config]";
24585
24601
  function configPath(accountDir) {
24586
24602
  return resolve9(accountDir, "account.json");
@@ -24604,6 +24620,25 @@ function reloadManagerConfig(accountDir) {
24604
24620
  }
24605
24621
  }
24606
24622
  var E164_PATTERN = /^\+\d{7,15}$/;
24623
+ function migrateAdminPhones(wa) {
24624
+ if (wa.adminPhones !== void 0) return 0;
24625
+ if (!Array.isArray(wa.allowFrom)) return 0;
24626
+ const allowFrom = wa.allowFrom;
24627
+ const phones = [];
24628
+ const remaining = [];
24629
+ for (const entry of allowFrom) {
24630
+ if (E164_PATTERN.test(entry)) {
24631
+ phones.push(entry);
24632
+ } else {
24633
+ remaining.push(entry);
24634
+ }
24635
+ }
24636
+ if (phones.length === 0) return 0;
24637
+ wa.adminPhones = phones;
24638
+ wa.allowFrom = remaining;
24639
+ console.error(`${TAG8} migrated ${phones.length} phone(s) from allowFrom to adminPhones`);
24640
+ return phones.length;
24641
+ }
24607
24642
  function persistAfterPairing(accountDir, accountId, selfPhone) {
24608
24643
  try {
24609
24644
  const config2 = readConfig(accountDir);
@@ -24620,16 +24655,16 @@ function persistAfterPairing(accountDir, accountId, selfPhone) {
24620
24655
  }
24621
24656
  if (selfPhone) {
24622
24657
  const normalized = selfPhone.startsWith("+") ? selfPhone : `+${selfPhone}`;
24623
- if (!Array.isArray(wa.allowFrom)) {
24624
- wa.allowFrom = [];
24658
+ if (!Array.isArray(wa.adminPhones)) {
24659
+ wa.adminPhones = [];
24625
24660
  }
24626
- const allowFrom = wa.allowFrom;
24627
- if (!allowFrom.includes(normalized)) {
24628
- allowFrom.push(normalized);
24629
- console.error(`${TAG8} added selfPhone=${normalized} to allowFrom`);
24661
+ const adminPhones = wa.adminPhones;
24662
+ if (!adminPhones.includes(normalized)) {
24663
+ adminPhones.push(normalized);
24664
+ console.error(`${TAG8} added selfPhone=${normalized} to adminPhones`);
24630
24665
  }
24631
24666
  } else {
24632
- console.error(`${TAG8} skipping allowFrom \u2014 selfPhone is null account=${accountId}`);
24667
+ console.error(`${TAG8} skipping adminPhones \u2014 selfPhone is null account=${accountId}`);
24633
24668
  }
24634
24669
  const parsed = WhatsAppConfigSchema.safeParse(wa);
24635
24670
  if (!parsed.success) {
@@ -24659,14 +24694,14 @@ function addAdminPhone(accountDir, phone) {
24659
24694
  config2.whatsapp = {};
24660
24695
  }
24661
24696
  const wa = config2.whatsapp;
24662
- if (!Array.isArray(wa.allowFrom)) {
24663
- wa.allowFrom = [];
24697
+ if (!Array.isArray(wa.adminPhones)) {
24698
+ wa.adminPhones = [];
24664
24699
  }
24665
- const allowFrom = wa.allowFrom;
24666
- if (allowFrom.includes(normalized)) {
24700
+ const adminPhones = wa.adminPhones;
24701
+ if (adminPhones.includes(normalized)) {
24667
24702
  return { ok: true, message: `Phone ${normalized} is already in the admin list.` };
24668
24703
  }
24669
- allowFrom.push(normalized);
24704
+ adminPhones.push(normalized);
24670
24705
  const parsed = WhatsAppConfigSchema.safeParse(wa);
24671
24706
  if (!parsed.success) {
24672
24707
  const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
@@ -24691,15 +24726,15 @@ function removeAdminPhone(accountDir, phone) {
24691
24726
  return { ok: true, message: `No WhatsApp config exists \u2014 nothing to remove.` };
24692
24727
  }
24693
24728
  const wa = config2.whatsapp;
24694
- if (!Array.isArray(wa.allowFrom)) {
24729
+ if (!Array.isArray(wa.adminPhones)) {
24695
24730
  return { ok: true, message: `No admin phones configured \u2014 nothing to remove.` };
24696
24731
  }
24697
- const allowFrom = wa.allowFrom;
24698
- const idx = allowFrom.indexOf(normalized);
24732
+ const adminPhones = wa.adminPhones;
24733
+ const idx = adminPhones.indexOf(normalized);
24699
24734
  if (idx === -1) {
24700
24735
  return { ok: true, message: `Phone ${normalized} is not in the admin list.` };
24701
24736
  }
24702
- allowFrom.splice(idx, 1);
24737
+ adminPhones.splice(idx, 1);
24703
24738
  const parsed = WhatsAppConfigSchema.safeParse(wa);
24704
24739
  if (!parsed.success) {
24705
24740
  const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
@@ -24720,8 +24755,10 @@ function readAdminPhones(accountDir) {
24720
24755
  try {
24721
24756
  const config2 = readConfig(accountDir);
24722
24757
  const wa = config2.whatsapp;
24723
- if (!wa || !Array.isArray(wa.allowFrom)) return [];
24724
- return wa.allowFrom.filter((p) => typeof p === "string");
24758
+ if (!wa) return [];
24759
+ migrateAdminPhones(wa);
24760
+ if (!Array.isArray(wa.adminPhones)) return [];
24761
+ return wa.adminPhones.filter((p) => typeof p === "string");
24725
24762
  } catch {
24726
24763
  return [];
24727
24764
  }
@@ -24731,7 +24768,7 @@ function setPublicAgent(accountDir, slug) {
24731
24768
  if (!trimmed) {
24732
24769
  return { ok: false, error: "Agent slug cannot be empty." };
24733
24770
  }
24734
- const agentConfigPath = join5(accountDir, "agents", trimmed, "config.json");
24771
+ const agentConfigPath = join6(accountDir, "agents", trimmed, "config.json");
24735
24772
  if (!existsSync10(agentConfigPath)) {
24736
24773
  return { ok: false, error: `Agent "${trimmed}" not found \u2014 no config.json at ${agentConfigPath}. Check the agent slug and try again.` };
24737
24774
  }
@@ -24772,6 +24809,57 @@ function getPublicAgent(accountDir) {
24772
24809
  return null;
24773
24810
  }
24774
24811
  }
24812
+ function updateConfig(accountDir, fields) {
24813
+ const fieldNames = Object.keys(fields);
24814
+ if (fieldNames.length === 0) {
24815
+ return { ok: false, error: "No fields provided to update." };
24816
+ }
24817
+ try {
24818
+ const config2 = readConfig(accountDir);
24819
+ if (!config2.whatsapp || typeof config2.whatsapp !== "object") {
24820
+ config2.whatsapp = {};
24821
+ }
24822
+ const wa = config2.whatsapp;
24823
+ migrateAdminPhones(wa);
24824
+ for (const [key, value] of Object.entries(fields)) {
24825
+ wa[key] = value;
24826
+ }
24827
+ const parsed = WhatsAppConfigSchema.safeParse(wa);
24828
+ if (!parsed.success) {
24829
+ const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
24830
+ console.error(`${TAG8} update validation failed: ${msg}`);
24831
+ return { ok: false, error: `Validation failed: ${msg}` };
24832
+ }
24833
+ config2.whatsapp = parsed.data;
24834
+ writeConfig(accountDir, config2);
24835
+ console.error(`${TAG8} updated fields=[${fieldNames.join(",")}]`);
24836
+ reloadManagerConfig(accountDir);
24837
+ return { ok: true, message: `Updated WhatsApp config: ${fieldNames.join(", ")}.` };
24838
+ } catch (err) {
24839
+ const msg = err instanceof Error ? err.message : String(err);
24840
+ console.error(`${TAG8} updateConfig failed: ${msg}`);
24841
+ return { ok: false, error: msg };
24842
+ }
24843
+ }
24844
+ function getConfig(accountDir) {
24845
+ try {
24846
+ const config2 = readConfig(accountDir);
24847
+ const wa = config2.whatsapp;
24848
+ if (!wa) return null;
24849
+ const migrated = migrateAdminPhones(wa);
24850
+ if (migrated > 0) {
24851
+ const parsed = WhatsAppConfigSchema.safeParse(wa);
24852
+ if (parsed.success) {
24853
+ config2.whatsapp = parsed.data;
24854
+ writeConfig(accountDir, config2);
24855
+ reloadManagerConfig(accountDir);
24856
+ }
24857
+ }
24858
+ return wa;
24859
+ } catch {
24860
+ return null;
24861
+ }
24862
+ }
24775
24863
 
24776
24864
  // app/api/whatsapp/login/wait/route.ts
24777
24865
  async function POST10(req) {
@@ -24958,7 +25046,7 @@ function serializeWhatsAppSchema() {
24958
25046
  "",
24959
25047
  "## Constraints (superRefine)",
24960
25048
  "",
24961
- '- `dmPolicy: "open"` requires `allowFrom` to include `"*"` (both top-level and per-account)',
25049
+ '- `dmPolicy: "open"` requires `allowFrom` to include `"*"` (both top-level and per-account). This is a DM policy constraint \u2014 it does not affect admin phones, which live in the separate `adminPhones` field.',
24962
25050
  "- Account-level fields override top-level defaults when present"
24963
25051
  ].join("\n");
24964
25052
  return [topLevel, "", account, constraints].join("\n");
@@ -24968,7 +25056,7 @@ function serializeWhatsAppSchema() {
24968
25056
  async function POST14(req) {
24969
25057
  try {
24970
25058
  const body = await req.json().catch(() => ({}));
24971
- const { action, phone, slug, accountId } = body;
25059
+ const { action, phone, slug, fields, accountId } = body;
24972
25060
  if (!action || typeof action !== "string") {
24973
25061
  return Response.json({ ok: false, error: 'Missing required field "action".' }, { status: 400 });
24974
25062
  }
@@ -25037,9 +25125,23 @@ async function POST14(req) {
25037
25125
  return Response.json({ ok: false, error: `Failed to fetch groups: ${String(err)}` });
25038
25126
  }
25039
25127
  }
25128
+ case "update-config": {
25129
+ if (!fields || typeof fields !== "object" || Array.isArray(fields)) {
25130
+ return Response.json({ ok: false, error: 'Missing required field "fields" (object with config field names and values).' }, { status: 400 });
25131
+ }
25132
+ const result = updateConfig(account.accountDir, fields);
25133
+ const fieldNames = Object.keys(fields);
25134
+ console.error(`[whatsapp:api] config action=update-config fields=[${fieldNames.join(",")}] ok=${result.ok}`);
25135
+ return Response.json(result, { status: result.ok ? 200 : 400 });
25136
+ }
25137
+ case "get-config": {
25138
+ const waConfig = getConfig(account.accountDir);
25139
+ console.error(`[whatsapp:api] config action=get-config`);
25140
+ return Response.json({ ok: true, config: waConfig });
25141
+ }
25040
25142
  default:
25041
25143
  return Response.json(
25042
- { ok: false, error: `Unknown action "${action}". Valid actions: add-admin-phone, remove-admin-phone, list-admin-phones, set-public-agent, get-public-agent, schema, list-groups.` },
25144
+ { ok: false, error: `Unknown action "${action}". Valid actions: add-admin-phone, remove-admin-phone, list-admin-phones, set-public-agent, get-public-agent, update-config, get-config, schema, list-groups.` },
25043
25145
  { status: 400 }
25044
25146
  );
25045
25147
  }
@@ -25800,11 +25902,11 @@ async function GET7() {
25800
25902
 
25801
25903
  // app/api/admin/version/route.ts
25802
25904
  import { readFileSync as readFileSync17, existsSync as existsSync17 } from "fs";
25803
- import { resolve as resolve14, join as join6 } from "path";
25905
+ import { resolve as resolve14, join as join7 } from "path";
25804
25906
  var PLATFORM_ROOT6 = process.env.MAXY_PLATFORM_ROOT ?? resolve14(process.cwd(), "..");
25805
25907
  var brandHostname = "maxy";
25806
25908
  var brandNpmPackage = "@rubytech/create-maxy";
25807
- var brandJsonPath = join6(PLATFORM_ROOT6, "config", "brand.json");
25909
+ var brandJsonPath = join7(PLATFORM_ROOT6, "config", "brand.json");
25808
25910
  if (existsSync17(brandJsonPath)) {
25809
25911
  try {
25810
25912
  const brand = JSON.parse(readFileSync17(brandJsonPath, "utf-8"));
@@ -25876,11 +25978,11 @@ async function GET8() {
25876
25978
  // app/api/admin/version/upgrade/route.ts
25877
25979
  import { spawn as spawn4 } from "child_process";
25878
25980
  import { existsSync as existsSync18, statSync as statSync4, writeFileSync as writeFileSync11, readFileSync as readFileSync18, openSync as openSync2, closeSync as closeSync2 } from "fs";
25879
- import { resolve as resolve15, join as join7 } from "path";
25981
+ import { resolve as resolve15, join as join8 } from "path";
25880
25982
  var PLATFORM_ROOT7 = process.env.MAXY_PLATFORM_ROOT ?? resolve15(process.cwd(), "..");
25881
25983
  var upgradePkg = "@rubytech/create-maxy";
25882
25984
  var upgradeHostname = "maxy";
25883
- var brandPath = join7(PLATFORM_ROOT7, "config", "brand.json");
25985
+ var brandPath = join8(PLATFORM_ROOT7, "config", "brand.json");
25884
25986
  if (existsSync18(brandPath)) {
25885
25987
  try {
25886
25988
  const brand = JSON.parse(readFileSync18(brandPath, "utf-8"));
@@ -26047,7 +26149,7 @@ async function POST21() {
26047
26149
 
26048
26150
  // server/index.ts
26049
26151
  var PLATFORM_ROOT8 = process.env.MAXY_PLATFORM_ROOT || "";
26050
- var BRAND_JSON_PATH = PLATFORM_ROOT8 ? join8(PLATFORM_ROOT8, "config", "brand.json") : "";
26152
+ var BRAND_JSON_PATH = PLATFORM_ROOT8 ? join9(PLATFORM_ROOT8, "config", "brand.json") : "";
26051
26153
  var BRAND = { productName: "Maxy", hostname: "maxy", configDir: ".maxy", domain: "getmaxy.com" };
26052
26154
  if (BRAND_JSON_PATH && !existsSync19(BRAND_JSON_PATH)) {
26053
26155
  console.error(`[brand] WARNING: brand.json not found at ${BRAND_JSON_PATH} \u2014 using Maxy defaults`);
@@ -26066,7 +26168,7 @@ var brandLoginOpts = {
26066
26168
  primaryHoverColor: BRAND.defaultColors?.primaryHover,
26067
26169
  primarySubtle: BRAND.defaultColors?.primarySubtle
26068
26170
  };
26069
- var ALIAS_DOMAINS_PATH = join8(homedir2(), BRAND.configDir, "alias-domains.json");
26171
+ var ALIAS_DOMAINS_PATH = join9(homedir3(), BRAND.configDir, "alias-domains.json");
26070
26172
  function loadAliasDomains() {
26071
26173
  try {
26072
26174
  if (!existsSync19(ALIAS_DOMAINS_PATH)) return null;
@@ -26436,14 +26538,14 @@ function cachedHtml(file2) {
26436
26538
  }
26437
26539
  var brandedHtmlCache = /* @__PURE__ */ new Map();
26438
26540
  function loadBrandingCache(agentSlug) {
26439
- const configDir2 = join8(homedir2(), BRAND.configDir);
26541
+ const configDir2 = join9(homedir3(), BRAND.configDir);
26440
26542
  try {
26441
- const accountJsonPath = join8(configDir2, "account.json");
26543
+ const accountJsonPath = join9(configDir2, "account.json");
26442
26544
  if (!existsSync19(accountJsonPath)) return null;
26443
26545
  const account = JSON.parse(readFileSync19(accountJsonPath, "utf-8"));
26444
26546
  const accountId = account.accountId;
26445
26547
  if (!accountId) return null;
26446
- const cachePath = join8(configDir2, "branding-cache", accountId, `${agentSlug}.json`);
26548
+ const cachePath = join9(configDir2, "branding-cache", accountId, `${agentSlug}.json`);
26447
26549
  if (!existsSync19(cachePath)) return null;
26448
26550
  return JSON.parse(readFileSync19(cachePath, "utf-8"));
26449
26551
  } catch {
@@ -26452,8 +26554,8 @@ function loadBrandingCache(agentSlug) {
26452
26554
  }
26453
26555
  function resolveDefaultSlug() {
26454
26556
  try {
26455
- const configDir2 = join8(homedir2(), BRAND.configDir);
26456
- const accountJsonPath = join8(configDir2, "account.json");
26557
+ const configDir2 = join9(homedir3(), BRAND.configDir);
26558
+ const accountJsonPath = join9(configDir2, "account.json");
26457
26559
  if (!existsSync19(accountJsonPath)) return null;
26458
26560
  const account = JSON.parse(readFileSync19(accountJsonPath, "utf-8"));
26459
26561
  return account.defaultAgent || null;
@@ -26532,7 +26634,7 @@ if (bootAccountConfig?.whatsapp) {
26532
26634
  }
26533
26635
  init({
26534
26636
  configDir: configDirForWhatsApp,
26535
- platformRoot: resolve16(process.env.MAXY_PLATFORM_ROOT ?? join8(__dirname, "..")),
26637
+ platformRoot: resolve16(process.env.MAXY_PLATFORM_ROOT ?? join9(__dirname, "..")),
26536
26638
  accountConfig: bootAccountConfig,
26537
26639
  onMessage: async (msg) => {
26538
26640
  try {
@@ -20,202 +20,28 @@ metadata: {"platform":{"optional":true,"recommended":true}}
20
20
 
21
21
  Manages the agent's own dedicated email account — IMAP for reading, SMTP for sending. Admin agent only.
22
22
 
23
- ## Setup
23
+ ## Capabilities
24
24
 
25
- Collect email credentials via a form. Render this `form` component with `render-component`:
25
+ - **Setup:** `email-setup` — collect credentials via rendered `form`, connect IMAP/SMTP. Supports alias addresses (`agentAddress`).
26
+ - **Read inbox:** `email-read` — metadata only (UID, sender, subject, date). Supports pagination (`before_uid`), folders (`inbox`/`sent`), filtering by sender/date/subject.
27
+ - **Search inbox:** `email-search` — live IMAP search by sender, subject, body keyword, date range.
28
+ - **Send:** `email-send` — new outbound email.
29
+ - **Reply:** `email-reply` — threaded reply to an existing email by `messageId`. Supports `replyAll`.
30
+ - **Recall/history:** `email-graph-query` — search stored emails in Neo4j by natural language, sender, date, direction.
31
+ - **Status:** `email-status` — connection health and configuration state.
32
+ - **Auto-respond:** `email-auto-respond-config` — enable/disable automated public agent replies to inbound email.
33
+ - **OTP:** `email-otp-extract` — poll for verification codes during service authentication.
26
34
 
27
- **Fields:**
35
+ ## Retrieval paths
28
36
 
29
- | name | label | type | required | placeholder |
30
- |------|-------|------|----------|-------------|
31
- | `email` | Email address | text | yes | user@example.com |
32
- | `password` | Password | text | yes | App password or account password |
33
- | `imapHost` | IMAP host | text | yes | imap.example.com |
34
- | `imapPort` | IMAP port | number | yes | 993 |
35
- | `imapSecurity` | IMAP security | select | yes | Options: `tls` (default), `starttls` |
36
- | `smtpHost` | SMTP host | text | yes | smtp.example.com |
37
- | `smtpPort` | SMTP port | number | yes | 587 |
38
- | `smtpSecurity` | SMTP security | select | yes | Options: `tls`, `starttls` (default) |
39
- | `agentAddress` | Agent email address (if different from login) | text | no | agent@yourdomain.com |
37
+ - **Live inbox** (`email-read`, `email-search`) real-time IMAP queries. Use for checking the inbox or reading recent messages.
38
+ - **Graph history** (`email-graph-query`) — stored Email nodes in Neo4j. Use for recall, semantic search, email history questions.
39
+ - **General knowledge** (`memory-search`) cross-type knowledge graph. Use when the question spans all memory, not just emails.
40
40
 
41
- The `agentAddress` field is for catchall/alias setups where the authentication email differs from the address the agent operates as. Leave empty when they're the same.
41
+ ## References
42
42
 
43
- After form submission, pass all fields to `email-setup`. On success, confirm "credentials stored" and the inbox count. On failure, relay the diagnostic — the tool returns specific guidance (wrong password, connection refused, TLS mismatch).
43
+ | Reference | Topics |
44
+ |-----------|--------|
45
+ | [email-reference.md](references/email-reference.md) | Setup form fields, pagination, filtering, replying, alias support, persistence, threading, screening, auto-respond, rate limiting, OTP integration |
44
46
 
45
- The agent never displays stored passwords. Never name or recommend specific email providers — the form collects the standard fields every provider gives its users.
46
-
47
- ## Reading and Searching
48
-
49
- `email-read` and `email-search` return message metadata only: UID, sender, subject, and date. Body content is not included — present results as a message list without commenting on missing bodies.
50
-
51
- ### Pagination
52
-
53
- Both tools return up to 50 messages per request (default 20). When more messages exist beyond the returned set, the response includes a `next_before_uid` value. Pass this value as the `before_uid` parameter on the next call to retrieve the next page of older messages. When `next_before_uid` is absent, all matching messages have been returned.
54
-
55
- ### Folders
56
-
57
- Both tools accept a `folder` parameter: `"inbox"` (default) or `"sent"`. The Sent folder is discovered automatically via the IMAP server's special-use flags — no folder name guessing required.
58
-
59
- ### Filtering
60
-
61
- Both tools support filtering by:
62
- - `sender` — sender address or domain
63
- - `to` — recipient address
64
- - `since` / `before` — ISO date range
65
- - `email-read` additionally supports `subjectPattern` (regex)
66
- - `email-search` additionally supports `subject` (text) and `body` (keyword)
67
-
68
- ## Replying
69
-
70
- `email-reply` sends a reply to an existing email, threaded correctly in the recipient's mail client.
71
-
72
- The tool accepts the `messageId` of the email being replied to (visible in `email-read` output as the `Message-ID` field). It looks up the original email in the graph, sets `In-Reply-To` and `References` headers, derives the recipient from the original sender, and prepends `Re:` to the subject (stripping duplicate prefixes).
73
-
74
- - **Reply to sender:** Default behaviour — replies to the original sender only.
75
- - **Reply all:** Set `replyAll: true` to include all original recipients (TO + CC). The agent's own address is automatically excluded from the recipient list.
76
- - **Threading:** The reply appears in the same conversation thread in Gmail, Apple Mail, Outlook, and other RFC 2822-compliant clients.
77
- - **Prerequisite:** The original email must exist in the graph (stored by the background polling job). If the email hasn't been fetched yet, the tool returns a clear error.
78
-
79
- Use `email-reply` when responding to a received email. Use `email-send` for new outbound messages.
80
-
81
- ## Alias Support
82
-
83
- When `agentAddress` is set and differs from the auth email:
84
-
85
- - **Sending:** emails are sent with the agent address as the From header. SMTP authentication still uses the auth email.
86
- - **Reading/searching:** only emails whose TO header contains the agent address are returned. The agent ignores emails to other addresses on the same mailbox.
87
- - **OTP extraction:** only OTPs addressed to the agent address are found.
88
- - **Status:** reports both the agent address and the auth email.
89
-
90
- When `agentAddress` is not set or matches the auth email, all tools behave as before — no filtering, From header uses the auth email.
91
-
92
- ## Email Persistence
93
-
94
- Emails are automatically polled from IMAP and stored as `Email` nodes in the graph. This happens via a background cron job — no manual triggering needed.
95
-
96
- - **Polling:** After `email-setup` completes, the platform polls IMAP at the configured interval (default: every 5 minutes). Only emails addressed TO the agent's `agentAddress` are stored.
97
- - **Deduplication:** Each email is identified by its Message-ID header. Re-polling the same messages does not create duplicates, even if the agent's email address changes between poll cycles. A composite unique constraint on `(messageId, accountId)` provides database-level enforcement.
98
- - **Semantic search:** Stored emails are vector-indexed (subject + body preview), enabling natural-language search over email history via `email-graph-query`.
99
- - **Isolation:** Each agent sees only emails addressed to its own bound email address, even when sharing a catchall mailbox.
100
-
101
- ## Email Threading
102
-
103
- Emails are linked into conversation threads via `REPLY_TO` graph edges. When an email has an `In-Reply-To` header, the platform looks up the parent email by `Message-ID` within the same account and creates an edge. Thread linking happens automatically during the polling cron job.
104
-
105
- - **Out-of-order delivery:** If a reply arrives before its parent, the edge is created later when the parent is stored (orphan back-fill).
106
- - **Thread context:** `email-read` and `email-search` include `Thread-Depth` (number of hops to the thread root) and `Thread-ID` (emailId of the root message) for any email that is part of a thread. Root emails (no parent) have no thread fields.
107
- - **Scope:** Thread edges never cross account boundaries — parent lookups are always scoped to the same account.
108
-
109
- ## Auto-Respond Audit Trail
110
-
111
- When auto-respond sends a reply, the outbound message is stored as an `Email` node with `direction: "outbound"` and a `REPLY_TO` edge to the original inbound email. This creates a queryable audit trail of every automated reply — what the agent said, when, and to whom.
112
-
113
- Outbound email nodes have the same properties as inbound emails (subject, bodyPreview, fromAddress, toAddresses, timestamps) plus the `direction` property. They appear in thread traversals alongside inbound messages.
114
-
115
- ### Agent-Email Binding
116
-
117
- Each email address is bound to exactly one agent. Each agent can have at most one email address. This is enforced during `email-setup`:
118
-
119
- - The `agentSlug` parameter identifies which agent owns this email address (defaults to `"admin"`)
120
- - If the address is already bound to a different agent, setup fails with a clear error naming the conflicting agent
121
- - The binding is on `agentAddress` (the identity the agent presents), not the auth email
122
-
123
- ### Email Retrieval Paths
124
-
125
- Three retrieval paths exist for email data — each scoped to a different purpose:
126
-
127
- - **Live inbox** (`email-read`, `email-search`) — queries IMAP directly. Use for checking the inbox, reading recent messages, browsing folders, and real-time inbox state. Requires IMAP connectivity.
128
- - **Graph list/search** (`email-graph-query`) — queries stored Email nodes in Neo4j. Use for email history, recall, and semantic search ("what emails are in memory?", "find emails about cabbages", "what did Sarah send about the contract?"). Works without IMAP. Supports date/sender/direction filters and natural-language semantic matching.
129
- - **General knowledge** (`memory-search`) — searches the entire knowledge graph across all node types. Use when the user's question spans all memory, not just emails ("what do you know about contract renewals?").
130
-
131
- When the user asks about stored emails, email history, or email recall — use `email-graph-query`. When they ask to check the inbox or read specific recent messages — use the IMAP tools. When the question is broader than emails — use `memory-search`.
132
-
133
- ## Inbound Email Screening
134
-
135
- Every inbound email is classified by Claude Haiku before entering Neo4j. The classifier examines `fromAddress`, `fromName`, `subject`, and the first 1024 characters of `bodyPreview`.
136
-
137
- ### Verdicts
138
-
139
- | Verdict | Stored? | Embedding? | Auto-respond? | Graph query? |
140
- |---------|---------|------------|---------------|--------------|
141
- | `clean` | Yes | Yes | Yes | Yes |
142
- | `suspicious` | Yes (with `screeningReason`) | Yes | No | Yes |
143
- | `discarded` | Yes (with `screeningReason`) | No | No | No (list mode) |
144
-
145
- - **clean** — legitimate personal or business correspondence
146
- - **suspicious** — phishing indicators (urgency + credential requests, mismatched sender), prompt injection patterns, or classification failure (safe default)
147
- - **discarded** — obvious spam, marketing bulk mail, auto-generated notifications with no actionable content
148
-
149
- ### Prompt injection flag
150
-
151
- When `promptInjectionFlag` is true (body contains instruction-like patterns targeting an AI agent), auto-respond skips the email entirely regardless of verdict. No reply is generated. The email is visible in Neo4j for admin review.
152
-
153
- ### Failure handling
154
-
155
- If the Haiku API is unavailable (network, rate limit, auth failure), the email defaults to `suspicious`. The entire fetch batch is never blocked — each email is classified independently. API key absence causes all emails in the batch to default to `suspicious`.
156
-
157
- ### Logs
158
-
159
- Classification verdicts are logged to `{accountDir}/logs/email-fetch.log` with per-email detail (fromAddress, verdict, reason, promptInjectionRisk) and batch summaries.
160
-
161
- ## Security
162
-
163
- - Credentials stored as a file with restricted permissions — never in conversation
164
- - IMAP and SMTP connections require TLS or STARTTLS — plaintext is rejected
165
- - Only the agent's own dedicated account is accessed — never the user's personal email
166
- - Email nodes have `scope: "admin"` — never visible to public agents
167
- - Inbound emails are classified by Haiku before storage — prompt injection and phishing are flagged
168
-
169
- ## Auto-Respond
170
-
171
- When enabled, a public agent automatically replies to incoming emails. A cron job polls the inbox for new messages and generates replies via the assigned agent.
172
-
173
- ### Setup
174
-
175
- To enable auto-respond, call `email-auto-respond-config` with `enabled: true`. The tool requires an `agentSlug` — render a `single-select` component with available public agents so the admin can choose which agent handles responses. The tool returns the agent list when called without a slug.
176
-
177
- When called without an `agentSlug`, the tool returns available agents. Present these as a `single-select` component for the admin to choose from, then call the tool again with the selected slug.
178
-
179
- ### Configuration
180
-
181
- - `enabled` — `true` to enable, `false` to disable
182
- - `agentSlug` — slug of the public agent that responds (required when enabling)
183
- - `pollIntervalMinutes` — how often to check for new emails (default: 5, range: 1–60)
184
- - `hourlyLimit` — maximum auto-replies per clock hour UTC (default: 20, range: 1–1000)
185
- - `dailyLimit` — maximum auto-replies per calendar day UTC (default: 100, range: 1–10000)
186
-
187
- ### Behaviour
188
-
189
- - The cron job runs every minute. Each account's poll is skipped if the configured interval hasn't elapsed since the last poll.
190
- - Only emails addressed TO the agent's email address are processed (alias filtering applies).
191
- - Auto-replies, mailing list messages, and emails from the agent's own address are automatically skipped (RFC 3834 loop prevention).
192
- - Outgoing replies include `In-Reply-To` and `References` headers for correct threading, and `Auto-Submitted: auto-replied` to prevent loops with other auto-responders.
193
- - The subject line carries a single `Re:` prefix (no doubling).
194
-
195
- ### Rate Limiting
196
-
197
- Per-account send caps prevent runaway auto-replies from spam waves, mailing list explosions, or deliberate attacks. Two independent limits:
198
-
199
- - **Hourly limit** (default 20) — resets at each UTC clock-hour boundary
200
- - **Daily limit** (default 100) — resets at midnight UTC
201
-
202
- When either limit is reached, remaining emails in the poll cycle are skipped (not replied to). Auto-respond is **not** disabled — counters reset naturally at the next boundary. A medium-priority admin Task is created to notify the admin.
203
-
204
- Within a single poll cycle, only one auto-reply is sent per unique sender address. This prevents reply storms when an automated sender delivers multiple messages. Sender deduplication is case-insensitive.
205
-
206
- ### Circuit Breaker
207
-
208
- After 3 consecutive failures (IMAP errors, API failures, SMTP rejections), auto-respond is automatically disabled and a high-priority Task is created for the admin. The admin sees this task at the start of their next session and can re-enable auto-respond after resolving the issue.
209
-
210
- ### Disabling
211
-
212
- Call `email-auto-respond-config` with `enabled: false`. This clears the agent assignment and resets the failure counter.
213
-
214
- ## OTP Integration
215
-
216
- Other skills call `email-otp-extract` during service authentication (Anthropic OAuth, Cloudflare setup) with:
217
- - `sender` — expected sender domain
218
- - `subject_pattern` — optional regex for subject line
219
- - `timeout` — how long to poll (default 60s)
220
-
221
- The tool polls at short intervals, extracts the verification code, and returns it. On timeout, it returns a clear failure so the calling skill can ask the user to check manually.
47
+ Load the reference via `plugin-read` when detailed configuration or behaviour information is needed.
@@ -0,0 +1,203 @@
1
+ # Email Reference
2
+
3
+ Full reference for the email plugin. Load with `plugin-read` when detailed configuration or behaviour reference is needed.
4
+
5
+ ## Setup
6
+
7
+ Collect email credentials via a form. Render this `form` component with `render-component`:
8
+
9
+ **Fields:**
10
+
11
+ | name | label | type | required | placeholder |
12
+ |------|-------|------|----------|-------------|
13
+ | `email` | Email address | text | yes | user@example.com |
14
+ | `password` | Password | text | yes | App password or account password |
15
+ | `imapHost` | IMAP host | text | yes | imap.example.com |
16
+ | `imapPort` | IMAP port | number | yes | 993 |
17
+ | `imapSecurity` | IMAP security | select | yes | Options: `tls` (default), `starttls` |
18
+ | `smtpHost` | SMTP host | text | yes | smtp.example.com |
19
+ | `smtpPort` | SMTP port | number | yes | 587 |
20
+ | `smtpSecurity` | SMTP security | select | yes | Options: `tls`, `starttls` (default) |
21
+ | `agentAddress` | Agent email address (if different from login) | text | no | agent@yourdomain.com |
22
+
23
+ The `agentAddress` field is for catchall/alias setups where the authentication email differs from the address the agent operates as. Leave empty when they're the same.
24
+
25
+ After form submission, pass all fields to `email-setup`. On success, confirm "credentials stored" and the inbox count. On failure, relay the diagnostic — the tool returns specific guidance (wrong password, connection refused, TLS mismatch).
26
+
27
+ The agent never displays stored passwords. Never name or recommend specific email providers — the form collects the standard fields every provider gives its users.
28
+
29
+ ## Reading and Searching
30
+
31
+ `email-read` and `email-search` return message metadata only: UID, sender, subject, and date. Body content is not included — present results as a message list without commenting on missing bodies.
32
+
33
+ ### Pagination
34
+
35
+ Both tools return up to 50 messages per request (default 20). When more messages exist beyond the returned set, the response includes a `next_before_uid` value. Pass this value as the `before_uid` parameter on the next call to retrieve the next page of older messages. When `next_before_uid` is absent, all matching messages have been returned.
36
+
37
+ ### Folders
38
+
39
+ Both tools accept a `folder` parameter: `"inbox"` (default) or `"sent"`. The Sent folder is discovered automatically via the IMAP server's special-use flags — no folder name guessing required.
40
+
41
+ ### Filtering
42
+
43
+ Both tools support filtering by:
44
+ - `sender` — sender address or domain
45
+ - `to` — recipient address
46
+ - `since` / `before` — ISO date range
47
+ - `email-read` additionally supports `subjectPattern` (regex)
48
+ - `email-search` additionally supports `subject` (text) and `body` (keyword)
49
+
50
+ ## Replying
51
+
52
+ `email-reply` sends a reply to an existing email, threaded correctly in the recipient's mail client.
53
+
54
+ The tool accepts the `messageId` of the email being replied to (visible in `email-read` output as the `Message-ID` field). It looks up the original email in the graph, sets `In-Reply-To` and `References` headers, derives the recipient from the original sender, and prepends `Re:` to the subject (stripping duplicate prefixes).
55
+
56
+ - **Reply to sender:** Default behaviour — replies to the original sender only.
57
+ - **Reply all:** Set `replyAll: true` to include all original recipients (TO + CC). The agent's own address is automatically excluded from the recipient list.
58
+ - **Threading:** The reply appears in the same conversation thread in Gmail, Apple Mail, Outlook, and other RFC 2822-compliant clients.
59
+ - **Prerequisite:** The original email must exist in the graph (stored by the background polling job). If the email hasn't been fetched yet, the tool returns a clear error.
60
+
61
+ Use `email-reply` when responding to a received email. Use `email-send` for new outbound messages.
62
+
63
+ ## Alias Support
64
+
65
+ When `agentAddress` is set and differs from the auth email:
66
+
67
+ - **Sending:** emails are sent with the agent address as the From header. SMTP authentication still uses the auth email.
68
+ - **Reading/searching:** only emails whose TO header contains the agent address are returned. The agent ignores emails to other addresses on the same mailbox.
69
+ - **OTP extraction:** only OTPs addressed to the agent address are found.
70
+ - **Status:** reports both the agent address and the auth email.
71
+
72
+ When `agentAddress` is not set or matches the auth email, all tools behave as before — no filtering, From header uses the auth email.
73
+
74
+ ## Email Persistence
75
+
76
+ Emails are automatically polled from IMAP and stored as `Email` nodes in the graph. This happens via a background cron job — no manual triggering needed.
77
+
78
+ - **Polling:** After `email-setup` completes, the platform polls IMAP at the configured interval (default: every 5 minutes). Only emails addressed TO the agent's `agentAddress` are stored.
79
+ - **Deduplication:** Each email is identified by its Message-ID header. Re-polling the same messages does not create duplicates, even if the agent's email address changes between poll cycles. A composite unique constraint on `(messageId, accountId)` provides database-level enforcement.
80
+ - **Semantic search:** Stored emails are vector-indexed (subject + body preview), enabling natural-language search over email history via `email-graph-query`.
81
+ - **Isolation:** Each agent sees only emails addressed to its own bound email address, even when sharing a catchall mailbox.
82
+
83
+ ## Email Threading
84
+
85
+ Emails are linked into conversation threads via `REPLY_TO` graph edges. When an email has an `In-Reply-To` header, the platform looks up the parent email by `Message-ID` within the same account and creates an edge. Thread linking happens automatically during the polling cron job.
86
+
87
+ - **Out-of-order delivery:** If a reply arrives before its parent, the edge is created later when the parent is stored (orphan back-fill).
88
+ - **Thread context:** `email-read` and `email-search` include `Thread-Depth` (number of hops to the thread root) and `Thread-ID` (emailId of the root message) for any email that is part of a thread. Root emails (no parent) have no thread fields.
89
+ - **Scope:** Thread edges never cross account boundaries — parent lookups are always scoped to the same account.
90
+
91
+ ## Auto-Respond Audit Trail
92
+
93
+ When auto-respond sends a reply, the outbound message is stored as an `Email` node with `direction: "outbound"` and a `REPLY_TO` edge to the original inbound email. This creates a queryable audit trail of every automated reply — what the agent said, when, and to whom.
94
+
95
+ Outbound email nodes have the same properties as inbound emails (subject, bodyPreview, fromAddress, toAddresses, timestamps) plus the `direction` property. They appear in thread traversals alongside inbound messages.
96
+
97
+ ### Agent-Email Binding
98
+
99
+ Each email address is bound to exactly one agent. Each agent can have at most one email address. This is enforced during `email-setup`:
100
+
101
+ - The `agentSlug` parameter identifies which agent owns this email address (defaults to `"admin"`)
102
+ - If the address is already bound to a different agent, setup fails with a clear error naming the conflicting agent
103
+ - The binding is on `agentAddress` (the identity the agent presents), not the auth email
104
+
105
+ ### Email Retrieval Paths
106
+
107
+ Three retrieval paths exist for email data — each scoped to a different purpose:
108
+
109
+ - **Live inbox** (`email-read`, `email-search`) — queries IMAP directly. Use for checking the inbox, reading recent messages, browsing folders, and real-time inbox state. Requires IMAP connectivity.
110
+ - **Graph list/search** (`email-graph-query`) — queries stored Email nodes in Neo4j. Use for email history, recall, and semantic search ("what emails are in memory?", "find emails about cabbages", "what did Sarah send about the contract?"). Works without IMAP. Supports date/sender/direction filters and natural-language semantic matching.
111
+ - **General knowledge** (`memory-search`) — searches the entire knowledge graph across all node types. Use when the user's question spans all memory, not just emails ("what do you know about contract renewals?").
112
+
113
+ When the user asks about stored emails, email history, or email recall — use `email-graph-query`. When they ask to check the inbox or read specific recent messages — use the IMAP tools. When the question is broader than emails — use `memory-search`.
114
+
115
+ ## Inbound Email Screening
116
+
117
+ Every inbound email is classified by Claude Haiku before entering Neo4j. The classifier examines `fromAddress`, `fromName`, `subject`, and the first 1024 characters of `bodyPreview`.
118
+
119
+ ### Verdicts
120
+
121
+ | Verdict | Stored? | Embedding? | Auto-respond? | Graph query? |
122
+ |---------|---------|------------|---------------|--------------|
123
+ | `clean` | Yes | Yes | Yes | Yes |
124
+ | `suspicious` | Yes (with `screeningReason`) | Yes | No | Yes |
125
+ | `discarded` | Yes (with `screeningReason`) | No | No | No (list mode) |
126
+
127
+ - **clean** — legitimate personal or business correspondence
128
+ - **suspicious** — phishing indicators (urgency + credential requests, mismatched sender), prompt injection patterns, or classification failure (safe default)
129
+ - **discarded** — obvious spam, marketing bulk mail, auto-generated notifications with no actionable content
130
+
131
+ ### Prompt injection flag
132
+
133
+ When `promptInjectionFlag` is true (body contains instruction-like patterns targeting an AI agent), auto-respond skips the email entirely regardless of verdict. No reply is generated. The email is visible in Neo4j for admin review.
134
+
135
+ ### Failure handling
136
+
137
+ If the Haiku API is unavailable (network, rate limit, auth failure), the email defaults to `suspicious`. The entire fetch batch is never blocked — each email is classified independently. API key absence causes all emails in the batch to default to `suspicious`.
138
+
139
+ ### Logs
140
+
141
+ Classification verdicts are logged to `{accountDir}/logs/email-fetch.log` with per-email detail (fromAddress, verdict, reason, promptInjectionRisk) and batch summaries.
142
+
143
+ ## Security
144
+
145
+ - Credentials stored as a file with restricted permissions — never in conversation
146
+ - IMAP and SMTP connections require TLS or STARTTLS — plaintext is rejected
147
+ - Only the agent's own dedicated account is accessed — never the user's personal email
148
+ - Email nodes have `scope: "admin"` — never visible to public agents
149
+ - Inbound emails are classified by Haiku before storage — prompt injection and phishing are flagged
150
+
151
+ ## Auto-Respond
152
+
153
+ When enabled, a public agent automatically replies to incoming emails. A cron job polls the inbox for new messages and generates replies via the assigned agent.
154
+
155
+ ### Setup
156
+
157
+ To enable auto-respond, call `email-auto-respond-config` with `enabled: true`. The tool requires an `agentSlug` — render a `single-select` component with available public agents so the admin can choose which agent handles responses. The tool returns the agent list when called without a slug.
158
+
159
+ When called without an `agentSlug`, the tool returns available agents. Present these as a `single-select` component for the admin to choose from, then call the tool again with the selected slug.
160
+
161
+ ### Configuration
162
+
163
+ - `enabled` — `true` to enable, `false` to disable
164
+ - `agentSlug` — slug of the public agent that responds (required when enabling)
165
+ - `pollIntervalMinutes` — how often to check for new emails (default: 5, range: 1–60)
166
+ - `hourlyLimit` — maximum auto-replies per clock hour UTC (default: 20, range: 1–1000)
167
+ - `dailyLimit` — maximum auto-replies per calendar day UTC (default: 100, range: 1–10000)
168
+
169
+ ### Behaviour
170
+
171
+ - The cron job runs every minute. Each account's poll is skipped if the configured interval hasn't elapsed since the last poll.
172
+ - Only emails addressed TO the agent's email address are processed (alias filtering applies).
173
+ - Auto-replies, mailing list messages, and emails from the agent's own address are automatically skipped (RFC 3834 loop prevention).
174
+ - Outgoing replies include `In-Reply-To` and `References` headers for correct threading, and `Auto-Submitted: auto-replied` to prevent loops with other auto-responders.
175
+ - The subject line carries a single `Re:` prefix (no doubling).
176
+
177
+ ### Rate Limiting
178
+
179
+ Per-account send caps prevent runaway auto-replies from spam waves, mailing list explosions, or deliberate attacks. Two independent limits:
180
+
181
+ - **Hourly limit** (default 20) — resets at each UTC clock-hour boundary
182
+ - **Daily limit** (default 100) — resets at midnight UTC
183
+
184
+ When either limit is reached, remaining emails in the poll cycle are skipped (not replied to). Auto-respond is **not** disabled — counters reset naturally at the next boundary. A medium-priority admin Task is created to notify the admin.
185
+
186
+ Within a single poll cycle, only one auto-reply is sent per unique sender address. This prevents reply storms when an automated sender delivers multiple messages. Sender deduplication is case-insensitive.
187
+
188
+ ### Circuit Breaker
189
+
190
+ After 3 consecutive failures (IMAP errors, API failures, SMTP rejections), auto-respond is automatically disabled and a high-priority Task is created for the admin. The admin sees this task at the start of their next session and can re-enable auto-respond after resolving the issue.
191
+
192
+ ### Disabling
193
+
194
+ Call `email-auto-respond-config` with `enabled: false`. This clears the agent assignment and resets the failure counter.
195
+
196
+ ## OTP Integration
197
+
198
+ Other skills call `email-otp-extract` during service authentication (Anthropic OAuth, Cloudflare setup) with:
199
+ - `sender` — expected sender domain
200
+ - `subject_pattern` — optional regex for subject line
201
+ - `timeout` — how long to poll (default 60s)
202
+
203
+ The tool polls at short intervals, extracts the verification code, and returns it. On timeout, it returns a clear failure so the calling skill can ask the user to check manually.
@@ -127,14 +127,25 @@ server.tool("whatsapp-send", "Send a WhatsApp message to a phone number or group
127
127
  }
128
128
  });
129
129
  // ─── whatsapp-config ──────────────────────────────────────────────────
130
- server.tool("whatsapp-config", "Manage WhatsApp configuration. Actions: add-admin-phone (register a phone as admin — messages from it route to admin agent), remove-admin-phone (demote to public routing), list-admin-phones (show current admin phones), set-public-agent (set which agent handles WhatsApp messages from non-admin phones — requires the agent slug), get-public-agent (show the currently configured public agent), schema (return all config field definitions — names, types, defaults, descriptions, constraints), list-groups (return WhatsApp groups the account belongs to — names, JIDs, participant counts). Phone numbers must be E.164 format (e.g. +441234567890).", {
131
- action: z.enum(["add-admin-phone", "remove-admin-phone", "list-admin-phones", "set-public-agent", "get-public-agent", "schema", "list-groups"]).describe("The config action to perform"),
130
+ server.tool("whatsapp-config", "Manage WhatsApp configuration. Actions: add-admin-phone (register a phone as admin — messages from it route to admin agent), remove-admin-phone (demote to public routing), list-admin-phones (show current admin phones), set-public-agent (set which agent handles WhatsApp messages from non-admin phones — requires the agent slug), get-public-agent (show the currently configured public agent), update-config (change any WhatsApp config field — pass field names and values in the fields parameter), get-config (return the full current WhatsApp config object), schema (return all config field definitions — names, types, defaults, descriptions, constraints), list-groups (return WhatsApp groups the account belongs to — names, JIDs, participant counts). Phone numbers must be E.164 format (e.g. +441234567890).", {
131
+ action: z.enum(["add-admin-phone", "remove-admin-phone", "list-admin-phones", "set-public-agent", "get-public-agent", "update-config", "get-config", "schema", "list-groups"]).describe("The config action to perform"),
132
132
  phone: z.string().optional().describe("Phone number in E.164 format (required for add/remove admin phone)"),
133
133
  slug: z.string().optional().describe("Agent slug (required for set-public-agent)"),
134
+ fields: z.string().optional().describe('JSON string of config fields to update (required for update-config, e.g. \'{"dmPolicy":"open","allowFrom":["*"]}\')'),
134
135
  accountId: z.string().optional().describe('Account ID for list-groups (default: "default")'),
135
- }, async ({ action, phone, slug, accountId }) => {
136
+ }, async ({ action, phone, slug, fields, accountId }) => {
136
137
  try {
137
- const result = await callApi("/api/whatsapp/config", "POST", { action, phone, slug, accountId });
138
+ // Parse fields JSON string for update-config
139
+ let parsedFields;
140
+ if (action === "update-config" && fields) {
141
+ try {
142
+ parsedFields = JSON.parse(fields);
143
+ }
144
+ catch {
145
+ return textResult('Invalid JSON in "fields" parameter. Expected a JSON object, e.g. {"dmPolicy":"open","allowFrom":["*"]}', true);
146
+ }
147
+ }
148
+ const result = await callApi("/api/whatsapp/config", "POST", { action, phone, slug, fields: parsedFields, accountId });
138
149
  if (!result.ok)
139
150
  return textResult(result.error ?? "Config operation failed.", true);
140
151
  if (action === "list-admin-phones") {
@@ -152,6 +163,12 @@ server.tool("whatsapp-config", "Manage WhatsApp configuration. Actions: add-admi
152
163
  if (action === "schema") {
153
164
  return textResult(result.text ?? "Schema unavailable.");
154
165
  }
166
+ if (action === "get-config") {
167
+ const waConfig = result.config;
168
+ if (!waConfig)
169
+ return textResult("No WhatsApp config found. Connect WhatsApp first.");
170
+ return textResult(JSON.stringify(waConfig, null, 2));
171
+ }
155
172
  if (action === "list-groups") {
156
173
  const groups = result.groups ?? [];
157
174
  if (groups.length === 0)
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -30,7 +30,7 @@ The initial first-run wizard flow stays single-account. Multi-account is managed
30
30
  ```yaml
31
31
  channels:
32
32
  whatsapp:
33
- allowFrom:
33
+ adminPhones:
34
34
  - "<admin-phone>" # E.164 phone the user messages from
35
35
  accounts:
36
36
  default:
@@ -88,14 +88,14 @@ The platform enforces this at multiple levels:
88
88
  | Term | Definition | Code |
89
89
  |------|-----------|------|
90
90
  | **Self phone** | The WhatsApp account's own phone number (the phone that scanned the QR code). Self-chat only. | `phonesMatch(senderPhone, selfPhone)` in `access-control.ts` |
91
- | **Admin phones** | Phones with explicit non-wildcard entries in `allowFrom` in `account.json`. The user's personal phone must be registered here during setup it is not auto-registered on QR link. | `isAdminPhone()` in `access-control.ts` — iterates `allowFrom`, skips `"*"` entries |
92
- | **Public/unknown** | Any phone that is not self and not in `allowFrom` (non-wildcard). Subject to DM policy gating. | Everything else in `checkDmAccess()` |
91
+ | **Admin phones** | Phones listed in `adminPhones` in `account.json`. The user's personal phone is auto-registered on QR link; additional phones are added via `whatsapp-config action: "add-admin-phone"`. | `isAdminPhone()` in `access-control.ts` — iterates `adminPhones` |
92
+ | **Public/unknown** | Any phone that is not self and not in `adminPhones`. Subject to DM policy gating. | Everything else in `checkDmAccess()` |
93
93
 
94
- **Critical distinction:** The *self phone* is the paired device's number (the WhatsApp account Maxy controls). The *admin phone* is the user's personal phone (the phone they message *from*). These are typically different numbers. If the user's personal phone is not in `allowFrom`, their messages route as public — not admin.
94
+ **Critical distinction:** The *self phone* is the paired device's number (the WhatsApp account Maxy controls). The *admin phone* is the user's personal phone (the phone they message *from*). These are typically different numbers. If the user's personal phone is not in `adminPhones`, their messages route as public — not admin.
95
95
 
96
96
  **`dmPolicy` behaviour (per-account):**
97
97
 
98
- | Policy | Self phone | Admin phones (in `allowFrom`) | Public/unknown |
98
+ | Policy | Self phone | Admin phones (in `adminPhones`) | Public/unknown |
99
99
  |--------|-----------|-------------------------------|----------------|
100
100
  | `"open"` | Allowed | Allowed | Allowed |
101
101
  | `"allowlist"` | Allowed | Allowed | Allowed only if in `allowFrom` (wildcard `"*"` counts) |
@@ -107,7 +107,7 @@ Admin phones always bypass DM policy — the `isAdminPhone()` check runs before
107
107
 
108
108
  **Key file:** `access-control.ts` — `checkDmAccess()` and `checkGroupAccess()`.
109
109
 
110
- **Config path:** Per-account at `whatsapp.accounts.{accountId}.dmPolicy` in `account.json`. Falls back to `whatsapp.dmPolicy`. Changes are conversational — the admin agent writes to config via the `account-update` tool.
110
+ **Config path:** Per-account at `whatsapp.accounts.{accountId}.dmPolicy` in `account.json`. Falls back to `whatsapp.dmPolicy`. Changes are conversational — the admin agent writes to config via `whatsapp-config action: "update-config"`.
111
111
 
112
112
  For config field definitions (field names, types, valid values), use `whatsapp-config action: schema`.
113
113
 
@@ -47,7 +47,7 @@ When the user wants to configure per-group settings:
47
47
 
48
48
  ## Writing changes
49
49
 
50
- After the user submits, write changes via `account-update` with `field: "whatsapp"` and the updated config object. Confirm what changed — name each setting that was modified and its new value.
50
+ After the user submits, write changes via `whatsapp-config action: "update-config"` with the `fields` parameter containing a JSON object of the changed fields. Each field is merged individually — unchanged fields are left untouched. Confirm what changed — name each setting that was modified and its new value.
51
51
 
52
52
  ## Language
53
53
 
@@ -75,6 +75,10 @@ Any request to create, edit, clone, list, preview, or delete a public agent must
75
75
 
76
76
  Plugin installation, removal, and account settings changes are managed via conversation. Load the relevant skill via `plugin-read` (find its path in the manifest under `admin`).
77
77
 
78
+ ## WhatsApp Configuration
79
+
80
+ When the user asks about WhatsApp settings, config, policies, or operational limits, load the manage-whatsapp-config skill via `plugin-read` (find its path in the manifest under `whatsapp`). The skill guides presenting config as an interactive form, not a text dump.
81
+
78
82
  ## Session Reset
79
83
 
80
84
  When the user asks to start a new session, clear the conversation, or start fresh, call `session-reset`. This compacts the current conversation to memory and clears the chat. Do not ask for confirmation. After calling the tool, do not say anything further — the UI clears and returns to idle.