@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.
Files changed (2) hide show
  1. package/dist/index.js +155 -79
  2. 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 http from "node:http";
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 the wahooks_reply tool to send responses back. Pass the from phone number.",
141
- "Use wahooks_send_image / wahooks_send_document to send media.",
142
- "You can also proactively message any phone with wahooks_send.",
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
- // ─── Webhook receiver ───────────────────────────────────────────────────
266
- function startWebhookServer() {
267
- const server = http.createServer(async (req, res) => {
268
- if (req.method !== "POST") {
269
- res.writeHead(404);
270
- res.end();
271
- return;
272
- }
273
- let body = "";
274
- req.on("data", (chunk) => { body += chunk; });
275
- req.on("end", async () => {
276
- try {
277
- const event = JSON.parse(body);
278
- // WAHooks webhook payload structure
279
- const eventType = event.event ?? "";
280
- const payload = event.payload ?? {};
281
- // Only handle incoming messages
282
- if (!eventType.startsWith("message")) {
283
- res.writeHead(200);
284
- res.end("ok");
285
- return;
286
- }
287
- const from = (payload.from ?? "").replace("@c.us", "").replace("@s.whatsapp.net", "");
288
- const text = payload.body ?? payload.text ?? "";
289
- const messageId = payload.id?._serialized ?? payload.id ?? `msg_${Date.now()}`;
290
- if (!from || !text) {
291
- res.writeHead(200);
292
- res.end("ok");
293
- return;
294
- }
295
- // Sender gating
296
- if (ALLOW_LIST.size > 0 && !ALLOW_LIST.has(from)) {
297
- console.error(`[wahooks-channel] Blocked message from ${from} (not in allow list)`);
298
- res.writeHead(200);
299
- res.end("ok");
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
- content: text,
324
- meta: {
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] Message from ${from}: ${text.slice(0, 80)}`);
331
- res.writeHead(200);
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
- server.listen(WEBHOOK_PORT, "127.0.0.1", () => {
342
- console.error(`[wahooks-channel] Webhook server listening on http://127.0.0.1:${WEBHOOK_PORT}`);
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
- // Start webhook receiver
353
- startWebhookServer();
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.2.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": ["dist", "manifest.json"],
15
- "keywords": ["wahooks", "whatsapp", "claude-code", "channel", "mcp"],
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
- "typescript": "^5.0.0",
27
- "@types/node": "^20.0.0"
36
+ "@types/node": "^20.0.0",
37
+ "@types/ws": "^8.18.1",
38
+ "typescript": "^5.0.0"
28
39
  }
29
40
  }