@wahooks/channel 0.1.0 → 0.3.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 CHANGED
@@ -6,13 +6,13 @@
6
6
  * Messages from WhatsApp appear as <channel> events; Claude replies
7
7
  * via the reply tool and messages are sent back through WhatsApp.
8
8
  *
9
- * Usage:
10
- * claude --dangerously-load-development-channels server:wahooks-channel
9
+ * Setup:
10
+ * /wahooks:configure <api-key>
11
11
  *
12
- * Environment:
12
+ * Or set environment variables:
13
13
  * WAHOOKS_API_KEY — WAHooks API token (wh_...)
14
14
  * WAHOOKS_API_URL — API base URL (default: https://api.wahooks.com)
15
- * WAHOOKS_CONNECTION — Connection ID to use (auto-detected if only one)
15
+ * WAHOOKS_CONNECTION — Connection ID (auto-detected if only one)
16
16
  * WAHOOKS_ALLOW — Comma-separated phone numbers to accept (empty = all)
17
17
  */
18
18
  export {};
package/dist/index.js CHANGED
@@ -6,30 +6,88 @@
6
6
  * Messages from WhatsApp appear as <channel> events; Claude replies
7
7
  * via the reply tool and messages are sent back through WhatsApp.
8
8
  *
9
- * Usage:
10
- * claude --dangerously-load-development-channels server:wahooks-channel
9
+ * Setup:
10
+ * /wahooks:configure <api-key>
11
11
  *
12
- * Environment:
12
+ * Or set environment variables:
13
13
  * WAHOOKS_API_KEY — WAHooks API token (wh_...)
14
14
  * WAHOOKS_API_URL — API base URL (default: https://api.wahooks.com)
15
- * WAHOOKS_CONNECTION — Connection ID to use (auto-detected if only one)
15
+ * WAHOOKS_CONNECTION — Connection ID (auto-detected if only one)
16
16
  * WAHOOKS_ALLOW — Comma-separated phone numbers to accept (empty = all)
17
17
  */
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
21
  import http from "node:http";
22
+ import fs from "node:fs";
23
+ import path from "node:path";
24
+ import os from "node:os";
22
25
  // ─── 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 ?? "")
26
+ const CONFIG_DIR = path.join(os.homedir(), ".claude", "channels", "wahooks");
27
+ const ENV_FILE = path.join(CONFIG_DIR, ".env");
28
+ /** Load config from ~/.claude/channels/wahooks/.env, then overlay env vars */
29
+ function loadConfig() {
30
+ const config = {};
31
+ // Read stored config file
32
+ if (fs.existsSync(ENV_FILE)) {
33
+ const lines = fs.readFileSync(ENV_FILE, "utf-8").split("\n");
34
+ for (const line of lines) {
35
+ const match = line.match(/^([A-Z_]+)=(.*)$/);
36
+ if (match)
37
+ config[match[1]] = match[2];
38
+ }
39
+ }
40
+ // Env vars override file config
41
+ for (const key of ["WAHOOKS_API_KEY", "WAHOOKS_API_URL", "WAHOOKS_CONNECTION", "WAHOOKS_ALLOW", "WAHOOKS_CHANNEL_PORT"]) {
42
+ if (process.env[key])
43
+ config[key] = process.env[key];
44
+ }
45
+ return config;
46
+ }
47
+ /** Save config to ~/.claude/channels/wahooks/.env */
48
+ function saveConfig(key, value) {
49
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
50
+ const config = {};
51
+ if (fs.existsSync(ENV_FILE)) {
52
+ const lines = fs.readFileSync(ENV_FILE, "utf-8").split("\n");
53
+ for (const line of lines) {
54
+ const match = line.match(/^([A-Z_]+)=(.*)$/);
55
+ if (match)
56
+ config[match[1]] = match[2];
57
+ }
58
+ }
59
+ config[key] = value;
60
+ const content = Object.entries(config)
61
+ .map(([k, v]) => `${k}=${v}`)
62
+ .join("\n");
63
+ fs.writeFileSync(ENV_FILE, content + "\n", { mode: 0o600 });
64
+ }
65
+ // Handle configure command: wahooks-channel --configure <api-key>
66
+ if (process.argv[2] === "--configure") {
67
+ const apiKey = process.argv[3];
68
+ if (!apiKey) {
69
+ console.error("Usage: wahooks-channel --configure <api-key>");
70
+ process.exit(1);
71
+ }
72
+ saveConfig("WAHOOKS_API_KEY", apiKey);
73
+ if (process.argv[4])
74
+ saveConfig("WAHOOKS_CONNECTION", process.argv[4]);
75
+ console.error(`Saved WAHooks API key to ${ENV_FILE}`);
76
+ process.exit(0);
77
+ }
78
+ const cfg = loadConfig();
79
+ const API_KEY = cfg.WAHOOKS_API_KEY ?? "";
80
+ const API_URL = (cfg.WAHOOKS_API_URL ?? "https://api.wahooks.com").replace(/\/$/, "");
81
+ const CONNECTION_ID = cfg.WAHOOKS_CONNECTION ?? "";
82
+ const ALLOW_LIST = new Set((cfg.WAHOOKS_ALLOW ?? "")
27
83
  .split(",")
28
84
  .map((s) => s.trim().replace(/\D/g, ""))
29
85
  .filter(Boolean));
30
- const WEBHOOK_PORT = parseInt(process.env.WAHOOKS_CHANNEL_PORT ?? "8790", 10);
86
+ const WEBHOOK_PORT = parseInt(cfg.WAHOOKS_CHANNEL_PORT ?? "8790", 10);
31
87
  if (!API_KEY) {
32
- console.error("[wahooks-channel] WAHOOKS_API_KEY is required");
88
+ console.error("[wahooks-channel] No API key found.");
89
+ console.error("[wahooks-channel] Run: wahooks-channel --configure <your-api-key>");
90
+ console.error("[wahooks-channel] Or set WAHOOKS_API_KEY environment variable");
33
91
  process.exit(1);
34
92
  }
35
93
  // ─── WAHooks API helpers ────────────────────────────────────────────────
@@ -68,21 +126,52 @@ async function resolveConnection() {
68
126
  }
69
127
  // ─── State ──────────────────────────────────────────────────────────────
70
128
  let connectionId;
71
- // Track inbound message → sender mapping for replies
72
- const messageToSender = new Map();
73
129
  // ─── MCP Server ─────────────────────────────────────────────────────────
74
130
  const mcp = new Server({ name: "wahooks-channel", version: "0.1.0" }, {
75
131
  capabilities: {
76
- experimental: { "claude/channel": {} },
132
+ experimental: {
133
+ "claude/channel": {},
134
+ "claude/channel/permission": {},
135
+ },
77
136
  tools: {},
78
137
  },
79
138
  instructions: [
80
139
  "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.",
140
+ "Use wahooks_reply to respond to the sender. Use wahooks_send to message any phone.",
141
+ "Media tools: wahooks_send_image, wahooks_send_video, wahooks_send_audio, wahooks_send_document.",
142
+ "Also available: wahooks_send_location (lat/lng) and wahooks_send_contact (name/phone).",
143
+ "For permission requests, the user can reply 'yes XXXXX' or 'no XXXXX' where XXXXX is the request ID.",
84
144
  ].join(" "),
85
145
  });
146
+ // ─── Permission relay ───────────────────────────────────────────────────
147
+ const PERMISSION_RE = /^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i;
148
+ // Track the last sender so we can forward permission requests
149
+ let lastSender = "";
150
+ // Listen for permission requests from Claude Code
151
+ const PermissionRequestSchema = {
152
+ method: "notifications/claude/channel/permission_request",
153
+ };
154
+ mcp.setNotificationHandler(PermissionRequestSchema, async (notification) => {
155
+ const params = notification.params;
156
+ if (!lastSender || !connectionId)
157
+ return;
158
+ // Forward the permission request to the WhatsApp user
159
+ const msg = [
160
+ `🔐 Claude wants to run: ${params.tool_name}`,
161
+ `${params.description}`,
162
+ ``,
163
+ `Reply "yes ${params.request_id}" to allow or "no ${params.request_id}" to deny`,
164
+ ].join("\n");
165
+ try {
166
+ await api("POST", `/connections/${connectionId}/send`, {
167
+ chatId: `${lastSender}@s.whatsapp.net`,
168
+ text: msg,
169
+ });
170
+ }
171
+ catch {
172
+ console.error("[wahooks-channel] Failed to forward permission request");
173
+ }
174
+ });
86
175
  // ─── Tools ──────────────────────────────────────────────────────────────
87
176
  mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
88
177
  tools: [
@@ -136,6 +225,59 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
136
225
  required: ["to", "url"],
137
226
  },
138
227
  },
228
+ {
229
+ name: "wahooks_send_video",
230
+ description: "Send a video via WhatsApp.",
231
+ inputSchema: {
232
+ type: "object",
233
+ properties: {
234
+ to: { type: "string", description: "Phone number" },
235
+ url: { type: "string", description: "Video URL" },
236
+ caption: { type: "string", description: "Optional caption" },
237
+ },
238
+ required: ["to", "url"],
239
+ },
240
+ },
241
+ {
242
+ name: "wahooks_send_audio",
243
+ description: "Send an audio/voice message via WhatsApp.",
244
+ inputSchema: {
245
+ type: "object",
246
+ properties: {
247
+ to: { type: "string", description: "Phone number" },
248
+ url: { type: "string", description: "Audio URL" },
249
+ },
250
+ required: ["to", "url"],
251
+ },
252
+ },
253
+ {
254
+ name: "wahooks_send_location",
255
+ description: "Send a location pin via WhatsApp.",
256
+ inputSchema: {
257
+ type: "object",
258
+ properties: {
259
+ to: { type: "string", description: "Phone number" },
260
+ latitude: { type: "number", description: "Latitude" },
261
+ longitude: { type: "number", description: "Longitude" },
262
+ name: { type: "string", description: "Location name" },
263
+ address: { type: "string", description: "Address" },
264
+ },
265
+ required: ["to", "latitude", "longitude"],
266
+ },
267
+ },
268
+ {
269
+ name: "wahooks_send_contact",
270
+ description: "Send a contact card via WhatsApp.",
271
+ inputSchema: {
272
+ type: "object",
273
+ properties: {
274
+ to: { type: "string", description: "Phone number" },
275
+ contact_name: { type: "string", description: "Contact's display name" },
276
+ contact_phone: { type: "string", description: "Contact's phone number" },
277
+ },
278
+ required: ["to", "contact_name", "contact_phone"],
279
+ },
280
+ },
139
281
  ],
140
282
  }));
141
283
  function toChatId(phone) {
@@ -169,6 +311,39 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
169
311
  });
170
312
  return { content: [{ type: "text", text: `Document sent to ${args.to}` }] };
171
313
  }
314
+ case "wahooks_send_video": {
315
+ await api("POST", `/connections/${connectionId}/send-video`, {
316
+ chatId: toChatId(args.to),
317
+ url: args.url,
318
+ caption: args.caption,
319
+ });
320
+ return { content: [{ type: "text", text: `Video sent to ${args.to}` }] };
321
+ }
322
+ case "wahooks_send_audio": {
323
+ await api("POST", `/connections/${connectionId}/send-audio`, {
324
+ chatId: toChatId(args.to),
325
+ url: args.url,
326
+ });
327
+ return { content: [{ type: "text", text: `Audio sent to ${args.to}` }] };
328
+ }
329
+ case "wahooks_send_location": {
330
+ await api("POST", `/connections/${connectionId}/send-location`, {
331
+ chatId: toChatId(args.to),
332
+ latitude: parseFloat(args.latitude),
333
+ longitude: parseFloat(args.longitude),
334
+ name: args.name,
335
+ address: args.address,
336
+ });
337
+ return { content: [{ type: "text", text: `Location sent to ${args.to}` }] };
338
+ }
339
+ case "wahooks_send_contact": {
340
+ await api("POST", `/connections/${connectionId}/send-contact`, {
341
+ chatId: toChatId(args.to),
342
+ contactName: args.contact_name,
343
+ contactPhone: args.contact_phone,
344
+ });
345
+ return { content: [{ type: "text", text: `Contact sent to ${args.to}` }] };
346
+ }
172
347
  default:
173
348
  throw new Error(`Unknown tool: ${req.params.name}`);
174
349
  }
@@ -210,8 +385,23 @@ function startWebhookServer() {
210
385
  res.end("ok");
211
386
  return;
212
387
  }
213
- // Track sender for replies
214
- messageToSender.set(messageId, from);
388
+ // Track last sender for permission relay
389
+ lastSender = from;
390
+ // Check if this is a permission verdict
391
+ const permMatch = PERMISSION_RE.exec(text);
392
+ if (permMatch) {
393
+ await mcp.notification({
394
+ method: "notifications/claude/channel/permission",
395
+ params: {
396
+ request_id: permMatch[2].toLowerCase(),
397
+ behavior: permMatch[1].toLowerCase().startsWith("y") ? "allow" : "deny",
398
+ },
399
+ });
400
+ console.error(`[wahooks-channel] Permission verdict: ${permMatch[1]} ${permMatch[2]}`);
401
+ res.writeHead(200);
402
+ res.end("ok");
403
+ return;
404
+ }
215
405
  // Forward to Claude Code
216
406
  await mcp.notification({
217
407
  method: "notifications/claude/channel",
@@ -236,25 +426,8 @@ function startWebhookServer() {
236
426
  });
237
427
  server.listen(WEBHOOK_PORT, "127.0.0.1", () => {
238
428
  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
429
  });
241
430
  }
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
431
  // ─── Main ───────────────────────────────────────────────────────────────
259
432
  async function main() {
260
433
  connectionId = await resolveConnection();
@@ -264,7 +437,6 @@ async function main() {
264
437
  await mcp.connect(transport);
265
438
  // Start webhook receiver
266
439
  startWebhookServer();
267
- await ensureWebhook();
268
440
  console.error("[wahooks-channel] Ready — WhatsApp messages will appear in Claude Code");
269
441
  }
270
442
  main().catch((err) => {
package/manifest.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "wahooks",
3
+ "version": "0.1.0",
4
+ "description": "WhatsApp channel for Claude Code — chat with Claude via WhatsApp using WAHooks",
5
+ "channels": [
6
+ {
7
+ "name": "wahooks-channel",
8
+ "command": "node",
9
+ "args": ["./dist/index.js"]
10
+ }
11
+ ],
12
+ "skills": [
13
+ {
14
+ "name": "configure",
15
+ "description": "Configure your WAHooks API key for the WhatsApp channel",
16
+ "command": "node",
17
+ "args": ["./dist/index.js", "--configure"]
18
+ }
19
+ ]
20
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wahooks/channel",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "WhatsApp channel for Claude Code — chat with Claude via WhatsApp",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,7 +11,7 @@
11
11
  "build": "tsc",
12
12
  "prepublishOnly": "npm run build"
13
13
  },
14
- "files": ["dist"],
14
+ "files": ["dist", "manifest.json"],
15
15
  "keywords": ["wahooks", "whatsapp", "claude-code", "channel", "mcp"],
16
16
  "license": "MIT",
17
17
  "repository": {