@wahooks/channel 0.1.0 → 0.2.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,12 +126,13 @@ 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: [
@@ -81,8 +140,38 @@ const mcp = new Server({ name: "wahooks-channel", version: "0.1.0" }, {
81
140
  "Use the wahooks_reply tool to send responses back. Pass the from phone number.",
82
141
  "Use wahooks_send_image / wahooks_send_document to send media.",
83
142
  "You can also proactively message any phone with wahooks_send.",
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: [
@@ -210,8 +299,23 @@ function startWebhookServer() {
210
299
  res.end("ok");
211
300
  return;
212
301
  }
213
- // Track sender for replies
214
- messageToSender.set(messageId, from);
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
+ }
215
319
  // Forward to Claude Code
216
320
  await mcp.notification({
217
321
  method: "notifications/claude/channel",
@@ -236,25 +340,8 @@ function startWebhookServer() {
236
340
  });
237
341
  server.listen(WEBHOOK_PORT, "127.0.0.1", () => {
238
342
  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
343
  });
241
344
  }
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
345
  // ─── Main ───────────────────────────────────────────────────────────────
259
346
  async function main() {
260
347
  connectionId = await resolveConnection();
@@ -264,7 +351,6 @@ async function main() {
264
351
  await mcp.connect(transport);
265
352
  // Start webhook receiver
266
353
  startWebhookServer();
267
- await ensureWebhook();
268
354
  console.error("[wahooks-channel] Ready — WhatsApp messages will appear in Claude Code");
269
355
  }
270
356
  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.2.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": {