@wahooks/channel 0.1.0
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.d.ts +18 -0
- package/dist/index.js +273 -0
- package/package.json +29 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @wahooks/channel — WhatsApp channel for Claude Code
|
|
4
|
+
*
|
|
5
|
+
* Connects WhatsApp to a running Claude Code session via WAHooks.
|
|
6
|
+
* Messages from WhatsApp appear as <channel> events; Claude replies
|
|
7
|
+
* via the reply tool and messages are sent back through WhatsApp.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* claude --dangerously-load-development-channels server:wahooks-channel
|
|
11
|
+
*
|
|
12
|
+
* Environment:
|
|
13
|
+
* WAHOOKS_API_KEY — WAHooks API token (wh_...)
|
|
14
|
+
* WAHOOKS_API_URL — API base URL (default: https://api.wahooks.com)
|
|
15
|
+
* WAHOOKS_CONNECTION — Connection ID to use (auto-detected if only one)
|
|
16
|
+
* WAHOOKS_ALLOW — Comma-separated phone numbers to accept (empty = all)
|
|
17
|
+
*/
|
|
18
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @wahooks/channel — WhatsApp channel for Claude Code
|
|
4
|
+
*
|
|
5
|
+
* Connects WhatsApp to a running Claude Code session via WAHooks.
|
|
6
|
+
* Messages from WhatsApp appear as <channel> events; Claude replies
|
|
7
|
+
* via the reply tool and messages are sent back through WhatsApp.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* claude --dangerously-load-development-channels server:wahooks-channel
|
|
11
|
+
*
|
|
12
|
+
* Environment:
|
|
13
|
+
* WAHOOKS_API_KEY — WAHooks API token (wh_...)
|
|
14
|
+
* WAHOOKS_API_URL — API base URL (default: https://api.wahooks.com)
|
|
15
|
+
* WAHOOKS_CONNECTION — Connection ID to use (auto-detected if only one)
|
|
16
|
+
* WAHOOKS_ALLOW — Comma-separated phone numbers to accept (empty = all)
|
|
17
|
+
*/
|
|
18
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
19
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
20
|
+
import { ListToolsRequestSchema, CallToolRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
21
|
+
import http from "node:http";
|
|
22
|
+
// ─── Config ─────────────────────────────────────────────────────────────
|
|
23
|
+
const API_KEY = process.env.WAHOOKS_API_KEY ?? "";
|
|
24
|
+
const API_URL = (process.env.WAHOOKS_API_URL ?? "https://api.wahooks.com").replace(/\/$/, "");
|
|
25
|
+
const CONNECTION_ID = process.env.WAHOOKS_CONNECTION ?? "";
|
|
26
|
+
const ALLOW_LIST = new Set((process.env.WAHOOKS_ALLOW ?? "")
|
|
27
|
+
.split(",")
|
|
28
|
+
.map((s) => s.trim().replace(/\D/g, ""))
|
|
29
|
+
.filter(Boolean));
|
|
30
|
+
const WEBHOOK_PORT = parseInt(process.env.WAHOOKS_CHANNEL_PORT ?? "8790", 10);
|
|
31
|
+
if (!API_KEY) {
|
|
32
|
+
console.error("[wahooks-channel] WAHOOKS_API_KEY is required");
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
// ─── WAHooks API helpers ────────────────────────────────────────────────
|
|
36
|
+
async function api(method, path, body) {
|
|
37
|
+
const res = await fetch(`${API_URL}/api${path}`, {
|
|
38
|
+
method,
|
|
39
|
+
headers: {
|
|
40
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
},
|
|
43
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
44
|
+
});
|
|
45
|
+
if (!res.ok) {
|
|
46
|
+
const text = await res.text().catch(() => "");
|
|
47
|
+
throw new Error(`WAHooks API ${method} ${path}: ${res.status} ${text}`);
|
|
48
|
+
}
|
|
49
|
+
return res.json();
|
|
50
|
+
}
|
|
51
|
+
async function resolveConnection() {
|
|
52
|
+
if (CONNECTION_ID)
|
|
53
|
+
return CONNECTION_ID;
|
|
54
|
+
const conns = await api("GET", "/connections");
|
|
55
|
+
const active = conns.filter((c) => c.status === "connected");
|
|
56
|
+
if (active.length === 0) {
|
|
57
|
+
throw new Error("No connected WAHooks connections found. Create one first.");
|
|
58
|
+
}
|
|
59
|
+
if (active.length === 1) {
|
|
60
|
+
console.error(`[wahooks-channel] Auto-selected connection: ${active[0].id}`);
|
|
61
|
+
return active[0].id;
|
|
62
|
+
}
|
|
63
|
+
console.error("[wahooks-channel] Multiple connections found. Set WAHOOKS_CONNECTION:");
|
|
64
|
+
for (const c of active) {
|
|
65
|
+
console.error(` ${c.id} (${c.phoneNumber ?? "no phone"})`);
|
|
66
|
+
}
|
|
67
|
+
throw new Error("Ambiguous connection — set WAHOOKS_CONNECTION env var");
|
|
68
|
+
}
|
|
69
|
+
// ─── State ──────────────────────────────────────────────────────────────
|
|
70
|
+
let connectionId;
|
|
71
|
+
// Track inbound message → sender mapping for replies
|
|
72
|
+
const messageToSender = new Map();
|
|
73
|
+
// ─── MCP Server ─────────────────────────────────────────────────────────
|
|
74
|
+
const mcp = new Server({ name: "wahooks-channel", version: "0.1.0" }, {
|
|
75
|
+
capabilities: {
|
|
76
|
+
experimental: { "claude/channel": {} },
|
|
77
|
+
tools: {},
|
|
78
|
+
},
|
|
79
|
+
instructions: [
|
|
80
|
+
"WhatsApp messages arrive as <channel source=\"wahooks-channel\" from=\"phone\" message_id=\"id\">.",
|
|
81
|
+
"Use the wahooks_reply tool to send responses back. Pass the from phone number.",
|
|
82
|
+
"Use wahooks_send_image / wahooks_send_document to send media.",
|
|
83
|
+
"You can also proactively message any phone with wahooks_send.",
|
|
84
|
+
].join(" "),
|
|
85
|
+
});
|
|
86
|
+
// ─── Tools ──────────────────────────────────────────────────────────────
|
|
87
|
+
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
88
|
+
tools: [
|
|
89
|
+
{
|
|
90
|
+
name: "wahooks_reply",
|
|
91
|
+
description: "Reply to a WhatsApp message. Use the 'from' phone number from the channel event.",
|
|
92
|
+
inputSchema: {
|
|
93
|
+
type: "object",
|
|
94
|
+
properties: {
|
|
95
|
+
to: { type: "string", description: "Phone number to reply to (from the channel tag)" },
|
|
96
|
+
text: { type: "string", description: "Message text" },
|
|
97
|
+
},
|
|
98
|
+
required: ["to", "text"],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "wahooks_send",
|
|
103
|
+
description: "Send a WhatsApp message to any phone number.",
|
|
104
|
+
inputSchema: {
|
|
105
|
+
type: "object",
|
|
106
|
+
properties: {
|
|
107
|
+
to: { type: "string", description: "Phone number (e.g. 1234567890)" },
|
|
108
|
+
text: { type: "string", description: "Message text" },
|
|
109
|
+
},
|
|
110
|
+
required: ["to", "text"],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: "wahooks_send_image",
|
|
115
|
+
description: "Send an image via WhatsApp.",
|
|
116
|
+
inputSchema: {
|
|
117
|
+
type: "object",
|
|
118
|
+
properties: {
|
|
119
|
+
to: { type: "string", description: "Phone number" },
|
|
120
|
+
url: { type: "string", description: "Image URL" },
|
|
121
|
+
caption: { type: "string", description: "Optional caption" },
|
|
122
|
+
},
|
|
123
|
+
required: ["to", "url"],
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "wahooks_send_document",
|
|
128
|
+
description: "Send a document/file via WhatsApp.",
|
|
129
|
+
inputSchema: {
|
|
130
|
+
type: "object",
|
|
131
|
+
properties: {
|
|
132
|
+
to: { type: "string", description: "Phone number" },
|
|
133
|
+
url: { type: "string", description: "Document URL" },
|
|
134
|
+
filename: { type: "string", description: "Filename" },
|
|
135
|
+
},
|
|
136
|
+
required: ["to", "url"],
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
}));
|
|
141
|
+
function toChatId(phone) {
|
|
142
|
+
const digits = phone.replace(/\D/g, "");
|
|
143
|
+
return digits.includes("@") ? digits : `${digits}@s.whatsapp.net`;
|
|
144
|
+
}
|
|
145
|
+
mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
146
|
+
const args = req.params.arguments;
|
|
147
|
+
switch (req.params.name) {
|
|
148
|
+
case "wahooks_reply":
|
|
149
|
+
case "wahooks_send": {
|
|
150
|
+
await api("POST", `/connections/${connectionId}/send`, {
|
|
151
|
+
chatId: toChatId(args.to),
|
|
152
|
+
text: args.text,
|
|
153
|
+
});
|
|
154
|
+
return { content: [{ type: "text", text: `Sent to ${args.to}` }] };
|
|
155
|
+
}
|
|
156
|
+
case "wahooks_send_image": {
|
|
157
|
+
await api("POST", `/connections/${connectionId}/send-image`, {
|
|
158
|
+
chatId: toChatId(args.to),
|
|
159
|
+
url: args.url,
|
|
160
|
+
caption: args.caption,
|
|
161
|
+
});
|
|
162
|
+
return { content: [{ type: "text", text: `Image sent to ${args.to}` }] };
|
|
163
|
+
}
|
|
164
|
+
case "wahooks_send_document": {
|
|
165
|
+
await api("POST", `/connections/${connectionId}/send-document`, {
|
|
166
|
+
chatId: toChatId(args.to),
|
|
167
|
+
url: args.url,
|
|
168
|
+
filename: args.filename,
|
|
169
|
+
});
|
|
170
|
+
return { content: [{ type: "text", text: `Document sent to ${args.to}` }] };
|
|
171
|
+
}
|
|
172
|
+
default:
|
|
173
|
+
throw new Error(`Unknown tool: ${req.params.name}`);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
// ─── Webhook receiver ───────────────────────────────────────────────────
|
|
177
|
+
function startWebhookServer() {
|
|
178
|
+
const server = http.createServer(async (req, res) => {
|
|
179
|
+
if (req.method !== "POST") {
|
|
180
|
+
res.writeHead(404);
|
|
181
|
+
res.end();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
let body = "";
|
|
185
|
+
req.on("data", (chunk) => { body += chunk; });
|
|
186
|
+
req.on("end", async () => {
|
|
187
|
+
try {
|
|
188
|
+
const event = JSON.parse(body);
|
|
189
|
+
// WAHooks webhook payload structure
|
|
190
|
+
const eventType = event.event ?? "";
|
|
191
|
+
const payload = event.payload ?? {};
|
|
192
|
+
// Only handle incoming messages
|
|
193
|
+
if (!eventType.startsWith("message")) {
|
|
194
|
+
res.writeHead(200);
|
|
195
|
+
res.end("ok");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const from = (payload.from ?? "").replace("@c.us", "").replace("@s.whatsapp.net", "");
|
|
199
|
+
const text = payload.body ?? payload.text ?? "";
|
|
200
|
+
const messageId = payload.id?._serialized ?? payload.id ?? `msg_${Date.now()}`;
|
|
201
|
+
if (!from || !text) {
|
|
202
|
+
res.writeHead(200);
|
|
203
|
+
res.end("ok");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// Sender gating
|
|
207
|
+
if (ALLOW_LIST.size > 0 && !ALLOW_LIST.has(from)) {
|
|
208
|
+
console.error(`[wahooks-channel] Blocked message from ${from} (not in allow list)`);
|
|
209
|
+
res.writeHead(200);
|
|
210
|
+
res.end("ok");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
// Track sender for replies
|
|
214
|
+
messageToSender.set(messageId, from);
|
|
215
|
+
// Forward to Claude Code
|
|
216
|
+
await mcp.notification({
|
|
217
|
+
method: "notifications/claude/channel",
|
|
218
|
+
params: {
|
|
219
|
+
content: text,
|
|
220
|
+
meta: {
|
|
221
|
+
from,
|
|
222
|
+
message_id: messageId,
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
console.error(`[wahooks-channel] Message from ${from}: ${text.slice(0, 80)}`);
|
|
227
|
+
res.writeHead(200);
|
|
228
|
+
res.end("ok");
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
console.error("[wahooks-channel] Webhook error:", err);
|
|
232
|
+
res.writeHead(500);
|
|
233
|
+
res.end("error");
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
server.listen(WEBHOOK_PORT, "127.0.0.1", () => {
|
|
238
|
+
console.error(`[wahooks-channel] Webhook server listening on http://127.0.0.1:${WEBHOOK_PORT}`);
|
|
239
|
+
console.error(`[wahooks-channel] Configure WAHooks webhook URL: http://localhost:${WEBHOOK_PORT}/webhook`);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
// ─── Setup webhook on WAHooks ───────────────────────────────────────────
|
|
243
|
+
async function ensureWebhook() {
|
|
244
|
+
try {
|
|
245
|
+
const webhooks = await api("GET", `/connections/${connectionId}/webhooks`);
|
|
246
|
+
const localUrl = `http://localhost:${WEBHOOK_PORT}/webhook`;
|
|
247
|
+
const existing = webhooks.find((w) => w.url === localUrl);
|
|
248
|
+
if (!existing) {
|
|
249
|
+
console.error("[wahooks-channel] Note: set up a webhook pointing to this server.");
|
|
250
|
+
console.error(`[wahooks-channel] URL: ${localUrl}`);
|
|
251
|
+
console.error("[wahooks-channel] Events: message, message.any");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// Non-critical
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// ─── Main ───────────────────────────────────────────────────────────────
|
|
259
|
+
async function main() {
|
|
260
|
+
connectionId = await resolveConnection();
|
|
261
|
+
console.error(`[wahooks-channel] Using connection: ${connectionId}`);
|
|
262
|
+
// Start MCP transport
|
|
263
|
+
const transport = new StdioServerTransport();
|
|
264
|
+
await mcp.connect(transport);
|
|
265
|
+
// Start webhook receiver
|
|
266
|
+
startWebhookServer();
|
|
267
|
+
await ensureWebhook();
|
|
268
|
+
console.error("[wahooks-channel] Ready — WhatsApp messages will appear in Claude Code");
|
|
269
|
+
}
|
|
270
|
+
main().catch((err) => {
|
|
271
|
+
console.error("[wahooks-channel] Fatal:", err.message);
|
|
272
|
+
process.exit(1);
|
|
273
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wahooks/channel",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "WhatsApp channel for Claude Code — chat with Claude via WhatsApp",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"wahooks-channel": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"prepublishOnly": "npm run build"
|
|
13
|
+
},
|
|
14
|
+
"files": ["dist"],
|
|
15
|
+
"keywords": ["wahooks", "whatsapp", "claude-code", "channel", "mcp"],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/dhruvyad/wahooks",
|
|
20
|
+
"directory": "sdks/channel"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@modelcontextprotocol/sdk": "^1.12.1"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"typescript": "^5.0.0",
|
|
27
|
+
"@types/node": "^20.0.0"
|
|
28
|
+
}
|
|
29
|
+
}
|