@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 +4 -4
- package/dist/index.js +119 -33
- 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,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: {
|
|
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
|
|
214
|
-
|
|
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.
|
|
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": {
|