@rubytech/create-maxy 1.0.457 → 1.0.459

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.457",
3
+ "version": "1.0.459",
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 join7, 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);
@@ -23636,7 +23655,8 @@ var WhatsAppAccountSchema = external_exports.object({
23636
23655
  direct: external_exports.boolean().optional().default(true),
23637
23656
  group: external_exports.enum(["always", "mentions", "never"]).optional().default("mentions")
23638
23657
  }).strict().optional().describe("React to incoming messages with an emoji to acknowledge receipt before the agent responds."),
23639
- debounceMs: external_exports.number().int().nonnegative().optional().default(0).describe("Wait this many milliseconds after the last message before processing, to batch rapid multi-message input into a single agent turn. 0 means process each message immediately.")
23658
+ debounceMs: external_exports.number().int().nonnegative().optional().default(0).describe("Wait this many milliseconds after the last message before processing, to batch rapid multi-message input into a single agent turn. 0 means process each message immediately."),
23659
+ publicAgent: external_exports.string().optional().describe("Slug of the public agent that handles WhatsApp inbound from non-admin phones for this account. Overrides the top-level publicAgent when set.")
23640
23660
  }).strict().superRefine((value, ctx) => {
23641
23661
  if (value.dmPolicy !== "open") return;
23642
23662
  const allow = (value.allowFrom ?? []).map((v) => String(v).trim()).filter(Boolean);
@@ -23656,7 +23676,8 @@ var WhatsAppConfigSchema = external_exports.object({
23656
23676
  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."),
23657
23677
  mediaMaxMb: external_exports.number().int().positive().optional().default(50).describe("Maximum file size in MB for media the agent will download and process."),
23658
23678
  textChunkLimit: external_exports.number().int().positive().optional().default(4e3).describe("Maximum characters per outbound message. Longer replies are split into multiple messages."),
23659
- debounceMs: external_exports.number().int().nonnegative().optional().default(0).describe("Wait this many milliseconds after the last message before processing, to batch rapid multi-message input into a single agent turn. 0 means process each message immediately.")
23679
+ debounceMs: external_exports.number().int().nonnegative().optional().default(0).describe("Wait this many milliseconds after the last message before processing, to batch rapid multi-message input into a single agent turn. 0 means process each message immediately."),
23680
+ publicAgent: external_exports.string().optional().describe("Slug of the public agent that handles WhatsApp inbound from non-admin phones. Per-account overrides take precedence.")
23660
23681
  }).strict().superRefine((value, ctx) => {
23661
23682
  if (value.dmPolicy !== "open") return;
23662
23683
  const allow = (value.allowFrom ?? []).map((v) => String(v).trim()).filter(Boolean);
@@ -23994,7 +24015,7 @@ async function sendReadReceipt(sock, chatJid, messageIds, participant) {
23994
24015
  // app/lib/whatsapp/inbound/media.ts
23995
24016
  import { randomUUID as randomUUID6 } from "crypto";
23996
24017
  import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
23997
- import { join as join4 } from "path";
24018
+ import { join as join5 } from "path";
23998
24019
  import {
23999
24020
  downloadMediaMessage,
24000
24021
  downloadContentFromMessage,
@@ -24080,7 +24101,7 @@ async function downloadInboundMedia(msg, sock, opts) {
24080
24101
  await mkdir2(MEDIA_DIR, { recursive: true });
24081
24102
  const ext = mimeToExt(mimetype ?? "application/octet-stream");
24082
24103
  const filename = `${randomUUID6()}.${ext}`;
24083
- const filePath = join4(MEDIA_DIR, filename);
24104
+ const filePath = join5(MEDIA_DIR, filename);
24084
24105
  await writeFile2(filePath, buffer);
24085
24106
  const sizeKB = (buffer.length / 1024).toFixed(0);
24086
24107
  console.error(`${TAG5} media downloaded type=${mimetype ?? "unknown"} size=${sizeKB}KB path=${filePath}`);
@@ -24578,7 +24599,7 @@ async function handleInboundMessage(conn, msg) {
24578
24599
 
24579
24600
  // app/lib/whatsapp/config-persist.ts
24580
24601
  import { readFileSync as readFileSync10, writeFileSync as writeFileSync6, existsSync as existsSync10 } from "fs";
24581
- import { resolve as resolve9 } from "path";
24602
+ import { resolve as resolve9, join as join6 } from "path";
24582
24603
  var TAG8 = "[whatsapp:config]";
24583
24604
  function configPath(accountDir) {
24584
24605
  return resolve9(accountDir, "account.json");
@@ -24724,6 +24745,52 @@ function readAdminPhones(accountDir) {
24724
24745
  return [];
24725
24746
  }
24726
24747
  }
24748
+ function setPublicAgent(accountDir, slug) {
24749
+ const trimmed = slug.trim();
24750
+ if (!trimmed) {
24751
+ return { ok: false, error: "Agent slug cannot be empty." };
24752
+ }
24753
+ const agentConfigPath = join6(accountDir, "agents", trimmed, "config.json");
24754
+ if (!existsSync10(agentConfigPath)) {
24755
+ return { ok: false, error: `Agent "${trimmed}" not found \u2014 no config.json at ${agentConfigPath}. Check the agent slug and try again.` };
24756
+ }
24757
+ try {
24758
+ const config2 = readConfig(accountDir);
24759
+ if (!config2.whatsapp || typeof config2.whatsapp !== "object") {
24760
+ config2.whatsapp = {};
24761
+ }
24762
+ const wa = config2.whatsapp;
24763
+ wa.publicAgent = trimmed;
24764
+ const parsed = WhatsAppConfigSchema.safeParse(wa);
24765
+ if (!parsed.success) {
24766
+ const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
24767
+ return { ok: false, error: `Validation failed: ${msg}` };
24768
+ }
24769
+ config2.whatsapp = parsed.data;
24770
+ writeConfig(accountDir, config2);
24771
+ console.error(`${TAG8} publicAgent set to ${trimmed}`);
24772
+ reloadManagerConfig(accountDir);
24773
+ return { ok: true, message: `Public agent set to "${trimmed}". WhatsApp messages from non-admin phones will be handled by this agent.` };
24774
+ } catch (err) {
24775
+ const msg = err instanceof Error ? err.message : String(err);
24776
+ console.error(`${TAG8} setPublicAgent failed: ${msg}`);
24777
+ return { ok: false, error: msg };
24778
+ }
24779
+ }
24780
+ function getPublicAgent(accountDir) {
24781
+ try {
24782
+ const config2 = readConfig(accountDir);
24783
+ const wa = config2.whatsapp;
24784
+ if (!wa) return null;
24785
+ const slug = wa.publicAgent;
24786
+ if (typeof slug === "string" && slug.trim()) {
24787
+ return slug.trim();
24788
+ }
24789
+ return null;
24790
+ } catch {
24791
+ return null;
24792
+ }
24793
+ }
24727
24794
 
24728
24795
  // app/api/whatsapp/login/wait/route.ts
24729
24796
  async function POST10(req) {
@@ -24920,7 +24987,7 @@ function serializeWhatsAppSchema() {
24920
24987
  async function POST14(req) {
24921
24988
  try {
24922
24989
  const body = await req.json().catch(() => ({}));
24923
- const { action, phone, accountId } = body;
24990
+ const { action, phone, slug, accountId } = body;
24924
24991
  if (!action || typeof action !== "string") {
24925
24992
  return Response.json({ ok: false, error: 'Missing required field "action".' }, { status: 400 });
24926
24993
  }
@@ -24950,6 +25017,19 @@ async function POST14(req) {
24950
25017
  console.error(`[whatsapp:api] config action=list-admin-phones count=${phones.length}`);
24951
25018
  return Response.json({ ok: true, phones });
24952
25019
  }
25020
+ case "set-public-agent": {
25021
+ if (!slug || typeof slug !== "string") {
25022
+ return Response.json({ ok: false, error: 'Missing required field "slug" (the agent directory name, e.g. "my-agent").' }, { status: 400 });
25023
+ }
25024
+ const result = setPublicAgent(account.accountDir, slug);
25025
+ console.error(`[whatsapp:api] config action=set-public-agent slug=${slug} ok=${result.ok}`);
25026
+ return Response.json(result, { status: result.ok ? 200 : 400 });
25027
+ }
25028
+ case "get-public-agent": {
25029
+ const currentSlug = getPublicAgent(account.accountDir);
25030
+ console.error(`[whatsapp:api] config action=get-public-agent slug=${currentSlug ?? "none"}`);
25031
+ return Response.json({ ok: true, slug: currentSlug });
25032
+ }
24953
25033
  case "schema": {
24954
25034
  const text = serializeWhatsAppSchema();
24955
25035
  console.error(`[whatsapp:api] config action=schema`);
@@ -24978,7 +25058,7 @@ async function POST14(req) {
24978
25058
  }
24979
25059
  default:
24980
25060
  return Response.json(
24981
- { ok: false, error: `Unknown action "${action}". Valid actions: add-admin-phone, remove-admin-phone, list-admin-phones, schema, list-groups.` },
25061
+ { 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.` },
24982
25062
  { status: 400 }
24983
25063
  );
24984
25064
  }
@@ -25739,11 +25819,11 @@ async function GET7() {
25739
25819
 
25740
25820
  // app/api/admin/version/route.ts
25741
25821
  import { readFileSync as readFileSync17, existsSync as existsSync17 } from "fs";
25742
- import { resolve as resolve14, join as join5 } from "path";
25822
+ import { resolve as resolve14, join as join7 } from "path";
25743
25823
  var PLATFORM_ROOT6 = process.env.MAXY_PLATFORM_ROOT ?? resolve14(process.cwd(), "..");
25744
25824
  var brandHostname = "maxy";
25745
25825
  var brandNpmPackage = "@rubytech/create-maxy";
25746
- var brandJsonPath = join5(PLATFORM_ROOT6, "config", "brand.json");
25826
+ var brandJsonPath = join7(PLATFORM_ROOT6, "config", "brand.json");
25747
25827
  if (existsSync17(brandJsonPath)) {
25748
25828
  try {
25749
25829
  const brand = JSON.parse(readFileSync17(brandJsonPath, "utf-8"));
@@ -25815,11 +25895,11 @@ async function GET8() {
25815
25895
  // app/api/admin/version/upgrade/route.ts
25816
25896
  import { spawn as spawn4 } from "child_process";
25817
25897
  import { existsSync as existsSync18, statSync as statSync4, writeFileSync as writeFileSync11, readFileSync as readFileSync18, openSync as openSync2, closeSync as closeSync2 } from "fs";
25818
- import { resolve as resolve15, join as join6 } from "path";
25898
+ import { resolve as resolve15, join as join8 } from "path";
25819
25899
  var PLATFORM_ROOT7 = process.env.MAXY_PLATFORM_ROOT ?? resolve15(process.cwd(), "..");
25820
25900
  var upgradePkg = "@rubytech/create-maxy";
25821
25901
  var upgradeHostname = "maxy";
25822
- var brandPath = join6(PLATFORM_ROOT7, "config", "brand.json");
25902
+ var brandPath = join8(PLATFORM_ROOT7, "config", "brand.json");
25823
25903
  if (existsSync18(brandPath)) {
25824
25904
  try {
25825
25905
  const brand = JSON.parse(readFileSync18(brandPath, "utf-8"));
@@ -25986,7 +26066,7 @@ async function POST21() {
25986
26066
 
25987
26067
  // server/index.ts
25988
26068
  var PLATFORM_ROOT8 = process.env.MAXY_PLATFORM_ROOT || "";
25989
- var BRAND_JSON_PATH = PLATFORM_ROOT8 ? join7(PLATFORM_ROOT8, "config", "brand.json") : "";
26069
+ var BRAND_JSON_PATH = PLATFORM_ROOT8 ? join9(PLATFORM_ROOT8, "config", "brand.json") : "";
25990
26070
  var BRAND = { productName: "Maxy", hostname: "maxy", configDir: ".maxy", domain: "getmaxy.com" };
25991
26071
  if (BRAND_JSON_PATH && !existsSync19(BRAND_JSON_PATH)) {
25992
26072
  console.error(`[brand] WARNING: brand.json not found at ${BRAND_JSON_PATH} \u2014 using Maxy defaults`);
@@ -26005,7 +26085,7 @@ var brandLoginOpts = {
26005
26085
  primaryHoverColor: BRAND.defaultColors?.primaryHover,
26006
26086
  primarySubtle: BRAND.defaultColors?.primarySubtle
26007
26087
  };
26008
- var ALIAS_DOMAINS_PATH = join7(homedir2(), BRAND.configDir, "alias-domains.json");
26088
+ var ALIAS_DOMAINS_PATH = join9(homedir3(), BRAND.configDir, "alias-domains.json");
26009
26089
  function loadAliasDomains() {
26010
26090
  try {
26011
26091
  if (!existsSync19(ALIAS_DOMAINS_PATH)) return null;
@@ -26375,14 +26455,14 @@ function cachedHtml(file2) {
26375
26455
  }
26376
26456
  var brandedHtmlCache = /* @__PURE__ */ new Map();
26377
26457
  function loadBrandingCache(agentSlug) {
26378
- const configDir2 = join7(homedir2(), BRAND.configDir);
26458
+ const configDir2 = join9(homedir3(), BRAND.configDir);
26379
26459
  try {
26380
- const accountJsonPath = join7(configDir2, "account.json");
26460
+ const accountJsonPath = join9(configDir2, "account.json");
26381
26461
  if (!existsSync19(accountJsonPath)) return null;
26382
26462
  const account = JSON.parse(readFileSync19(accountJsonPath, "utf-8"));
26383
26463
  const accountId = account.accountId;
26384
26464
  if (!accountId) return null;
26385
- const cachePath = join7(configDir2, "branding-cache", accountId, `${agentSlug}.json`);
26465
+ const cachePath = join9(configDir2, "branding-cache", accountId, `${agentSlug}.json`);
26386
26466
  if (!existsSync19(cachePath)) return null;
26387
26467
  return JSON.parse(readFileSync19(cachePath, "utf-8"));
26388
26468
  } catch {
@@ -26391,8 +26471,8 @@ function loadBrandingCache(agentSlug) {
26391
26471
  }
26392
26472
  function resolveDefaultSlug() {
26393
26473
  try {
26394
- const configDir2 = join7(homedir2(), BRAND.configDir);
26395
- const accountJsonPath = join7(configDir2, "account.json");
26474
+ const configDir2 = join9(homedir3(), BRAND.configDir);
26475
+ const accountJsonPath = join9(configDir2, "account.json");
26396
26476
  if (!existsSync19(accountJsonPath)) return null;
26397
26477
  const account = JSON.parse(readFileSync19(accountJsonPath, "utf-8"));
26398
26478
  return account.defaultAgent || null;
@@ -26463,17 +26543,29 @@ console.log(`${BRAND.productName} listening on http://${hostname3}:${port}`);
26463
26543
  var configDirForWhatsApp = basename2(MAXY_DIR) || ".maxy";
26464
26544
  var bootAccount = resolveAccount();
26465
26545
  var bootAccountConfig = bootAccount?.config;
26546
+ var bootPublicAgent = bootAccount ? getPublicAgent(bootAccount.accountDir) : null;
26466
26547
  if (bootAccountConfig?.whatsapp) {
26467
- console.error("[whatsapp:boot] loading whatsapp config from account.json");
26548
+ console.error(`[whatsapp:boot] loading whatsapp config from account.json publicAgent=${bootPublicAgent ?? "none"}`);
26468
26549
  } else {
26469
26550
  console.error("[whatsapp:boot] no whatsapp config in account.json \u2014 routing policies will use defaults");
26470
26551
  }
26471
26552
  init({
26472
26553
  configDir: configDirForWhatsApp,
26473
- platformRoot: resolve16(process.env.MAXY_PLATFORM_ROOT ?? join7(__dirname, "..")),
26554
+ platformRoot: resolve16(process.env.MAXY_PLATFORM_ROOT ?? join9(__dirname, "..")),
26474
26555
  accountConfig: bootAccountConfig,
26475
26556
  onMessage: async (msg) => {
26476
26557
  try {
26558
+ let agentName;
26559
+ if (msg.agentType === "admin") {
26560
+ agentName = "admin";
26561
+ } else {
26562
+ const publicAgentSlug = bootAccount ? getPublicAgent(bootAccount.accountDir) : null;
26563
+ if (!publicAgentSlug) {
26564
+ console.error(`[whatsapp:route] rejected: no publicAgent configured account=${msg.accountId} from=${msg.senderPhone}`);
26565
+ return;
26566
+ }
26567
+ agentName = publicAgentSlug;
26568
+ }
26477
26569
  const parts = [];
26478
26570
  if (msg.replyContext) {
26479
26571
  const quotedText = msg.replyContext.text ? `: "${msg.replyContext.text}"` : "";
@@ -26492,7 +26584,7 @@ init({
26492
26584
  if (msg.isOwnerMirror) {
26493
26585
  let responseText2 = "";
26494
26586
  for await (const event of invokeAgent(
26495
- { type: msg.agentType, agentName: msg.agentType },
26587
+ { type: msg.agentType, agentName },
26496
26588
  enrichedText,
26497
26589
  msg.sessionKey
26498
26590
  )) {
@@ -26507,7 +26599,7 @@ init({
26507
26599
  }
26508
26600
  let responseText = "";
26509
26601
  for await (const event of invokeAgent(
26510
- { type: msg.agentType, agentName: msg.agentType },
26602
+ { type: msg.agentType, agentName },
26511
26603
  enrichedText,
26512
26604
  msg.sessionKey
26513
26605
  )) {
@@ -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,13 +127,14 @@ 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), 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", "schema", "list-groups"]).describe("The config action to perform"),
132
- phone: z.string().optional().describe("Phone number in E.164 format (required for add/remove)"),
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"),
132
+ phone: z.string().optional().describe("Phone number in E.164 format (required for add/remove admin phone)"),
133
+ slug: z.string().optional().describe("Agent slug (required for set-public-agent)"),
133
134
  accountId: z.string().optional().describe('Account ID for list-groups (default: "default")'),
134
- }, async ({ action, phone, accountId }) => {
135
+ }, async ({ action, phone, slug, accountId }) => {
135
136
  try {
136
- const result = await callApi("/api/whatsapp/config", "POST", { action, phone, accountId });
137
+ const result = await callApi("/api/whatsapp/config", "POST", { action, phone, slug, accountId });
137
138
  if (!result.ok)
138
139
  return textResult(result.error ?? "Config operation failed.", true);
139
140
  if (action === "list-admin-phones") {
@@ -142,6 +143,12 @@ server.tool("whatsapp-config", "Manage WhatsApp configuration. Actions: add-admi
142
143
  return textResult("No admin phones configured. Use add-admin-phone to register one.");
143
144
  return textResult(`Admin phones:\n${phones.map((p) => ` ${p}`).join("\n")}`);
144
145
  }
146
+ if (action === "get-public-agent") {
147
+ const agentSlug = result.slug;
148
+ if (!agentSlug)
149
+ return textResult("No public agent configured for WhatsApp. Use set-public-agent to assign one.");
150
+ return textResult(`WhatsApp public agent: ${agentSlug}`);
151
+ }
145
152
  if (action === "schema") {
146
153
  return textResult(result.text ?? "Schema unavailable.");
147
154
  }
@@ -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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"}
@@ -43,17 +43,17 @@ The self phone from Phase 1 is automatically registered as an admin phone. If th
43
43
 
44
44
  ### Phase 3: Public Agent Selection
45
45
 
46
- WhatsApp inbound from non-admin phones routes to a public agent. The user must explicitly choose which one there is no automatic fallback.
46
+ WhatsApp inbound from non-admin phones routes to a public agent configured in the WhatsApp config — not the account-level `defaultAgent`. The user must explicitly choose which agent handles WhatsApp. There is no automatic fallback; if no public agent is set, WhatsApp silently rejects non-admin messages.
47
47
 
48
- 1. **List available agents** — Read the current config via `account-manage` to find the current `defaultAgent` setting. List the available public agents so the user can see their options.
48
+ 1. **Check current state** — Call `whatsapp-config` with `action: "get-public-agent"` to see if one is already set. Also call `account-manage` to list the available public agents.
49
49
 
50
50
  2. **Ask which agent should handle WhatsApp** — "Which of your public agents should respond to WhatsApp messages from customers?"
51
51
 
52
- 3. **Set the default** — Call `account-update` with `field: "defaultAgent"` and `value: "{chosen-slug}"`.
52
+ 3. **Set the public agent** — Call `whatsapp-config` with `action: "set-public-agent"` and `slug: "{chosen-slug}"`.
53
53
 
54
54
  4. **Confirm** — "WhatsApp messages from customers will be handled by {agent name}."
55
55
 
56
- If only one public agent exists, confirm it as the default rather than asking. If no public agents exist, tell the user: "You don't have a public agent yet. WhatsApp will be connected but won't respond to customer messages until you create one."
56
+ If only one public agent exists, confirm it as the choice rather than asking. If no public agents exist, tell the user: "You don't have a public agent yet. WhatsApp will be connected but won't respond to customer messages until you create one and set it with `whatsapp-config action: set-public-agent`."
57
57
 
58
58
  ## Relinking
59
59
 
@@ -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.