@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 +1 -1
- package/payload/maxy/server.js +167 -65
- package/payload/platform/plugins/email/PLUGIN.md +19 -193
- package/payload/platform/plugins/email/references/email-reference.md +203 -0
- package/payload/platform/plugins/whatsapp/mcp/dist/index.js +21 -4
- package/payload/platform/plugins/whatsapp/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +6 -6
- package/payload/platform/plugins/whatsapp/skills/manage-whatsapp-config/SKILL.md +1 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +4 -0
package/package.json
CHANGED
package/payload/maxy/server.js
CHANGED
|
@@ -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
|
|
2856
|
-
import { homedir as
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
23863
|
+
function isAdminPhone(phone, adminPhones) {
|
|
23842
23864
|
const normalized = normalizeE164(phone);
|
|
23843
23865
|
if (!normalized) return false;
|
|
23844
|
-
return
|
|
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.
|
|
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.
|
|
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
|
|
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 =
|
|
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
|
|
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.
|
|
24624
|
-
wa.
|
|
24658
|
+
if (!Array.isArray(wa.adminPhones)) {
|
|
24659
|
+
wa.adminPhones = [];
|
|
24625
24660
|
}
|
|
24626
|
-
const
|
|
24627
|
-
if (!
|
|
24628
|
-
|
|
24629
|
-
console.error(`${TAG8} added selfPhone=${normalized} to
|
|
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
|
|
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.
|
|
24663
|
-
wa.
|
|
24697
|
+
if (!Array.isArray(wa.adminPhones)) {
|
|
24698
|
+
wa.adminPhones = [];
|
|
24664
24699
|
}
|
|
24665
|
-
const
|
|
24666
|
-
if (
|
|
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
|
-
|
|
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.
|
|
24729
|
+
if (!Array.isArray(wa.adminPhones)) {
|
|
24695
24730
|
return { ok: true, message: `No admin phones configured \u2014 nothing to remove.` };
|
|
24696
24731
|
}
|
|
24697
|
-
const
|
|
24698
|
-
const idx =
|
|
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
|
-
|
|
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
|
|
24724
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 ?
|
|
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 =
|
|
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 =
|
|
26541
|
+
const configDir2 = join9(homedir3(), BRAND.configDir);
|
|
26440
26542
|
try {
|
|
26441
|
-
const accountJsonPath =
|
|
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 =
|
|
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 =
|
|
26456
|
-
const accountJsonPath =
|
|
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 ??
|
|
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
|
-
##
|
|
23
|
+
## Capabilities
|
|
24
24
|
|
|
25
|
-
|
|
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
|
-
|
|
35
|
+
## Retrieval paths
|
|
28
36
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
41
|
+
## References
|
|
42
42
|
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
92
|
-
| **Public/unknown** | Any phone that is not self and not in `
|
|
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 `
|
|
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 `
|
|
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
|
|
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 `
|
|
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.
|