@rubytech/create-maxy 1.0.439 → 1.0.441
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/dist/index.js +1 -1
- package/package.json +1 -1
- package/payload/maxy/package.json +2 -1
- package/payload/maxy/server.js +15553 -542
- package/payload/platform/plugins/whatsapp/PLUGIN.md +36 -0
- package/payload/platform/plugins/whatsapp/mcp/dist/index.d.ts +8 -0
- package/payload/platform/plugins/whatsapp/mcp/dist/index.d.ts.map +1 -0
- package/payload/platform/plugins/whatsapp/mcp/dist/index.js +128 -0
- package/payload/platform/plugins/whatsapp/mcp/dist/index.js.map +1 -0
- package/payload/platform/plugins/whatsapp/mcp/package.json +19 -0
- package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +329 -0
- package/payload/platform/plugins/whatsapp/skills/connect-whatsapp/SKILL.md +47 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: whatsapp
|
|
3
|
+
description: WhatsApp messaging channel — Baileys QR pairing, DM/group policy enforcement, inbound routing, outbound messaging.
|
|
4
|
+
tools:
|
|
5
|
+
- whatsapp-login-start
|
|
6
|
+
- whatsapp-login-wait
|
|
7
|
+
- whatsapp-status
|
|
8
|
+
- whatsapp-disconnect
|
|
9
|
+
- whatsapp-send
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# WhatsApp
|
|
13
|
+
|
|
14
|
+
Activate when the user asks about WhatsApp — connecting, linking, checking status, managing accounts, DM policies, group policies, or sending messages.
|
|
15
|
+
|
|
16
|
+
## What this unlocks
|
|
17
|
+
|
|
18
|
+
- QR-code-based WhatsApp pairing via Baileys (linked device)
|
|
19
|
+
- Multi-account support (personal + business numbers)
|
|
20
|
+
- Inbound message routing (admin phone → admin agent, public → public agent)
|
|
21
|
+
- Group messaging with mention gating and per-group activation
|
|
22
|
+
- DM policy enforcement (open, pairing, allowlist, disabled)
|
|
23
|
+
- Voice note normalization (OGG Opus via ffmpeg)
|
|
24
|
+
- Auto-reconnection with exponential backoff
|
|
25
|
+
|
|
26
|
+
## Skills
|
|
27
|
+
|
|
28
|
+
| Skill | Purpose |
|
|
29
|
+
|-------|---------|
|
|
30
|
+
| [connect-whatsapp](skills/connect-whatsapp/SKILL.md) | Conversational WhatsApp pairing — QR generation, scanning, status confirmation |
|
|
31
|
+
|
|
32
|
+
## References
|
|
33
|
+
|
|
34
|
+
| Reference | Topics |
|
|
35
|
+
|-----------|--------|
|
|
36
|
+
| [channels-whatsapp.md](references/channels-whatsapp.md) | Architecture, DM/group policies, reconnection, voice notes, outbound policy |
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WhatsApp MCP server — exposes WhatsApp lifecycle tools to the admin agent.
|
|
3
|
+
*
|
|
4
|
+
* Tools call the platform's Hono API routes (localhost) which manage the
|
|
5
|
+
* Baileys connection lifecycle in the server process.
|
|
6
|
+
*/
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WhatsApp MCP server — exposes WhatsApp lifecycle tools to the admin agent.
|
|
3
|
+
*
|
|
4
|
+
* Tools call the platform's Hono API routes (localhost) which manage the
|
|
5
|
+
* Baileys connection lifecycle in the server process.
|
|
6
|
+
*/
|
|
7
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
const PLATFORM_PORT = 19201;
|
|
11
|
+
const BASE_URL = `http://127.0.0.1:${PLATFORM_PORT}`;
|
|
12
|
+
const server = new McpServer({
|
|
13
|
+
name: "maxy-whatsapp",
|
|
14
|
+
version: "0.1.0",
|
|
15
|
+
});
|
|
16
|
+
async function callApi(path, method = "POST", body) {
|
|
17
|
+
const res = await fetch(`${BASE_URL}${path}`, {
|
|
18
|
+
method,
|
|
19
|
+
headers: body ? { "Content-Type": "application/json" } : undefined,
|
|
20
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
21
|
+
});
|
|
22
|
+
return res.json();
|
|
23
|
+
}
|
|
24
|
+
function textResult(text, isError = false) {
|
|
25
|
+
return {
|
|
26
|
+
content: [{ type: "text", text }],
|
|
27
|
+
...(isError ? { isError: true } : {}),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// ─── whatsapp-login-start ──────────────────────────────────────────────
|
|
31
|
+
server.tool("whatsapp-login-start", "Start WhatsApp QR code pairing. Initiates a Baileys WebSocket, generates a QR code for the user to scan with their phone. Returns QR data for rendering inline via render-component.", {
|
|
32
|
+
accountId: z.string().optional().describe('Account ID (default: "default")'),
|
|
33
|
+
force: z.boolean().optional().describe("Force re-link even if already connected"),
|
|
34
|
+
}, async ({ accountId, force }) => {
|
|
35
|
+
try {
|
|
36
|
+
const result = await callApi("/api/whatsapp/login/start", "POST", { accountId, force });
|
|
37
|
+
if (result.error)
|
|
38
|
+
return textResult(`Login start failed: ${result.error}`, true);
|
|
39
|
+
let response = result.message;
|
|
40
|
+
if (result.qrRaw) {
|
|
41
|
+
response += `\n\nQR data (render inline): ${result.qrRaw}`;
|
|
42
|
+
}
|
|
43
|
+
if (result.selfPhone) {
|
|
44
|
+
response += `\nLinked phone: ${result.selfPhone}`;
|
|
45
|
+
}
|
|
46
|
+
return textResult(response);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
return textResult(`Login start failed: ${err instanceof Error ? err.message : String(err)}`, true);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
// ─── whatsapp-login-wait ───────────────────────────────────────────────
|
|
53
|
+
server.tool("whatsapp-login-wait", "Wait for WhatsApp QR code to be scanned and connection to be established. Call after whatsapp-login-start. Polls for up to 60 seconds.", {
|
|
54
|
+
accountId: z.string().optional().describe('Account ID (default: "default")'),
|
|
55
|
+
timeoutMs: z.number().optional().describe("Max wait time in ms (default: 60000)"),
|
|
56
|
+
}, async ({ accountId, timeoutMs }) => {
|
|
57
|
+
try {
|
|
58
|
+
const result = await callApi("/api/whatsapp/login/wait", "POST", { accountId, timeoutMs });
|
|
59
|
+
if (result.error)
|
|
60
|
+
return textResult(`Login wait failed: ${result.error}`, true);
|
|
61
|
+
let response = result.message;
|
|
62
|
+
if (result.connected && result.selfPhone) {
|
|
63
|
+
response += `\nLinked phone: ${result.selfPhone}`;
|
|
64
|
+
}
|
|
65
|
+
return textResult(response);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
return textResult(`Login wait failed: ${err instanceof Error ? err.message : String(err)}`, true);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
// ─── whatsapp-status ───────────────────────────────────────────────────
|
|
72
|
+
server.tool("whatsapp-status", "Get WhatsApp connection status for all linked accounts. Shows connected/disconnected state, linked phone number, and reconnection attempts.", {}, async () => {
|
|
73
|
+
try {
|
|
74
|
+
const result = await callApi("/api/whatsapp/status", "GET");
|
|
75
|
+
if (result.error)
|
|
76
|
+
return textResult(`Status check failed: ${result.error}`, true);
|
|
77
|
+
const accounts = result.accounts ?? [];
|
|
78
|
+
if (accounts.length === 0) {
|
|
79
|
+
return textResult("No WhatsApp accounts configured. Say \"connect WhatsApp\" to get started.");
|
|
80
|
+
}
|
|
81
|
+
const lines = accounts.map((a) => {
|
|
82
|
+
const status = a.connected ? "✓ Connected" : "✗ Disconnected";
|
|
83
|
+
const phone = a.selfPhone ? ` (${a.selfPhone})` : "";
|
|
84
|
+
const name = a.accountName ? ` — ${a.accountName}` : "";
|
|
85
|
+
const error = a.lastError ? ` — ${a.lastError}` : "";
|
|
86
|
+
const retries = a.reconnectAttempts > 0 ? ` [${a.reconnectAttempts} reconnect attempts]` : "";
|
|
87
|
+
return `${a.accountId}${name}: ${status}${phone}${error}${retries}`;
|
|
88
|
+
});
|
|
89
|
+
return textResult(lines.join("\n"));
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
return textResult(`Status check failed: ${err instanceof Error ? err.message : String(err)}`, true);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
// ─── whatsapp-disconnect ───────────────────────────────────────────────
|
|
96
|
+
server.tool("whatsapp-disconnect", "Disconnect a WhatsApp account. Closes the Baileys WebSocket connection cleanly.", {
|
|
97
|
+
accountId: z.string().optional().describe('Account ID to disconnect (default: "default")'),
|
|
98
|
+
}, async ({ accountId }) => {
|
|
99
|
+
try {
|
|
100
|
+
const result = await callApi("/api/whatsapp/disconnect", "POST", { accountId });
|
|
101
|
+
if (result.error)
|
|
102
|
+
return textResult(`Disconnect failed: ${result.error}`, true);
|
|
103
|
+
return textResult(`WhatsApp account "${result.accountId}" disconnected.`);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
return textResult(`Disconnect failed: ${err instanceof Error ? err.message : String(err)}`, true);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
// ─── whatsapp-send ─────────────────────────────────────────────────────
|
|
110
|
+
server.tool("whatsapp-send", "Send a WhatsApp message to a phone number or group. The target can be an E.164 phone number or a WhatsApp group JID.", {
|
|
111
|
+
to: z.string().describe("Target phone number (E.164) or group JID"),
|
|
112
|
+
text: z.string().describe("Message text to send"),
|
|
113
|
+
accountId: z.string().optional().describe('Account to send from (default: "default")'),
|
|
114
|
+
}, async ({ to, text, accountId }) => {
|
|
115
|
+
try {
|
|
116
|
+
const result = await callApi("/api/whatsapp/send", "POST", { to, text, accountId });
|
|
117
|
+
if (result.error)
|
|
118
|
+
return textResult(`Send failed: ${result.error}`, true);
|
|
119
|
+
return textResult(`Message sent via WhatsApp${result.messageId ? ` (ID: ${result.messageId})` : ""}`);
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
return textResult(`Send failed: ${err instanceof Error ? err.message : String(err)}`, true);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
// ─── Start server ──────────────────────────────────────────────────────
|
|
126
|
+
const transport = new StdioServerTransport();
|
|
127
|
+
await server.connect(transport);
|
|
128
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@maxy/whatsapp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"start": "node dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
13
|
+
"zod": "^3.24.0"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"typescript": "^5.7.0",
|
|
17
|
+
"@types/node": "^22.0.0"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# WhatsApp Channels
|
|
2
|
+
|
|
3
|
+
Taskmaster supports two WhatsApp connection methods via Baileys multi-account:
|
|
4
|
+
|
|
5
|
+
| Provider | Use Case | Connection |
|
|
6
|
+
|----------|----------|------------|
|
|
7
|
+
| **baileys** (default) | All WhatsApp accounts — personal and business | WebSocket via QR code (linked device) |
|
|
8
|
+
| **cloud** | Enterprise — Meta's official API | Meta Graph API + webhooks |
|
|
9
|
+
|
|
10
|
+
## Primary Architecture: Multi-Account Baileys
|
|
11
|
+
|
|
12
|
+
Most users will have two Baileys accounts (two phone numbers, both linked via QR):
|
|
13
|
+
|
|
14
|
+
| Account | Purpose |
|
|
15
|
+
|---------|---------|
|
|
16
|
+
| **Personal** | Admin self-chat, groups, personal contacts |
|
|
17
|
+
| **Business** | Customer-facing DMs (can be WhatsApp Business app or regular WhatsApp) |
|
|
18
|
+
|
|
19
|
+
WhatsApp Business **app** vs personal WhatsApp is irrelevant from Taskmaster's perspective — Baileys connects to both identically via QR. The Business app just adds business profile, catalog, and labels in the phone UI.
|
|
20
|
+
|
|
21
|
+
**Linked device note:** The primary phone must come online periodically (~every 14 days) to keep the Baileys linked-device session alive. It does NOT need to be on 24/7.
|
|
22
|
+
|
|
23
|
+
## Managing Accounts via UI
|
|
24
|
+
|
|
25
|
+
The Setup page (`/setup`) supports multi-account management in its status dashboard (shown after initial setup is complete):
|
|
26
|
+
|
|
27
|
+
- **Add Account:** Click "+ Add WhatsApp Account" below the status dashboard, enter a name (e.g. "Business"), and scan the QR code to link
|
|
28
|
+
- **Relink:** Each account has its own relink button (rotate icon) that shows a per-account QR code inline
|
|
29
|
+
- **Remove:** Each account (except the last one) has a remove button (X icon) that disconnects and removes the account from config
|
|
30
|
+
- **Per-account QR:** QR codes appear inline under the specific account being linked
|
|
31
|
+
|
|
32
|
+
The initial first-run wizard flow stays single-account. Multi-account is managed from the status dashboard that appears once setup is complete.
|
|
33
|
+
|
|
34
|
+
## Multi-Account Config Example
|
|
35
|
+
|
|
36
|
+
```yaml
|
|
37
|
+
channels:
|
|
38
|
+
whatsapp:
|
|
39
|
+
allowFrom:
|
|
40
|
+
- "+447490553305" # Admin phone (routes to admin agent)
|
|
41
|
+
accounts:
|
|
42
|
+
personal:
|
|
43
|
+
provider: baileys
|
|
44
|
+
name: "Personal"
|
|
45
|
+
selfChatMode: true
|
|
46
|
+
business:
|
|
47
|
+
provider: baileys
|
|
48
|
+
name: "Business"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Both accounts are linked via QR in the Channels UI. Outbound routing (`src/web/outbound-routing.ts`) selects the right account automatically:
|
|
52
|
+
|
|
53
|
+
| Target | Account | Reason |
|
|
54
|
+
|--------|---------|--------|
|
|
55
|
+
| Admin/allowFrom phones | Personal | Admin's own number |
|
|
56
|
+
| Self-chat | Personal | Admin talking to themselves |
|
|
57
|
+
| Group messages | Personal or Business | Whichever is in the group |
|
|
58
|
+
| Customer DMs | Business (preferred), Personal (fallback) | Dedicated business number |
|
|
59
|
+
| Inbound replies | Same account that received | Closures capture the right provider |
|
|
60
|
+
|
|
61
|
+
## Cloud API (Enterprise)
|
|
62
|
+
|
|
63
|
+
Full Cloud API support is implemented for enterprise users who need:
|
|
64
|
+
- Verified green checkmark badge
|
|
65
|
+
- Message templates for proactive outreach
|
|
66
|
+
- No phone dependency at all
|
|
67
|
+
- Higher throughput limits
|
|
68
|
+
|
|
69
|
+
**Important:** Cloud API requires a **different number** from any Baileys-linked number. A single number cannot be on both — Meta's Cloud API takes exclusive control of the number (no app access, no groups).
|
|
70
|
+
|
|
71
|
+
Cloud API code lives in:
|
|
72
|
+
- `src/web/providers/cloud/` — CloudApiProvider, Graph API client, webhooks
|
|
73
|
+
- `src/web/auto-reply/cloud-monitor.ts` — Cloud API reconnection loop
|
|
74
|
+
- Channel plugin in `extensions/whatsapp/src/channel.ts` — provider-aware branching
|
|
75
|
+
|
|
76
|
+
Cloud API requirements: Meta Business Account, System User Access Token, Cloudflare Tunnel for webhooks (`src/infra/tunnel.ts`). Webhook endpoint: `/webhook/whatsapp`.
|
|
77
|
+
|
|
78
|
+
## Baileys Risk Profile
|
|
79
|
+
|
|
80
|
+
Baileys uses the same WebSocket protocol as WhatsApp Web/Desktop linked devices. Meta cannot blanket-block it without breaking their own official clients for hundreds of millions of users.
|
|
81
|
+
|
|
82
|
+
**Responsibility model:** Taskmaster and its other users are not at risk — bans are per-number, targeting individual accounts for abusive behaviour. Users should be told: *"Don't use this for bulk messaging or spam. Meta may ban your number for abusive patterns. Normal business conversations are fine."*
|
|
83
|
+
|
|
84
|
+
- Normal conversational use (replies, scheduling, quotes) = effectively zero risk
|
|
85
|
+
- Ack reactions, typing indicators, and reply delays mimic real linked-device behaviour
|
|
86
|
+
- Risk increases only with spam patterns: mass messaging, rapid-fire sends to unknown contacts, broadcasting
|
|
87
|
+
- If a user's number is banned, it's between them and Meta — other Taskmaster users are unaffected
|
|
88
|
+
- Cloud API code is available as a fallback for enterprise users who want official Meta support
|
|
89
|
+
|
|
90
|
+
## WhatsApp Outbound Policy — No Automated Broadcasts
|
|
91
|
+
|
|
92
|
+
**WhatsApp's Terms of Service explicitly prohibit automated bulk/broadcast messaging.** This applies to both Baileys and Cloud API connections. Violations result in permanent number bans with no appeal.
|
|
93
|
+
|
|
94
|
+
Taskmaster enforces this at multiple levels:
|
|
95
|
+
|
|
96
|
+
1. **Broadcast action blocked** — The `message broadcast` action (which sends to multiple targets) always excludes WhatsApp as a target channel, regardless of config. This is a hard block that cannot be overridden.
|
|
97
|
+
2. **Agent tool suppression** — When an agent session is on WhatsApp, the `broadcast` action is removed from the message tool schema entirely. Agents cannot invoke it.
|
|
98
|
+
3. **Broadcast disabled by default** — `tools.message.broadcast.enabled` defaults to `false`. Even when enabled for other channels (Discord, Telegram, Slack), WhatsApp remains blocked.
|
|
99
|
+
|
|
100
|
+
**What IS allowed on WhatsApp:**
|
|
101
|
+
- Replying to incoming messages (the core use case)
|
|
102
|
+
- Sending messages to a single known contact via the `send` action
|
|
103
|
+
- Heartbeat/scheduled messages to the admin's own number
|
|
104
|
+
- Owner outbound mirroring (detecting manual WhatsApp Web sends)
|
|
105
|
+
|
|
106
|
+
**What is NOT allowed:**
|
|
107
|
+
- Sending the same message to multiple WhatsApp contacts in a loop
|
|
108
|
+
- Any form of automated outreach to contacts who have not messaged first
|
|
109
|
+
- Using the broadcast action with WhatsApp as a target channel
|
|
110
|
+
- Building "campaign" or "blast" workflows that target WhatsApp contacts
|
|
111
|
+
|
|
112
|
+
## DM Policy & Access Control
|
|
113
|
+
|
|
114
|
+
**Precise definitions — these terms are NOT interchangeable:**
|
|
115
|
+
|
|
116
|
+
| Term | Definition | Code |
|
|
117
|
+
|------|-----------|------|
|
|
118
|
+
| **Self phone** | The WhatsApp account's own phone number (the phone that scanned the QR code). Self-chat only. | `isSamePhone = params.from === params.selfE164` |
|
|
119
|
+
| **Admin phones** | Phones with explicit peer bindings (`match.peer.kind === "dm"`, `match.peer.id === phone`) for this WhatsApp account in `cfg.bindings`. These are phones added via the Admins page or auto-paired on QR link. A phone is admin if it has a specific binding — not just because it's in `allowFrom`. | Binding lookup in `cfg.bindings` |
|
|
120
|
+
| **Paired phones** | Phones in the pairing store (`storeAllowFrom`) that completed the pairing handshake. Subset of explicitly allowed phones. | `readChannelAllowFromStore("whatsapp")` |
|
|
121
|
+
| **Public/unknown** | Any phone that is not self, not admin-bound, and not explicitly in `allowFrom` (non-wildcard). | Everything else |
|
|
122
|
+
|
|
123
|
+
**`dmPolicy` behaviour (per-account, set via Setup toggle):**
|
|
124
|
+
|
|
125
|
+
| Policy | Self phone | Admin phones (explicit binding) | Explicitly allowed (non-wildcard) | Public/unknown |
|
|
126
|
+
|--------|-----------|-------------------------------|----------------------------------|----------------|
|
|
127
|
+
| `"open"` | Allowed | Allowed | Allowed | Allowed |
|
|
128
|
+
| `"pairing"` | Allowed | Allowed | Allowed | Pairing reply sent |
|
|
129
|
+
| `"allowlist"` | Allowed | Allowed | Allowed | Blocked |
|
|
130
|
+
| `"disabled"` | Allowed | Allowed | Blocked | **Blocked** |
|
|
131
|
+
|
|
132
|
+
**Admin phones always have the same access as self phone.** An explicit peer binding (`match.peer.kind === "dm"`, `match.peer.id === phone`) bypasses dmPolicy entirely — the phone is allowed in every mode. The binding check runs once before any policy-specific logic.
|
|
133
|
+
|
|
134
|
+
The "Public facing" toggle on Setup switches between `"open"` (on) and `"disabled"` (off). When disabled, only self-phone and admin-bound phones get through. Everyone else is blocked.
|
|
135
|
+
|
|
136
|
+
**Key file:** `src/web/inbound/access-control.ts` — `checkInboundAccessControl()`.
|
|
137
|
+
|
|
138
|
+
**Config path:** Per-account at `channels.whatsapp.accounts.{accountId}.dmPolicy`. Falls back to `channels.whatsapp.dmPolicy`. The UI toggle writes to the per-account path with `skipRestart: true` (no gateway restart needed — `channels.whatsapp` prefix is `kind: "none"` in config-reload rules).
|
|
139
|
+
|
|
140
|
+
## Public Agent Settings Modal
|
|
141
|
+
|
|
142
|
+
When WhatsApp is connected, a gear icon appears inline next to the "WhatsApp" label in the Setup dashboard. Clicking it opens a "Public Agent Settings" modal containing: **Agent responds** master toggle, Model selector, and Thinking level selector. This keeps the main dashboard row clean while giving access to all WhatsApp configuration.
|
|
143
|
+
|
|
144
|
+
**Agent responds toggle** — master on/off switch controlling both DMs and groups simultaneously. When enabled, writes `dmPolicy: "open"` + `allowFrom: ["*"]` + `groupPolicy: "open"`. When disabled, writes `dmPolicy: "disabled"` + `groupPolicy: "disabled"`. Individual group activation is configured per-chat in the WhatsApp tab of the chat view. All groups default to `"off"` (no response) until explicitly configured.
|
|
145
|
+
|
|
146
|
+
## Per-Account Model Selector
|
|
147
|
+
|
|
148
|
+
The Public Agent Settings modal includes a **Model** dropdown that sets the default AI model for the selected account's public agent. This controls which model responds to customer messages on WhatsApp.
|
|
149
|
+
|
|
150
|
+
**Options:** Default (global fallback), Opus, Sonnet, Haiku — filtered against the available model catalog.
|
|
151
|
+
|
|
152
|
+
**Config path:** `agents.list[{publicAgentId}].model` — uses the existing per-agent model override mechanism. When "Default" is selected, the override is removed and the global `agents.defaults.model` applies.
|
|
153
|
+
|
|
154
|
+
**How it works:**
|
|
155
|
+
1. UI resolves the selected workspace's public agent ID (e.g. `joes-coffee-public`)
|
|
156
|
+
2. On change, writes the model to config via `writeAgentModelToConfig` (two params: agentId + model)
|
|
157
|
+
3. Config stored in `agents.list[{agentId}].model` in `~/.taskmaster/taskmaster.json`
|
|
158
|
+
4. No gateway restart needed — `agents` prefix has `kind: "none"` reload rule (read dynamically)
|
|
159
|
+
|
|
160
|
+
**Runtime model resolution — config is the single source of truth.** Every model read (LLM API call, `/status` display, chat UI initial load) calls `loadConfig()` fresh and resolves via `resolveDefaultModelForAgent({ cfg, agentId })`. There are no session-level model overrides — the model is always read from config at the point of use.
|
|
161
|
+
|
|
162
|
+
**Key files:**
|
|
163
|
+
| File | Purpose |
|
|
164
|
+
|------|---------|
|
|
165
|
+
| `ui/src/ui/views/setup.ts` | Model dropdown rendering (`renderWhatsAppModelSelector`) |
|
|
166
|
+
| `ui/src/ui/app-render.ts` | State resolution + change handler |
|
|
167
|
+
| `src/agents/agent-model-config.ts` | `writeAgentModelToConfig()` — writes per-agent model to config |
|
|
168
|
+
| `src/agents/model-selection.ts` | `resolveDefaultModelForAgent()` — runtime resolution |
|
|
169
|
+
| `src/agents/agent-scope.ts` | `resolveAgentModelPrimary()` — reads per-agent config |
|
|
170
|
+
| `src/auto-reply/reply/get-reply-run.ts` | Fresh config read before LLM call |
|
|
171
|
+
| `src/auto-reply/reply/commands-status.ts` | Fresh config read for `/status` display |
|
|
172
|
+
| `src/gateway/session-utils.ts` | `resolveSessionModelRef()` — fresh config read for chat UI |
|
|
173
|
+
|
|
174
|
+
## Per-Account Thinking Level Selector
|
|
175
|
+
|
|
176
|
+
The Public Agent Settings modal includes a **Thinking** dropdown that sets the default thinking level for the selected account's public agent. This controls how deeply the agent reasons before responding to customer messages.
|
|
177
|
+
|
|
178
|
+
**Options:** Off, Minimal, Low, Medium, High.
|
|
179
|
+
|
|
180
|
+
**Config path:** `agents.list[{publicAgentId}].thinkingLevel` — uses the per-agent thinking override mechanism (same pattern as model). When "Off" is selected, the override is removed and the global default applies.
|
|
181
|
+
|
|
182
|
+
**How it works:**
|
|
183
|
+
1. UI resolves the selected workspace's public agent ID (e.g. `joes-coffee-public`)
|
|
184
|
+
2. On change, writes the thinking level to config via `writeAgentThinkingToConfig` (two params: agentId + thinkingLevel)
|
|
185
|
+
3. Config stored in `agents.list[{agentId}].thinkingLevel` in `~/.taskmaster/taskmaster.json`
|
|
186
|
+
4. No gateway restart needed — `agents` prefix has `kind: "none"` reload rule (read dynamically)
|
|
187
|
+
|
|
188
|
+
**Runtime thinking resolution — config is the single source of truth.** Every thinking level read calls `loadConfig()` fresh and resolves via `resolveThinkingDefault({ cfg, provider, model, catalog, agentId })`. There are no session-level thinking overrides — thinking is always read from config at the point of use.
|
|
189
|
+
|
|
190
|
+
**Key files:**
|
|
191
|
+
| File | Purpose |
|
|
192
|
+
|------|---------|
|
|
193
|
+
| `ui/src/ui/views/setup.ts` | Thinking dropdown rendering (`renderWhatsAppThinkingSelector`) |
|
|
194
|
+
| `ui/src/ui/app-render.ts` | State resolution + change handler |
|
|
195
|
+
| `src/agents/agent-model-config.ts` | `writeAgentThinkingToConfig()` — writes per-agent thinking to config |
|
|
196
|
+
| `src/agents/model-selection.ts` | `resolveThinkingDefault()` — runtime resolution (checks per-agent first) |
|
|
197
|
+
| `src/agents/agent-scope.ts` | `resolveAgentThinkingLevel()` — reads per-agent config |
|
|
198
|
+
|
|
199
|
+
## Owner Outbound Mirroring
|
|
200
|
+
|
|
201
|
+
When the business owner sends a WhatsApp message directly via WhatsApp Web (not through the agent), Baileys detects the echo with `key.fromMe = true`. The inbound monitor (`src/web/inbound/monitor.ts`) checks `access.isOwnerOutbound` and calls `mirrorOwnerReplyToSession()`, which writes a `[Owner reply] {body}` delivery-mirror into the public agent's session transcript via `appendAssistantMessageToSessionTranscript`. This ensures the public agent knows about the owner's direct intervention when the customer next replies.
|
|
202
|
+
|
|
203
|
+
The same path is used when an **admin agent** sends to a customer via the `message` tool (cross-agent echo): `fireCrossAgentEcho` in `src/infra/outbound/cross-agent-echo.ts` resolves the public agent's session key and delegates to `mirrorOwnerReplyToSession`. For established sessions the mirror is written to disk immediately; for first-contact customers with no session yet, an ephemeral system event is queued instead.
|
|
204
|
+
|
|
205
|
+
### Agent-sent echo suppression
|
|
206
|
+
|
|
207
|
+
Every outbound Baileys message echoes back as a `fromMe` `messages.upsert` event. To prevent the monitor from treating agent-sent echoes as manual owner messages, all send paths must track message IDs via `trackAgentSentMessage(id)` in `src/web/inbound/dedupe.ts`. The monitor then checks `isAgentSentMessage(id)` before calling `mirrorOwnerReplyToSession`.
|
|
208
|
+
|
|
209
|
+
There are two send paths — both must track IDs:
|
|
210
|
+
- **IPC send API** (`src/web/inbound/send-api.ts`): used by the message tool, gateway RPC, and `sendPoll`. Tracks IDs after `sock.sendMessage`.
|
|
211
|
+
- **Auto-reply closures** (`monitor.ts` lines 390-401): the `reply()` and `sendMedia()` functions on `WebInboundMessage`, used by `deliverWebReply`. Track IDs after `sock.sendMessage`.
|
|
212
|
+
|
|
213
|
+
The dedupe cache (`agentSentMessages`) has a 2-minute TTL and 500-entry cap, which is sufficient because Baileys echoes arrive within seconds.
|
|
214
|
+
|
|
215
|
+
**Key files:**
|
|
216
|
+
| File | Purpose |
|
|
217
|
+
|------|---------|
|
|
218
|
+
| `src/web/inbound/monitor.ts` | Detects `fromMe` echoes, calls `mirrorOwnerReplyToSession`; auto-reply `reply`/`sendMedia` closures |
|
|
219
|
+
| `src/web/inbound/owner-mirror.ts` | `mirrorOwnerReplyToSession()` — canonical implementation |
|
|
220
|
+
| `src/web/inbound/access-control.ts` | `isOwnerOutbound` flag for `fromMe` DMs |
|
|
221
|
+
| `src/web/inbound/send-api.ts` | IPC send path — tracks agent-sent message IDs |
|
|
222
|
+
| `src/web/inbound/dedupe.ts` | `trackAgentSentMessage` / `isAgentSentMessage` — echo suppression cache |
|
|
223
|
+
| `src/infra/outbound/cross-agent-echo.ts` | Adapter: agent tool → `mirrorOwnerReplyToSession` |
|
|
224
|
+
|
|
225
|
+
## Offline Message Recovery
|
|
226
|
+
|
|
227
|
+
Baileys is a WhatsApp Web protocol client. When the gateway is offline, WhatsApp's servers hold incoming messages and push them back as history (`append`-type `messages.upsert` events) when Baileys reconnects — so the system self-recovers automatically on restart. These replayed messages are stored in the UI message cache and marked as read, but **not** dispatched to the agent (no auto-reply on catch-up). The recovery window is determined entirely by WhatsApp's server-side delivery queue (approximately 30 days); `syncFullHistory: false` only prevents requesting full chat history on first pairing and does not restrict this window.
|
|
228
|
+
|
|
229
|
+
## Voice Notes (PTT)
|
|
230
|
+
|
|
231
|
+
Outbound TTS audio is sent as native WhatsApp PTT (Push-To-Talk) voice notes. The pipeline:
|
|
232
|
+
|
|
233
|
+
1. **TTS output format:** `resolveOutputFormat("whatsapp")` returns Opus (OGG container) instead of MP3 (`src/tts/tts.ts`)
|
|
234
|
+
2. **ffmpeg normalization:** `normalizePttAudio()` re-encodes to WhatsApp-native format — OGG Opus, 48 kHz, mono, 64 kbps, VoIP mode (`src/media/ptt-normalize.ts`)
|
|
235
|
+
3. **Baileys metadata:** `music-metadata` and `audio-decode` npm packages compute `seconds` (duration) and `waveform` fields for the PTT message. Without these, WhatsApp renders audio as a generic file attachment.
|
|
236
|
+
4. **Send:** `ptt: true` flag on the audio message, no caption (voice IS the content)
|
|
237
|
+
|
|
238
|
+
**System dependency:** ffmpeg must be on PATH. Installed automatically by `install.sh` (fresh) and `postinstall.js` (upgrades). Best-effort — if ffmpeg is unavailable, the original audio is sent as-is.
|
|
239
|
+
|
|
240
|
+
**Key files:**
|
|
241
|
+
| File | Purpose |
|
|
242
|
+
|------|---------|
|
|
243
|
+
| `src/media/ptt-normalize.ts` | ffmpeg re-encoding to WhatsApp-native OGG Opus |
|
|
244
|
+
| `src/web/auto-reply/deliver-reply.ts` | Auto-reply path (calls normalizePttAudio for audio) |
|
|
245
|
+
| `src/web/inbound/send-api.ts` | Baileys send wrapper (sets `ptt: true`) |
|
|
246
|
+
| `src/tts/tts.ts` | TTS output format resolution (Opus for WhatsApp) |
|
|
247
|
+
|
|
248
|
+
## Session Credentials (Baileys Auth)
|
|
249
|
+
|
|
250
|
+
Baileys stores WhatsApp session credentials (linked-device state) on disk. The auth directory contains `creds.json` (session keys) and Signal protocol pre-keys.
|
|
251
|
+
|
|
252
|
+
**Path resolution** (`src/web/accounts.ts` → `resolveWhatsAppAuthDir`):
|
|
253
|
+
|
|
254
|
+
| Priority | Path | When |
|
|
255
|
+
|----------|------|------|
|
|
256
|
+
| 1. Explicit config | `channels.whatsapp.accounts.{id}.authDir` | User overrides in config |
|
|
257
|
+
| 2. Default | `~/.taskmaster/credentials/whatsapp/{accountId}/` | Normal operation |
|
|
258
|
+
| 3. Legacy fallback | `~/.taskmaster/credentials/` | Old single-account installs (only if creds exist here and not at default path) |
|
|
259
|
+
|
|
260
|
+
The state dir (`~/.taskmaster/`) can be overridden via `TASKMASTER_STATE_DIR` env var; the credentials subdirectory via `TASKMASTER_OAUTH_DIR`.
|
|
261
|
+
|
|
262
|
+
**Diagnostics on the Pi:**
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
# List all WhatsApp auth directories
|
|
266
|
+
ls -la ~/.taskmaster/credentials/whatsapp/
|
|
267
|
+
|
|
268
|
+
# Check if credentials exist for an account
|
|
269
|
+
ls ~/.taskmaster/credentials/whatsapp/default/creds.json
|
|
270
|
+
|
|
271
|
+
# Check credential freshness (should be recent if just linked)
|
|
272
|
+
stat ~/.taskmaster/credentials/whatsapp/default/creds.json
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
If `creds.json` is missing, the session was never linked or was deleted. If it exists but WhatsApp rejects it (401), the session was invalidated (cleared from phone, expired after 14 days offline, or account banned).
|
|
276
|
+
|
|
277
|
+
**Key files:** `src/web/accounts.ts` (resolveWhatsAppAuthDir), `src/config/paths.ts` (resolveOAuthDir, resolveStateDir), `src/web/session.ts` (enqueueSaveCreds — writes creds on update).
|
|
278
|
+
|
|
279
|
+
## Disconnect Reasons
|
|
280
|
+
|
|
281
|
+
Baileys disconnect errors carry a numeric status code mapped to `DisconnectReason`. The monitor translates these to user-facing messages via `humanizeWhatsAppError()` in `src/web/auto-reply/monitor.ts`.
|
|
282
|
+
|
|
283
|
+
| Status | Baileys Reason | Meaning | Auto-reconnect? |
|
|
284
|
+
|--------|---------------|---------|-----------------|
|
|
285
|
+
| 401 | loggedOut | Session removed from phone or expired | No — re-link required |
|
|
286
|
+
| 401 + "conflict" | connectionReplaced (server override) | Another WhatsApp Web session took over | Yes |
|
|
287
|
+
| 403 | forbidden | Number banned or restricted | No |
|
|
288
|
+
| 408 | connectionLost / timedOut | Network interruption or timeout | Yes |
|
|
289
|
+
| 428 | connectionClosed | Server closed the connection | Yes |
|
|
290
|
+
| 440 | connectionReplaced | Another session replaced this one | Yes |
|
|
291
|
+
| 500 | badSession | Corrupted session data | No — re-link required |
|
|
292
|
+
| 515 | restartRequired | Server requests reconnection | Yes |
|
|
293
|
+
|
|
294
|
+
**Conflict vs logout (401 ambiguity):** WhatsApp's server can send a stream error with code 401 and reason "conflict" — Baileys normally maps "conflict" to 440, but the server's explicit `code` attribute overrides the mapping. The monitor checks for "conflict" in the error message to distinguish this from a true logout.
|
|
295
|
+
|
|
296
|
+
## Reconnection: Setup Failures vs Runtime Disconnects
|
|
297
|
+
|
|
298
|
+
The reconnection loop in `monitorWebChannel` handles two distinct failure modes:
|
|
299
|
+
|
|
300
|
+
| Failure Mode | When | Example | Handling |
|
|
301
|
+
|-------------|------|---------|----------|
|
|
302
|
+
| **Runtime disconnect** | After a successful connection drops | 408 (connectionLost), 428 (connectionClosed) | `listener.onClose` resolves → backoff retry |
|
|
303
|
+
| **Setup failure** | Connection never establishes | 405 (Method Not Allowed), 503 (service unavailable) | `monitorWebInbox` throws → caught by try-catch → same backoff retry |
|
|
304
|
+
|
|
305
|
+
Both paths use the same backoff policy (2s → 30s, 12 max attempts) and the same loggedOut detection. A 401 status only triggers logout if the error message does NOT contain "conflict" (see Disconnect Reasons above). Setup failures log as `"web reconnect: connection setup failed"` instead of `"web reconnect: connection closed"`.
|
|
306
|
+
|
|
307
|
+
Without the setup-failure handling, a transient WhatsApp server rejection (e.g., 405 during a protocol update) would kill the channel permanently — the error would escape the reconnection loop, and only a gateway restart could recover.
|
|
308
|
+
|
|
309
|
+
## Provider Architecture
|
|
310
|
+
|
|
311
|
+
```
|
|
312
|
+
src/web/
|
|
313
|
+
├── outbound-routing.ts # Multi-account outbound routing logic
|
|
314
|
+
├── active-listener.ts # Per-account listener registry (listActiveWebAccountIds, etc.)
|
|
315
|
+
├── outbound.ts # Outbound send (routes through outbound-routing)
|
|
316
|
+
├── providers/
|
|
317
|
+
│ ├── types.ts # WhatsAppProvider interface
|
|
318
|
+
│ ├── factory.ts # createWhatsAppProvider()
|
|
319
|
+
│ ├── baileys/ # BaileysProvider (wraps monitorWebInbox)
|
|
320
|
+
│ └── cloud/ # CloudApiProvider (parked — enterprise)
|
|
321
|
+
└── auto-reply/
|
|
322
|
+
├── monitor.ts # Baileys reconnection loop (monitorWebChannel)
|
|
323
|
+
├── cloud-monitor.ts # Cloud API reconnection loop (monitorCloudChannel)
|
|
324
|
+
└── monitor/
|
|
325
|
+
└── on-message.ts # Shared message handler (used by both providers)
|
|
326
|
+
|
|
327
|
+
extensions/whatsapp/src/
|
|
328
|
+
└── channel.ts # Channel plugin (provider-aware throughout)
|
|
329
|
+
```
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: connect-whatsapp
|
|
3
|
+
description: Guide the user through connecting WhatsApp via QR code pairing. Renders QR inline, polls for completion, confirms connection, and sets up admin/public bindings.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Connect WhatsApp
|
|
7
|
+
|
|
8
|
+
Conversational flow for linking a WhatsApp account to Maxy via Baileys QR pairing.
|
|
9
|
+
|
|
10
|
+
## When to activate
|
|
11
|
+
|
|
12
|
+
User says anything about connecting, linking, setting up, or adding WhatsApp.
|
|
13
|
+
|
|
14
|
+
## Prerequisites
|
|
15
|
+
|
|
16
|
+
Call `whatsapp-status` first to check the current state. If already connected, tell the user and offer to relink.
|
|
17
|
+
|
|
18
|
+
## Flow
|
|
19
|
+
|
|
20
|
+
1. **Initiate pairing** — call `whatsapp-login-start`. If the account is already linked and the user didn't say "relink", inform them and stop.
|
|
21
|
+
|
|
22
|
+
2. **Show QR code** — the tool returns QR data. Use `render-component` to display the QR code inline in the chat. Tell the user: "Open WhatsApp on your phone → Settings → Linked Devices → Link a Device → Scan this QR code."
|
|
23
|
+
|
|
24
|
+
3. **Wait for scan** — call `whatsapp-login-wait`. This polls for up to 60 seconds. While waiting, tell the user you're watching for the scan.
|
|
25
|
+
|
|
26
|
+
4. **Confirm connection** — when pairing completes, the tool returns the linked phone number. Confirm: "WhatsApp connected as {phone}. Messages from your phone route to this admin chat. Messages from other numbers route to the public agent."
|
|
27
|
+
|
|
28
|
+
5. **Verify** — call `whatsapp-status` to confirm the connection is live.
|
|
29
|
+
|
|
30
|
+
## Relinking
|
|
31
|
+
|
|
32
|
+
If the user says "relink WhatsApp" or the status shows disconnected with a 401 error:
|
|
33
|
+
1. Call `whatsapp-login-start` with `force: true` to clear old credentials and generate a fresh QR
|
|
34
|
+
2. Follow the same flow as above
|
|
35
|
+
|
|
36
|
+
## Troubleshooting
|
|
37
|
+
|
|
38
|
+
- **QR expired** — generate a new one with `whatsapp-login-start`
|
|
39
|
+
- **Phone not scanning** — ensure the phone has internet, WhatsApp is updated, and the user is in Linked Devices
|
|
40
|
+
- **Keeps disconnecting** — check `whatsapp-status` for error details. A 401 means re-link is needed. Repeated transient errors suggest network issues on the device.
|
|
41
|
+
|
|
42
|
+
## Language
|
|
43
|
+
|
|
44
|
+
Use plain English:
|
|
45
|
+
- "QR code" not "Baileys pairing token"
|
|
46
|
+
- "Link your phone" not "initiate WebSocket connection"
|
|
47
|
+
- "WhatsApp is connected" not "Baileys socket is open"
|