@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 +4 -4
- package/dist/index.js +208 -36
- package/manifest.json +20 -0
- package/package.json +2 -2
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
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* Setup:
|
|
10
|
+
* /wahooks:configure <api-key>
|
|
11
11
|
*
|
|
12
|
-
*
|
|
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
|
|
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
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* Setup:
|
|
10
|
+
* /wahooks:configure <api-key>
|
|
11
11
|
*
|
|
12
|
-
*
|
|
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
|
|
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
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
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(
|
|
86
|
+
const WEBHOOK_PORT = parseInt(cfg.WAHOOKS_CHANNEL_PORT ?? "8790", 10);
|
|
31
87
|
if (!API_KEY) {
|
|
32
|
-
console.error("[wahooks-channel]
|
|
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: {
|
|
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
|
|
82
|
-
"
|
|
83
|
-
"
|
|
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
|
|
214
|
-
|
|
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.
|
|
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": {
|