@wahooks/channel 0.2.0 → 0.4.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.js +155 -79
- package/package.json +17 -6
package/dist/index.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
19
19
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
20
20
|
import { ListToolsRequestSchema, CallToolRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
21
|
-
import
|
|
21
|
+
import WebSocket from "ws";
|
|
22
22
|
import fs from "node:fs";
|
|
23
23
|
import path from "node:path";
|
|
24
24
|
import os from "node:os";
|
|
@@ -83,7 +83,6 @@ const ALLOW_LIST = new Set((cfg.WAHOOKS_ALLOW ?? "")
|
|
|
83
83
|
.split(",")
|
|
84
84
|
.map((s) => s.trim().replace(/\D/g, ""))
|
|
85
85
|
.filter(Boolean));
|
|
86
|
-
const WEBHOOK_PORT = parseInt(cfg.WAHOOKS_CHANNEL_PORT ?? "8790", 10);
|
|
87
86
|
if (!API_KEY) {
|
|
88
87
|
console.error("[wahooks-channel] No API key found.");
|
|
89
88
|
console.error("[wahooks-channel] Run: wahooks-channel --configure <your-api-key>");
|
|
@@ -137,9 +136,9 @@ const mcp = new Server({ name: "wahooks-channel", version: "0.1.0" }, {
|
|
|
137
136
|
},
|
|
138
137
|
instructions: [
|
|
139
138
|
"WhatsApp messages arrive as <channel source=\"wahooks-channel\" from=\"phone\" message_id=\"id\">.",
|
|
140
|
-
"Use
|
|
141
|
-
"
|
|
142
|
-
"
|
|
139
|
+
"Use wahooks_reply to respond to the sender. Use wahooks_send to message any phone.",
|
|
140
|
+
"Media tools: wahooks_send_image, wahooks_send_video, wahooks_send_audio, wahooks_send_document.",
|
|
141
|
+
"Also available: wahooks_send_location (lat/lng) and wahooks_send_contact (name/phone).",
|
|
143
142
|
"For permission requests, the user can reply 'yes XXXXX' or 'no XXXXX' where XXXXX is the request ID.",
|
|
144
143
|
].join(" "),
|
|
145
144
|
});
|
|
@@ -225,6 +224,59 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
225
224
|
required: ["to", "url"],
|
|
226
225
|
},
|
|
227
226
|
},
|
|
227
|
+
{
|
|
228
|
+
name: "wahooks_send_video",
|
|
229
|
+
description: "Send a video via WhatsApp.",
|
|
230
|
+
inputSchema: {
|
|
231
|
+
type: "object",
|
|
232
|
+
properties: {
|
|
233
|
+
to: { type: "string", description: "Phone number" },
|
|
234
|
+
url: { type: "string", description: "Video URL" },
|
|
235
|
+
caption: { type: "string", description: "Optional caption" },
|
|
236
|
+
},
|
|
237
|
+
required: ["to", "url"],
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: "wahooks_send_audio",
|
|
242
|
+
description: "Send an audio/voice message via WhatsApp.",
|
|
243
|
+
inputSchema: {
|
|
244
|
+
type: "object",
|
|
245
|
+
properties: {
|
|
246
|
+
to: { type: "string", description: "Phone number" },
|
|
247
|
+
url: { type: "string", description: "Audio URL" },
|
|
248
|
+
},
|
|
249
|
+
required: ["to", "url"],
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
name: "wahooks_send_location",
|
|
254
|
+
description: "Send a location pin via WhatsApp.",
|
|
255
|
+
inputSchema: {
|
|
256
|
+
type: "object",
|
|
257
|
+
properties: {
|
|
258
|
+
to: { type: "string", description: "Phone number" },
|
|
259
|
+
latitude: { type: "number", description: "Latitude" },
|
|
260
|
+
longitude: { type: "number", description: "Longitude" },
|
|
261
|
+
name: { type: "string", description: "Location name" },
|
|
262
|
+
address: { type: "string", description: "Address" },
|
|
263
|
+
},
|
|
264
|
+
required: ["to", "latitude", "longitude"],
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
name: "wahooks_send_contact",
|
|
269
|
+
description: "Send a contact card via WhatsApp.",
|
|
270
|
+
inputSchema: {
|
|
271
|
+
type: "object",
|
|
272
|
+
properties: {
|
|
273
|
+
to: { type: "string", description: "Phone number" },
|
|
274
|
+
contact_name: { type: "string", description: "Contact's display name" },
|
|
275
|
+
contact_phone: { type: "string", description: "Contact's phone number" },
|
|
276
|
+
},
|
|
277
|
+
required: ["to", "contact_name", "contact_phone"],
|
|
278
|
+
},
|
|
279
|
+
},
|
|
228
280
|
],
|
|
229
281
|
}));
|
|
230
282
|
function toChatId(phone) {
|
|
@@ -258,88 +310,112 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
258
310
|
});
|
|
259
311
|
return { content: [{ type: "text", text: `Document sent to ${args.to}` }] };
|
|
260
312
|
}
|
|
313
|
+
case "wahooks_send_video": {
|
|
314
|
+
await api("POST", `/connections/${connectionId}/send-video`, {
|
|
315
|
+
chatId: toChatId(args.to),
|
|
316
|
+
url: args.url,
|
|
317
|
+
caption: args.caption,
|
|
318
|
+
});
|
|
319
|
+
return { content: [{ type: "text", text: `Video sent to ${args.to}` }] };
|
|
320
|
+
}
|
|
321
|
+
case "wahooks_send_audio": {
|
|
322
|
+
await api("POST", `/connections/${connectionId}/send-audio`, {
|
|
323
|
+
chatId: toChatId(args.to),
|
|
324
|
+
url: args.url,
|
|
325
|
+
});
|
|
326
|
+
return { content: [{ type: "text", text: `Audio sent to ${args.to}` }] };
|
|
327
|
+
}
|
|
328
|
+
case "wahooks_send_location": {
|
|
329
|
+
await api("POST", `/connections/${connectionId}/send-location`, {
|
|
330
|
+
chatId: toChatId(args.to),
|
|
331
|
+
latitude: parseFloat(args.latitude),
|
|
332
|
+
longitude: parseFloat(args.longitude),
|
|
333
|
+
name: args.name,
|
|
334
|
+
address: args.address,
|
|
335
|
+
});
|
|
336
|
+
return { content: [{ type: "text", text: `Location sent to ${args.to}` }] };
|
|
337
|
+
}
|
|
338
|
+
case "wahooks_send_contact": {
|
|
339
|
+
await api("POST", `/connections/${connectionId}/send-contact`, {
|
|
340
|
+
chatId: toChatId(args.to),
|
|
341
|
+
contactName: args.contact_name,
|
|
342
|
+
contactPhone: args.contact_phone,
|
|
343
|
+
});
|
|
344
|
+
return { content: [{ type: "text", text: `Contact sent to ${args.to}` }] };
|
|
345
|
+
}
|
|
261
346
|
default:
|
|
262
347
|
throw new Error(`Unknown tool: ${req.params.name}`);
|
|
263
348
|
}
|
|
264
349
|
});
|
|
265
|
-
// ───
|
|
266
|
-
function
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
// Track last sender for permission relay
|
|
303
|
-
lastSender = from;
|
|
304
|
-
// Check if this is a permission verdict
|
|
305
|
-
const permMatch = PERMISSION_RE.exec(text);
|
|
306
|
-
if (permMatch) {
|
|
307
|
-
await mcp.notification({
|
|
308
|
-
method: "notifications/claude/channel/permission",
|
|
309
|
-
params: {
|
|
310
|
-
request_id: permMatch[2].toLowerCase(),
|
|
311
|
-
behavior: permMatch[1].toLowerCase().startsWith("y") ? "allow" : "deny",
|
|
312
|
-
},
|
|
313
|
-
});
|
|
314
|
-
console.error(`[wahooks-channel] Permission verdict: ${permMatch[1]} ${permMatch[2]}`);
|
|
315
|
-
res.writeHead(200);
|
|
316
|
-
res.end("ok");
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
// Forward to Claude Code
|
|
350
|
+
// ─── WebSocket event stream ─────────────────────────────────────────────
|
|
351
|
+
function connectWebSocket() {
|
|
352
|
+
const wsProtocol = API_URL.startsWith("https") ? "wss" : "ws";
|
|
353
|
+
const wsHost = API_URL.replace(/^https?/, wsProtocol);
|
|
354
|
+
const wsUrl = `${wsHost}/ws?token=${encodeURIComponent(API_KEY)}`;
|
|
355
|
+
console.error("[wahooks-channel] Connecting to event stream...");
|
|
356
|
+
const ws = new WebSocket(wsUrl);
|
|
357
|
+
ws.on("open", () => {
|
|
358
|
+
console.error("[wahooks-channel] Connected to event stream");
|
|
359
|
+
});
|
|
360
|
+
ws.on("message", async (data) => {
|
|
361
|
+
try {
|
|
362
|
+
const event = JSON.parse(data.toString());
|
|
363
|
+
const eventType = event.event ?? "";
|
|
364
|
+
const payload = event.payload ?? {};
|
|
365
|
+
// Only handle incoming messages
|
|
366
|
+
if (!eventType.startsWith("message"))
|
|
367
|
+
return;
|
|
368
|
+
const from = (payload.from ?? "")
|
|
369
|
+
.replace("@c.us", "")
|
|
370
|
+
.replace("@s.whatsapp.net", "");
|
|
371
|
+
const text = payload.body ?? payload.text ?? "";
|
|
372
|
+
const messageId = payload.id?._serialized ?? payload.id ?? `msg_${Date.now()}`;
|
|
373
|
+
if (!from || !text)
|
|
374
|
+
return;
|
|
375
|
+
// Sender gating
|
|
376
|
+
if (ALLOW_LIST.size > 0 && !ALLOW_LIST.has(from)) {
|
|
377
|
+
console.error(`[wahooks-channel] Blocked message from ${from} (not in allow list)`);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
// Track last sender for permission relay
|
|
381
|
+
lastSender = from;
|
|
382
|
+
// Check if this is a permission verdict
|
|
383
|
+
const permMatch = PERMISSION_RE.exec(text);
|
|
384
|
+
if (permMatch) {
|
|
320
385
|
await mcp.notification({
|
|
321
|
-
method: "notifications/claude/channel",
|
|
386
|
+
method: "notifications/claude/channel/permission",
|
|
322
387
|
params: {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
from,
|
|
326
|
-
message_id: messageId,
|
|
327
|
-
},
|
|
388
|
+
request_id: permMatch[2].toLowerCase(),
|
|
389
|
+
behavior: permMatch[1].toLowerCase().startsWith("y") ? "allow" : "deny",
|
|
328
390
|
},
|
|
329
391
|
});
|
|
330
|
-
console.error(`[wahooks-channel]
|
|
331
|
-
|
|
332
|
-
res.end("ok");
|
|
333
|
-
}
|
|
334
|
-
catch (err) {
|
|
335
|
-
console.error("[wahooks-channel] Webhook error:", err);
|
|
336
|
-
res.writeHead(500);
|
|
337
|
-
res.end("error");
|
|
392
|
+
console.error(`[wahooks-channel] Permission verdict: ${permMatch[1]} ${permMatch[2]}`);
|
|
393
|
+
return;
|
|
338
394
|
}
|
|
339
|
-
|
|
395
|
+
// Forward to Claude Code
|
|
396
|
+
await mcp.notification({
|
|
397
|
+
method: "notifications/claude/channel",
|
|
398
|
+
params: {
|
|
399
|
+
content: text,
|
|
400
|
+
meta: {
|
|
401
|
+
from,
|
|
402
|
+
message_id: messageId,
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
console.error(`[wahooks-channel] Message from ${from}: ${text.slice(0, 80)}`);
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
console.error("[wahooks-channel] Event parse error:", err);
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
ws.on("close", (code) => {
|
|
413
|
+
console.error(`[wahooks-channel] Connection closed (${code}), reconnecting in 5s...`);
|
|
414
|
+
setTimeout(connectWebSocket, 5000);
|
|
340
415
|
});
|
|
341
|
-
|
|
342
|
-
console.error(`[wahooks-channel]
|
|
416
|
+
ws.on("error", (err) => {
|
|
417
|
+
console.error(`[wahooks-channel] WebSocket error: ${err.message}`);
|
|
418
|
+
// close event will fire after this and trigger reconnect
|
|
343
419
|
});
|
|
344
420
|
}
|
|
345
421
|
// ─── Main ───────────────────────────────────────────────────────────────
|
|
@@ -349,8 +425,8 @@ async function main() {
|
|
|
349
425
|
// Start MCP transport
|
|
350
426
|
const transport = new StdioServerTransport();
|
|
351
427
|
await mcp.connect(transport);
|
|
352
|
-
//
|
|
353
|
-
|
|
428
|
+
// Connect to real-time event stream
|
|
429
|
+
connectWebSocket();
|
|
354
430
|
console.error("[wahooks-channel] Ready — WhatsApp messages will appear in Claude Code");
|
|
355
431
|
}
|
|
356
432
|
main().catch((err) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wahooks/channel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "WhatsApp channel for Claude Code — chat with Claude via WhatsApp",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,8 +11,17 @@
|
|
|
11
11
|
"build": "tsc",
|
|
12
12
|
"prepublishOnly": "npm run build"
|
|
13
13
|
},
|
|
14
|
-
"files": [
|
|
15
|
-
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"manifest.json"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"wahooks",
|
|
20
|
+
"whatsapp",
|
|
21
|
+
"claude-code",
|
|
22
|
+
"channel",
|
|
23
|
+
"mcp"
|
|
24
|
+
],
|
|
16
25
|
"license": "MIT",
|
|
17
26
|
"repository": {
|
|
18
27
|
"type": "git",
|
|
@@ -20,10 +29,12 @@
|
|
|
20
29
|
"directory": "sdks/channel"
|
|
21
30
|
},
|
|
22
31
|
"dependencies": {
|
|
23
|
-
"@modelcontextprotocol/sdk": "^1.12.1"
|
|
32
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
33
|
+
"ws": "^8.20.0"
|
|
24
34
|
},
|
|
25
35
|
"devDependencies": {
|
|
26
|
-
"
|
|
27
|
-
"@types/
|
|
36
|
+
"@types/node": "^20.0.0",
|
|
37
|
+
"@types/ws": "^8.18.1",
|
|
38
|
+
"typescript": "^5.0.0"
|
|
28
39
|
}
|
|
29
40
|
}
|