@paean-ai/claude-code-wechat 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-present Paean AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # @paean-ai/claude-code-wechat
2
+
3
+ Bridge WeChat messages into [Claude Code](https://docs.anthropic.com/en/docs/claude-code) sessions.
4
+
5
+ This plugin connects WeChat (via the official ClawBot ilink API) to Claude Code's [Channels](https://docs.anthropic.com/en/docs/claude-code) MCP protocol, so you can chat with Claude Code directly from WeChat.
6
+
7
+ ```
8
+ WeChat (iOS) --> ClawBot --> ilink API --> [this plugin] --> Claude Code
9
+ |
10
+ Claude Code <-- MCP Channel Protocol <-- wechat_reply tool
11
+ ```
12
+
13
+ ## Prerequisites
14
+
15
+ - [Node.js](https://nodejs.org/) >= 18 (or [Bun](https://bun.sh) >= 1.0)
16
+ - [Claude Code](https://docs.anthropic.com/en/docs/claude-code) >= 2.1.80
17
+ - A Claude.ai account (API key auth is not supported)
18
+ - WeChat iOS (latest version with ClawBot support)
19
+
20
+ ## Installation
21
+
22
+ Install globally with your preferred package manager:
23
+
24
+ ```bash
25
+ # npm
26
+ npm install -g @paean-ai/claude-code-wechat
27
+
28
+ # bun
29
+ bun add -g @paean-ai/claude-code-wechat
30
+
31
+ # pnpm
32
+ pnpm add -g @paean-ai/claude-code-wechat
33
+ ```
34
+
35
+ ## Quick Start
36
+
37
+ ### 1. Setup (one-time)
38
+
39
+ Authenticate with WeChat and register the MCP server:
40
+
41
+ ```bash
42
+ claude-wechat setup
43
+ ```
44
+
45
+ This will:
46
+ - Display a QR code in your terminal
47
+ - Wait for you to scan it with WeChat
48
+ - Save credentials to `~/.claude/channels/wechat/account.json`
49
+ - Register the channel server with Claude Code
50
+
51
+ ### 2. Start
52
+
53
+ Launch Claude Code with the WeChat channel:
54
+
55
+ ```bash
56
+ claude-wechat
57
+ ```
58
+
59
+ That's it! Open WeChat, find the ClawBot conversation, and start chatting. Messages will appear in your Claude Code terminal, and Claude's replies will be sent back to WeChat automatically.
60
+
61
+ ## CLI Reference
62
+
63
+ | Command | Description |
64
+ |---|---|
65
+ | `claude-wechat setup` | Authenticate with WeChat via QR code and register the MCP server |
66
+ | `claude-wechat start` | Launch Claude Code with the WeChat channel (default command) |
67
+ | `claude-wechat status` | Show current login and configuration status |
68
+ | `claude-wechat logout` | Remove saved credentials and unregister MCP server |
69
+ | `claude-wechat --help` | Show help |
70
+ | `claude-wechat --version` | Show version |
71
+
72
+ ### Options
73
+
74
+ `claude-wechat setup --force` — Skip the confirmation prompt when re-authenticating.
75
+
76
+ ## How It Works
77
+
78
+ 1. **Authentication**: The `setup` command uses WeChat's ClawBot QR code login flow (`ilink/bot/get_bot_qrcode` + `get_qrcode_status`) to obtain a bearer token.
79
+
80
+ 2. **Message Receiving**: The channel server long-polls `ilink/bot/getupdates` for incoming WeChat messages and forwards them to Claude Code as MCP channel notifications.
81
+
82
+ 3. **Message Sending**: Claude Code uses the `wechat_reply` tool (exposed via MCP) to send replies back through `ilink/bot/sendmessage`.
83
+
84
+ 4. **MCP Integration**: The server implements Claude Code's experimental Channels protocol, registering as a `wechat` channel with tool capabilities.
85
+
86
+ ## Configuration
87
+
88
+ Credentials are stored at:
89
+
90
+ ```
91
+ ~/.claude/channels/wechat/account.json
92
+ ```
93
+
94
+ The file has `0600` permissions (owner-readable only). No credentials are stored in the package itself.
95
+
96
+ ## Programmatic Usage
97
+
98
+ The package also exports its core API for advanced use cases:
99
+
100
+ ```typescript
101
+ import {
102
+ loadCredentials,
103
+ fetchQRCode,
104
+ pollQRStatus,
105
+ sendTextMessage,
106
+ } from "@paean-ai/claude-code-wechat";
107
+ ```
108
+
109
+ ## Notes
110
+
111
+ - This is a **research preview** feature. The `--dangerously-load-development-channels` flag is required by Claude Code for channel plugins.
112
+ - The Claude Code session and WeChat channel share the same lifecycle — closing Claude Code disconnects the channel.
113
+ - WeChat ClawBot currently supports iOS only.
114
+ - Each ClawBot instance can connect to one agent at a time.
115
+
116
+ ## Troubleshooting
117
+
118
+ **"Claude Code CLI not found"** — Make sure `claude` is installed and available in your PATH. See [Claude Code docs](https://docs.anthropic.com/en/docs/claude-code).
119
+
120
+ **QR code expired** — The QR code is valid for ~8 minutes. Run `claude-wechat setup` again to get a fresh one.
121
+
122
+ **Messages not arriving** — Check `claude-wechat status` to verify credentials and MCP registration. Try `claude-wechat logout` followed by `claude-wechat setup` to re-authenticate.
123
+
124
+ ## License
125
+
126
+ [MIT](LICENSE)
127
+
128
+ ## Disclaimer
129
+
130
+ "Claude" and "Claude Code" are trademarks of Anthropic, PBC. "WeChat" (微信) is a trademark of Tencent Holdings Limited. This project is not affiliated with, endorsed by, or sponsored by Anthropic or Tencent. It is an independent open-source community project.
@@ -0,0 +1,448 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/channel.ts
4
+ import "fs";
5
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import {
8
+ ListToolsRequestSchema,
9
+ CallToolRequestSchema
10
+ } from "@modelcontextprotocol/sdk/types.js";
11
+
12
+ // src/constants.ts
13
+ import path from "path";
14
+ var CHANNEL_NAME = "wechat";
15
+ var CHANNEL_VERSION = "0.1.0";
16
+ var DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com";
17
+ var BOT_TYPE = "3";
18
+ var CREDENTIALS_DIR = path.join(
19
+ process.env.HOME || "~",
20
+ ".claude",
21
+ "channels",
22
+ "wechat"
23
+ );
24
+ var CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, "account.json");
25
+ var SYNC_BUF_FILE = path.join(CREDENTIALS_DIR, "sync_buf.txt");
26
+ var LONG_POLL_TIMEOUT_MS = 35e3;
27
+ var MAX_CONSECUTIVE_FAILURES = 3;
28
+ var BACKOFF_DELAY_MS = 3e4;
29
+ var RETRY_DELAY_MS = 2e3;
30
+ var QR_POLL_TIMEOUT_MS = 35e3;
31
+ var SEND_TIMEOUT_MS = 15e3;
32
+ var MSG_TYPE_USER = 1;
33
+ var MSG_ITEM_TEXT = 1;
34
+ var MSG_ITEM_VOICE = 3;
35
+ var MSG_TYPE_BOT = 2;
36
+ var MSG_STATE_FINISH = 2;
37
+
38
+ // src/credentials.ts
39
+ import fs from "fs";
40
+ function loadCredentials() {
41
+ try {
42
+ if (!fs.existsSync(CREDENTIALS_FILE)) return null;
43
+ return JSON.parse(fs.readFileSync(CREDENTIALS_FILE, "utf-8"));
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+ function saveCredentials(data) {
49
+ fs.mkdirSync(CREDENTIALS_DIR, { recursive: true });
50
+ fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(data, null, 2), "utf-8");
51
+ try {
52
+ fs.chmodSync(CREDENTIALS_FILE, 384);
53
+ } catch {
54
+ }
55
+ }
56
+ function loadSyncBuf() {
57
+ try {
58
+ if (fs.existsSync(SYNC_BUF_FILE)) {
59
+ return fs.readFileSync(SYNC_BUF_FILE, "utf-8");
60
+ }
61
+ } catch {
62
+ }
63
+ return "";
64
+ }
65
+ function saveSyncBuf(buf) {
66
+ try {
67
+ fs.mkdirSync(CREDENTIALS_DIR, { recursive: true });
68
+ fs.writeFileSync(SYNC_BUF_FILE, buf, "utf-8");
69
+ } catch {
70
+ }
71
+ }
72
+
73
+ // src/api.ts
74
+ import crypto from "crypto";
75
+ function randomWechatUin() {
76
+ const uint32 = crypto.randomBytes(4).readUInt32BE(0);
77
+ return Buffer.from(String(uint32), "utf-8").toString("base64");
78
+ }
79
+ function buildHeaders(token, body) {
80
+ const headers = {
81
+ "Content-Type": "application/json",
82
+ AuthorizationType: "ilink_bot_token",
83
+ "X-WECHAT-UIN": randomWechatUin()
84
+ };
85
+ if (body) {
86
+ headers["Content-Length"] = String(Buffer.byteLength(body, "utf-8"));
87
+ }
88
+ if (token?.trim()) {
89
+ headers.Authorization = `Bearer ${token.trim()}`;
90
+ }
91
+ return headers;
92
+ }
93
+ function normalizeBaseUrl(baseUrl) {
94
+ return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
95
+ }
96
+ async function apiFetch(params) {
97
+ const url = new URL(params.endpoint, normalizeBaseUrl(params.baseUrl)).toString();
98
+ const headers = buildHeaders(params.token, params.body);
99
+ const controller = new AbortController();
100
+ const timer = setTimeout(() => controller.abort(), params.timeoutMs);
101
+ try {
102
+ const res = await fetch(url, {
103
+ method: "POST",
104
+ headers,
105
+ body: params.body,
106
+ signal: controller.signal
107
+ });
108
+ clearTimeout(timer);
109
+ const text = await res.text();
110
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${text}`);
111
+ return text;
112
+ } catch (err) {
113
+ clearTimeout(timer);
114
+ throw err;
115
+ }
116
+ }
117
+ async function fetchQRCode(baseUrl) {
118
+ const url = new URL(
119
+ `ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(BOT_TYPE)}`,
120
+ normalizeBaseUrl(baseUrl)
121
+ );
122
+ const res = await fetch(url.toString());
123
+ if (!res.ok) throw new Error(`QR fetch failed: ${res.status}`);
124
+ return await res.json();
125
+ }
126
+ async function pollQRStatus(baseUrl, qrcode) {
127
+ const url = new URL(
128
+ `ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`,
129
+ normalizeBaseUrl(baseUrl)
130
+ );
131
+ const controller = new AbortController();
132
+ const timer = setTimeout(() => controller.abort(), QR_POLL_TIMEOUT_MS);
133
+ try {
134
+ const res = await fetch(url.toString(), {
135
+ headers: { "iLink-App-ClientVersion": "1" },
136
+ signal: controller.signal
137
+ });
138
+ clearTimeout(timer);
139
+ if (!res.ok) throw new Error(`QR status failed: ${res.status}`);
140
+ return await res.json();
141
+ } catch (err) {
142
+ clearTimeout(timer);
143
+ if (err instanceof Error && err.name === "AbortError") {
144
+ return { status: "wait" };
145
+ }
146
+ throw err;
147
+ }
148
+ }
149
+ async function getUpdates(baseUrl, token, getUpdatesBuf) {
150
+ try {
151
+ const raw = await apiFetch({
152
+ baseUrl,
153
+ endpoint: "ilink/bot/getupdates",
154
+ body: JSON.stringify({
155
+ get_updates_buf: getUpdatesBuf,
156
+ base_info: { channel_version: CHANNEL_VERSION }
157
+ }),
158
+ token,
159
+ timeoutMs: LONG_POLL_TIMEOUT_MS
160
+ });
161
+ return JSON.parse(raw);
162
+ } catch (err) {
163
+ if (err instanceof Error && err.name === "AbortError") {
164
+ return { ret: 0, msgs: [], get_updates_buf: getUpdatesBuf };
165
+ }
166
+ throw err;
167
+ }
168
+ }
169
+ function generateClientId() {
170
+ return `claude-wechat:${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
171
+ }
172
+ async function sendTextMessage(baseUrl, token, to, text, contextToken) {
173
+ const clientId = generateClientId();
174
+ await apiFetch({
175
+ baseUrl,
176
+ endpoint: "ilink/bot/sendmessage",
177
+ body: JSON.stringify({
178
+ msg: {
179
+ from_user_id: "",
180
+ to_user_id: to,
181
+ client_id: clientId,
182
+ message_type: MSG_TYPE_BOT,
183
+ message_state: MSG_STATE_FINISH,
184
+ item_list: [{ type: MSG_ITEM_TEXT, text_item: { text } }],
185
+ context_token: contextToken
186
+ },
187
+ base_info: { channel_version: CHANNEL_VERSION }
188
+ }),
189
+ token,
190
+ timeoutMs: SEND_TIMEOUT_MS
191
+ });
192
+ return clientId;
193
+ }
194
+ function extractTextFromMessage(msg) {
195
+ if (!msg.item_list?.length) return "";
196
+ for (const item of msg.item_list) {
197
+ if (item.type === MSG_ITEM_TEXT && item.text_item?.text) {
198
+ const text = item.text_item.text;
199
+ const ref = item.ref_msg;
200
+ if (!ref) return text;
201
+ const parts = [];
202
+ if (ref.title) parts.push(ref.title);
203
+ if (!parts.length) return text;
204
+ return `[Quote: ${parts.join(" | ")}]
205
+ ${text}`;
206
+ }
207
+ if (item.type === MSG_ITEM_VOICE && item.voice_item?.text) {
208
+ return item.voice_item.text;
209
+ }
210
+ }
211
+ return "";
212
+ }
213
+
214
+ // src/logger.ts
215
+ var PREFIX = "[claude-wechat]";
216
+ function log(msg) {
217
+ process.stderr.write(`${PREFIX} ${msg}
218
+ `);
219
+ }
220
+ function logError(msg) {
221
+ process.stderr.write(`${PREFIX} ERROR: ${msg}
222
+ `);
223
+ }
224
+
225
+ // src/channel.ts
226
+ var contextTokenCache = /* @__PURE__ */ new Map();
227
+ async function doQRLogin(baseUrl) {
228
+ log("Fetching WeChat login QR code...");
229
+ const qrResp = await fetchQRCode(baseUrl);
230
+ log("\nScan the QR code below with WeChat:\n");
231
+ try {
232
+ const qrterm = await import("qrcode-terminal");
233
+ await new Promise((resolve) => {
234
+ qrterm.default.generate(
235
+ qrResp.qrcode_img_content,
236
+ { small: true },
237
+ (qr) => {
238
+ process.stderr.write(qr + "\n");
239
+ resolve();
240
+ }
241
+ );
242
+ });
243
+ } catch {
244
+ log(`QR code URL: ${qrResp.qrcode_img_content}`);
245
+ }
246
+ log("Waiting for scan...");
247
+ const deadline = Date.now() + 48e4;
248
+ let scannedLogged = false;
249
+ while (Date.now() < deadline) {
250
+ const status = await pollQRStatus(baseUrl, qrResp.qrcode);
251
+ switch (status.status) {
252
+ case "wait":
253
+ break;
254
+ case "scaned":
255
+ if (!scannedLogged) {
256
+ log("Scanned! Please confirm on your phone...");
257
+ scannedLogged = true;
258
+ }
259
+ break;
260
+ case "expired":
261
+ log("QR code expired. Please restart.");
262
+ return null;
263
+ case "confirmed": {
264
+ if (!status.ilink_bot_id || !status.bot_token) {
265
+ logError("Login confirmed but server did not return bot info");
266
+ return null;
267
+ }
268
+ const account = {
269
+ token: status.bot_token,
270
+ baseUrl: status.baseurl || baseUrl,
271
+ accountId: status.ilink_bot_id,
272
+ userId: status.ilink_user_id,
273
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
274
+ };
275
+ saveCredentials(account);
276
+ log("WeChat connected successfully!");
277
+ return account;
278
+ }
279
+ }
280
+ await new Promise((r) => setTimeout(r, 1e3));
281
+ }
282
+ log("Login timed out");
283
+ return null;
284
+ }
285
+ var mcp = new Server(
286
+ { name: CHANNEL_NAME, version: CHANNEL_VERSION },
287
+ {
288
+ capabilities: {
289
+ experimental: { "claude/channel": {} },
290
+ tools: {}
291
+ },
292
+ instructions: [
293
+ `Messages from WeChat users arrive as <channel source="wechat" sender="..." sender_id="...">`,
294
+ "Reply using the wechat_reply tool. You MUST pass the sender_id from the inbound tag.",
295
+ "Messages are from real WeChat users via the WeChat ClawBot interface.",
296
+ "Respond naturally in Chinese unless the user writes in another language.",
297
+ "Keep replies concise \u2014 WeChat is a chat app, not an essay platform.",
298
+ "Strip markdown formatting (WeChat doesn't render it). Use plain text."
299
+ ].join("\n")
300
+ }
301
+ );
302
+ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
303
+ tools: [
304
+ {
305
+ name: "wechat_reply",
306
+ description: "Send a text reply back to the WeChat user",
307
+ inputSchema: {
308
+ type: "object",
309
+ properties: {
310
+ sender_id: {
311
+ type: "string",
312
+ description: "The sender_id from the inbound <channel> tag (xxx@im.wechat format)"
313
+ },
314
+ text: {
315
+ type: "string",
316
+ description: "The plain-text message to send (no markdown)"
317
+ }
318
+ },
319
+ required: ["sender_id", "text"]
320
+ }
321
+ }
322
+ ]
323
+ }));
324
+ var activeAccount = null;
325
+ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
326
+ if (req.params.name === "wechat_reply") {
327
+ const { sender_id, text } = req.params.arguments;
328
+ if (!activeAccount) {
329
+ return {
330
+ content: [{ type: "text", text: "error: not logged in" }]
331
+ };
332
+ }
333
+ const contextToken = contextTokenCache.get(sender_id);
334
+ if (!contextToken) {
335
+ return {
336
+ content: [
337
+ {
338
+ type: "text",
339
+ text: `error: no context_token for ${sender_id}. The user may need to send a message first.`
340
+ }
341
+ ]
342
+ };
343
+ }
344
+ try {
345
+ await sendTextMessage(
346
+ activeAccount.baseUrl,
347
+ activeAccount.token,
348
+ sender_id,
349
+ text,
350
+ contextToken
351
+ );
352
+ return { content: [{ type: "text", text: "sent" }] };
353
+ } catch (err) {
354
+ return {
355
+ content: [
356
+ { type: "text", text: `send failed: ${String(err)}` }
357
+ ]
358
+ };
359
+ }
360
+ }
361
+ throw new Error(`unknown tool: ${req.params.name}`);
362
+ });
363
+ async function startPolling(account) {
364
+ const { baseUrl, token } = account;
365
+ let getUpdatesBuf = loadSyncBuf();
366
+ let consecutiveFailures = 0;
367
+ if (getUpdatesBuf) {
368
+ log(`Restored sync state (${getUpdatesBuf.length} bytes)`);
369
+ }
370
+ log("Listening for WeChat messages...");
371
+ while (true) {
372
+ try {
373
+ const resp = await getUpdates(baseUrl, token, getUpdatesBuf);
374
+ const isError = resp.ret !== void 0 && resp.ret !== 0 || resp.errcode !== void 0 && resp.errcode !== 0;
375
+ if (isError) {
376
+ consecutiveFailures++;
377
+ logError(
378
+ `getUpdates failed: ret=${resp.ret} errcode=${resp.errcode} errmsg=${resp.errmsg ?? ""}`
379
+ );
380
+ if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
381
+ logError(
382
+ `${MAX_CONSECUTIVE_FAILURES} consecutive failures, backing off ${BACKOFF_DELAY_MS / 1e3}s`
383
+ );
384
+ consecutiveFailures = 0;
385
+ await new Promise((r) => setTimeout(r, BACKOFF_DELAY_MS));
386
+ } else {
387
+ await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
388
+ }
389
+ continue;
390
+ }
391
+ consecutiveFailures = 0;
392
+ if (resp.get_updates_buf) {
393
+ getUpdatesBuf = resp.get_updates_buf;
394
+ saveSyncBuf(getUpdatesBuf);
395
+ }
396
+ for (const msg of resp.msgs ?? []) {
397
+ if (msg.message_type !== MSG_TYPE_USER) continue;
398
+ const text = extractTextFromMessage(msg);
399
+ if (!text) continue;
400
+ const senderId = msg.from_user_id ?? "unknown";
401
+ if (msg.context_token) {
402
+ contextTokenCache.set(senderId, msg.context_token);
403
+ }
404
+ log(`Message received: from=${senderId} text=${text.slice(0, 50)}...`);
405
+ await mcp.notification({
406
+ method: "notifications/claude/channel",
407
+ params: {
408
+ content: text,
409
+ meta: {
410
+ sender: senderId.split("@")[0] || senderId,
411
+ sender_id: senderId
412
+ }
413
+ }
414
+ });
415
+ }
416
+ } catch (err) {
417
+ consecutiveFailures++;
418
+ logError(`Poll error: ${String(err)}`);
419
+ if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
420
+ consecutiveFailures = 0;
421
+ await new Promise((r) => setTimeout(r, BACKOFF_DELAY_MS));
422
+ } else {
423
+ await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
424
+ }
425
+ }
426
+ }
427
+ }
428
+ async function main() {
429
+ await mcp.connect(new StdioServerTransport());
430
+ log("MCP connection ready");
431
+ let account = loadCredentials();
432
+ if (!account) {
433
+ log("No saved credentials found, starting QR login...");
434
+ account = await doQRLogin(DEFAULT_BASE_URL);
435
+ if (!account) {
436
+ logError("Login failed, exiting.");
437
+ process.exit(1);
438
+ }
439
+ } else {
440
+ log(`Using saved account: ${account.accountId}`);
441
+ }
442
+ activeAccount = account;
443
+ await startPolling(account);
444
+ }
445
+ main().catch((err) => {
446
+ logError(`Fatal: ${String(err)}`);
447
+ process.exit(1);
448
+ });
package/dist/cli.js ADDED
@@ -0,0 +1,312 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { execFileSync, execSync } from "child_process";
5
+ import "fs";
6
+ import path2 from "path";
7
+ import { fileURLToPath } from "url";
8
+ import { Command } from "commander";
9
+
10
+ // src/constants.ts
11
+ import path from "path";
12
+ var CHANNEL_VERSION = "0.1.0";
13
+ var DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com";
14
+ var BOT_TYPE = "3";
15
+ var CREDENTIALS_DIR = path.join(
16
+ process.env.HOME || "~",
17
+ ".claude",
18
+ "channels",
19
+ "wechat"
20
+ );
21
+ var CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, "account.json");
22
+ var SYNC_BUF_FILE = path.join(CREDENTIALS_DIR, "sync_buf.txt");
23
+ var QR_LOGIN_TIMEOUT_MS = 48e4;
24
+ var QR_POLL_TIMEOUT_MS = 35e3;
25
+
26
+ // src/credentials.ts
27
+ import fs from "fs";
28
+ function loadCredentials() {
29
+ try {
30
+ if (!fs.existsSync(CREDENTIALS_FILE)) return null;
31
+ return JSON.parse(fs.readFileSync(CREDENTIALS_FILE, "utf-8"));
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+ function saveCredentials(data) {
37
+ fs.mkdirSync(CREDENTIALS_DIR, { recursive: true });
38
+ fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(data, null, 2), "utf-8");
39
+ try {
40
+ fs.chmodSync(CREDENTIALS_FILE, 384);
41
+ } catch {
42
+ }
43
+ }
44
+ function removeCredentials() {
45
+ try {
46
+ if (fs.existsSync(CREDENTIALS_FILE)) fs.unlinkSync(CREDENTIALS_FILE);
47
+ } catch {
48
+ }
49
+ try {
50
+ if (fs.existsSync(SYNC_BUF_FILE)) fs.unlinkSync(SYNC_BUF_FILE);
51
+ } catch {
52
+ }
53
+ }
54
+
55
+ // src/api.ts
56
+ import crypto from "crypto";
57
+ function normalizeBaseUrl(baseUrl) {
58
+ return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
59
+ }
60
+ async function fetchQRCode(baseUrl) {
61
+ const url = new URL(
62
+ `ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(BOT_TYPE)}`,
63
+ normalizeBaseUrl(baseUrl)
64
+ );
65
+ const res = await fetch(url.toString());
66
+ if (!res.ok) throw new Error(`QR fetch failed: ${res.status}`);
67
+ return await res.json();
68
+ }
69
+ async function pollQRStatus(baseUrl, qrcode) {
70
+ const url = new URL(
71
+ `ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`,
72
+ normalizeBaseUrl(baseUrl)
73
+ );
74
+ const controller = new AbortController();
75
+ const timer = setTimeout(() => controller.abort(), QR_POLL_TIMEOUT_MS);
76
+ try {
77
+ const res = await fetch(url.toString(), {
78
+ headers: { "iLink-App-ClientVersion": "1" },
79
+ signal: controller.signal
80
+ });
81
+ clearTimeout(timer);
82
+ if (!res.ok) throw new Error(`QR status failed: ${res.status}`);
83
+ return await res.json();
84
+ } catch (err) {
85
+ clearTimeout(timer);
86
+ if (err instanceof Error && err.name === "AbortError") {
87
+ return { status: "wait" };
88
+ }
89
+ throw err;
90
+ }
91
+ }
92
+
93
+ // src/cli.ts
94
+ var __dirname = path2.dirname(fileURLToPath(import.meta.url));
95
+ function getServerBinPath() {
96
+ return path2.resolve(__dirname, "channel.js");
97
+ }
98
+ function claudeAvailable() {
99
+ try {
100
+ execSync("claude --version", { stdio: "ignore" });
101
+ return true;
102
+ } catch {
103
+ return false;
104
+ }
105
+ }
106
+ function isMcpServerRegistered() {
107
+ try {
108
+ const output = execSync("claude mcp get wechat", {
109
+ encoding: "utf-8",
110
+ stdio: ["ignore", "pipe", "ignore"]
111
+ });
112
+ return output.includes("wechat");
113
+ } catch {
114
+ return false;
115
+ }
116
+ }
117
+ function registerMcpServer() {
118
+ const serverPath = getServerBinPath();
119
+ try {
120
+ execSync("claude mcp remove wechat --scope user", {
121
+ stdio: "ignore"
122
+ });
123
+ } catch {
124
+ }
125
+ execFileSync("claude", [
126
+ "mcp",
127
+ "add",
128
+ "--scope",
129
+ "user",
130
+ "wechat",
131
+ "--",
132
+ "node",
133
+ serverPath
134
+ ], { stdio: "inherit" });
135
+ }
136
+ function unregisterMcpServer() {
137
+ try {
138
+ execSync("claude mcp remove wechat --scope user", { stdio: "inherit" });
139
+ } catch {
140
+ }
141
+ }
142
+ async function doInteractiveQRLogin() {
143
+ console.log("Fetching WeChat login QR code...\n");
144
+ const qrResp = await fetchQRCode(DEFAULT_BASE_URL);
145
+ try {
146
+ const qrterm = await import("qrcode-terminal");
147
+ await new Promise((resolve) => {
148
+ qrterm.default.generate(
149
+ qrResp.qrcode_img_content,
150
+ { small: true },
151
+ (qr) => {
152
+ console.log(qr);
153
+ resolve();
154
+ }
155
+ );
156
+ });
157
+ } catch {
158
+ console.log(`Open this URL to scan: ${qrResp.qrcode_img_content}
159
+ `);
160
+ }
161
+ console.log("Scan the QR code above with WeChat...\n");
162
+ const deadline = Date.now() + QR_LOGIN_TIMEOUT_MS;
163
+ let scannedPrinted = false;
164
+ while (Date.now() < deadline) {
165
+ const status = await pollQRStatus(DEFAULT_BASE_URL, qrResp.qrcode);
166
+ switch (status.status) {
167
+ case "wait":
168
+ process.stdout.write(".");
169
+ break;
170
+ case "scaned":
171
+ if (!scannedPrinted) {
172
+ console.log("\nScanned! Please confirm on your phone...");
173
+ scannedPrinted = true;
174
+ }
175
+ break;
176
+ case "expired":
177
+ console.error("\nQR code expired. Please run setup again.");
178
+ return null;
179
+ case "confirmed": {
180
+ if (!status.ilink_bot_id || !status.bot_token) {
181
+ console.error("\nLogin failed: server did not return complete info.");
182
+ return null;
183
+ }
184
+ const account = {
185
+ token: status.bot_token,
186
+ baseUrl: status.baseurl || DEFAULT_BASE_URL,
187
+ accountId: status.ilink_bot_id,
188
+ userId: status.ilink_user_id,
189
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
190
+ };
191
+ saveCredentials(account);
192
+ return account;
193
+ }
194
+ }
195
+ await new Promise((r) => setTimeout(r, 1e3));
196
+ }
197
+ console.error("\nLogin timed out. Please try again.");
198
+ return null;
199
+ }
200
+ async function setupCommand(opts) {
201
+ if (!claudeAvailable()) {
202
+ console.error("Error: Claude Code CLI not found. Please install it first:");
203
+ console.error(" https://docs.anthropic.com/en/docs/claude-code");
204
+ process.exit(1);
205
+ }
206
+ const existing = loadCredentials();
207
+ if (existing && !opts.force) {
208
+ console.log(`Existing account found: ${existing.accountId}`);
209
+ console.log(`Saved at: ${existing.savedAt}`);
210
+ console.log();
211
+ const readline = await import("readline");
212
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
213
+ const answer = await new Promise((resolve) => {
214
+ rl.question("Re-authenticate? (y/N) ", resolve);
215
+ });
216
+ rl.close();
217
+ if (answer.toLowerCase() !== "y") {
218
+ console.log("Keeping existing credentials.");
219
+ if (!isMcpServerRegistered()) {
220
+ console.log("\nRegistering MCP server...");
221
+ registerMcpServer();
222
+ console.log("MCP server registered.");
223
+ }
224
+ return;
225
+ }
226
+ }
227
+ const account = await doInteractiveQRLogin();
228
+ if (!account) {
229
+ process.exit(1);
230
+ }
231
+ console.log(`
232
+ WeChat connected successfully!`);
233
+ console.log(` Account ID: ${account.accountId}`);
234
+ console.log(` User ID: ${account.userId ?? "N/A"}`);
235
+ console.log(` Saved to: ${CREDENTIALS_FILE}`);
236
+ console.log();
237
+ console.log("Registering MCP server with Claude Code...");
238
+ registerMcpServer();
239
+ console.log("Done!\n");
240
+ console.log("You can now start Claude Code with the WeChat channel:");
241
+ console.log(" claude-wechat start");
242
+ }
243
+ function startCommand() {
244
+ if (!claudeAvailable()) {
245
+ console.error("Error: Claude Code CLI not found. Please install it first:");
246
+ console.error(" https://docs.anthropic.com/en/docs/claude-code");
247
+ process.exit(1);
248
+ }
249
+ const account = loadCredentials();
250
+ if (!account) {
251
+ console.error("No WeChat credentials found. Run setup first:");
252
+ console.error(" claude-wechat setup");
253
+ process.exit(1);
254
+ }
255
+ if (!isMcpServerRegistered()) {
256
+ console.log("MCP server not registered. Registering...");
257
+ registerMcpServer();
258
+ console.log("Registered.\n");
259
+ }
260
+ console.log("Starting Claude Code with WeChat channel...\n");
261
+ try {
262
+ execFileSync("claude", [
263
+ "--dangerously-load-development-channels",
264
+ "server:wechat"
265
+ ], { stdio: "inherit" });
266
+ } catch (err) {
267
+ const code = err.status;
268
+ process.exit(code ?? 1);
269
+ }
270
+ }
271
+ function statusCommand() {
272
+ console.log("Claude Code WeChat Channel Status\n");
273
+ const account = loadCredentials();
274
+ if (account) {
275
+ console.log(` Logged in: Yes`);
276
+ console.log(` Account ID: ${account.accountId}`);
277
+ console.log(` User ID: ${account.userId ?? "N/A"}`);
278
+ console.log(` Saved at: ${account.savedAt}`);
279
+ console.log(` Credentials: ${CREDENTIALS_FILE}`);
280
+ } else {
281
+ console.log(` Logged in: No`);
282
+ }
283
+ console.log();
284
+ if (claudeAvailable()) {
285
+ console.log(` Claude Code: Installed`);
286
+ const registered = isMcpServerRegistered();
287
+ console.log(` MCP server: ${registered ? "Registered" : "Not registered"}`);
288
+ } else {
289
+ console.log(` Claude Code: Not found`);
290
+ }
291
+ }
292
+ function logoutCommand() {
293
+ const account = loadCredentials();
294
+ if (!account) {
295
+ console.log("No credentials found. Already logged out.");
296
+ return;
297
+ }
298
+ removeCredentials();
299
+ console.log("Credentials removed.");
300
+ if (claudeAvailable()) {
301
+ unregisterMcpServer();
302
+ console.log("MCP server unregistered.");
303
+ }
304
+ console.log("Logged out successfully.");
305
+ }
306
+ var program = new Command();
307
+ program.name("claude-wechat").description("WeChat channel plugin for Claude Code").version(CHANNEL_VERSION);
308
+ program.command("setup").description("Authenticate with WeChat via QR code and register the MCP server").option("-f, --force", "Skip confirmation prompt for re-authentication").action(setupCommand);
309
+ program.command("start", { isDefault: true }).description("Launch Claude Code with the WeChat channel").action(startCommand);
310
+ program.command("status").description("Show current login and configuration status").action(statusCommand);
311
+ program.command("logout").description("Remove saved credentials and MCP registration").action(logoutCommand);
312
+ program.parse();
@@ -0,0 +1,70 @@
1
+ interface AccountData {
2
+ token: string;
3
+ baseUrl: string;
4
+ accountId: string;
5
+ userId?: string;
6
+ savedAt: string;
7
+ }
8
+ interface QRCodeResponse {
9
+ qrcode: string;
10
+ qrcode_img_content: string;
11
+ }
12
+ interface QRStatusResponse {
13
+ status: "wait" | "scaned" | "confirmed" | "expired";
14
+ bot_token?: string;
15
+ ilink_bot_id?: string;
16
+ baseurl?: string;
17
+ ilink_user_id?: string;
18
+ }
19
+ interface TextItem {
20
+ text?: string;
21
+ }
22
+ interface RefMessage {
23
+ message_item?: MessageItem;
24
+ title?: string;
25
+ }
26
+ interface MessageItem {
27
+ type?: number;
28
+ text_item?: TextItem;
29
+ voice_item?: {
30
+ text?: string;
31
+ };
32
+ ref_msg?: RefMessage;
33
+ }
34
+ interface WeixinMessage {
35
+ from_user_id?: string;
36
+ to_user_id?: string;
37
+ client_id?: string;
38
+ session_id?: string;
39
+ message_type?: number;
40
+ message_state?: number;
41
+ item_list?: MessageItem[];
42
+ context_token?: string;
43
+ create_time_ms?: number;
44
+ }
45
+ interface GetUpdatesResp {
46
+ ret?: number;
47
+ errcode?: number;
48
+ errmsg?: string;
49
+ msgs?: WeixinMessage[];
50
+ get_updates_buf?: string;
51
+ longpolling_timeout_ms?: number;
52
+ }
53
+
54
+ declare function loadCredentials(): AccountData | null;
55
+ declare function saveCredentials(data: AccountData): void;
56
+ declare function removeCredentials(): void;
57
+
58
+ declare function fetchQRCode(baseUrl: string): Promise<QRCodeResponse>;
59
+ declare function pollQRStatus(baseUrl: string, qrcode: string): Promise<QRStatusResponse>;
60
+ declare function getUpdates(baseUrl: string, token: string, getUpdatesBuf: string): Promise<GetUpdatesResp>;
61
+ declare function sendTextMessage(baseUrl: string, token: string, to: string, text: string, contextToken: string): Promise<string>;
62
+ declare function extractTextFromMessage(msg: WeixinMessage): string;
63
+
64
+ declare const CHANNEL_NAME = "wechat";
65
+ declare const CHANNEL_VERSION = "0.1.0";
66
+ declare const DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com";
67
+ declare const CREDENTIALS_DIR: string;
68
+ declare const CREDENTIALS_FILE: string;
69
+
70
+ export { type AccountData, CHANNEL_NAME, CHANNEL_VERSION, CREDENTIALS_DIR, CREDENTIALS_FILE, DEFAULT_BASE_URL, type GetUpdatesResp, type QRCodeResponse, type QRStatusResponse, type WeixinMessage, extractTextFromMessage, fetchQRCode, getUpdates, loadCredentials, pollQRStatus, removeCredentials, saveCredentials, sendTextMessage };
package/dist/index.js ADDED
@@ -0,0 +1,208 @@
1
+ // src/credentials.ts
2
+ import fs from "fs";
3
+
4
+ // src/constants.ts
5
+ import path from "path";
6
+ var CHANNEL_NAME = "wechat";
7
+ var CHANNEL_VERSION = "0.1.0";
8
+ var DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com";
9
+ var BOT_TYPE = "3";
10
+ var CREDENTIALS_DIR = path.join(
11
+ process.env.HOME || "~",
12
+ ".claude",
13
+ "channels",
14
+ "wechat"
15
+ );
16
+ var CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, "account.json");
17
+ var SYNC_BUF_FILE = path.join(CREDENTIALS_DIR, "sync_buf.txt");
18
+ var LONG_POLL_TIMEOUT_MS = 35e3;
19
+ var QR_POLL_TIMEOUT_MS = 35e3;
20
+ var SEND_TIMEOUT_MS = 15e3;
21
+ var MSG_ITEM_TEXT = 1;
22
+ var MSG_ITEM_VOICE = 3;
23
+ var MSG_TYPE_BOT = 2;
24
+ var MSG_STATE_FINISH = 2;
25
+
26
+ // src/credentials.ts
27
+ function loadCredentials() {
28
+ try {
29
+ if (!fs.existsSync(CREDENTIALS_FILE)) return null;
30
+ return JSON.parse(fs.readFileSync(CREDENTIALS_FILE, "utf-8"));
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+ function saveCredentials(data) {
36
+ fs.mkdirSync(CREDENTIALS_DIR, { recursive: true });
37
+ fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(data, null, 2), "utf-8");
38
+ try {
39
+ fs.chmodSync(CREDENTIALS_FILE, 384);
40
+ } catch {
41
+ }
42
+ }
43
+ function removeCredentials() {
44
+ try {
45
+ if (fs.existsSync(CREDENTIALS_FILE)) fs.unlinkSync(CREDENTIALS_FILE);
46
+ } catch {
47
+ }
48
+ try {
49
+ if (fs.existsSync(SYNC_BUF_FILE)) fs.unlinkSync(SYNC_BUF_FILE);
50
+ } catch {
51
+ }
52
+ }
53
+
54
+ // src/api.ts
55
+ import crypto from "crypto";
56
+ function randomWechatUin() {
57
+ const uint32 = crypto.randomBytes(4).readUInt32BE(0);
58
+ return Buffer.from(String(uint32), "utf-8").toString("base64");
59
+ }
60
+ function buildHeaders(token, body) {
61
+ const headers = {
62
+ "Content-Type": "application/json",
63
+ AuthorizationType: "ilink_bot_token",
64
+ "X-WECHAT-UIN": randomWechatUin()
65
+ };
66
+ if (body) {
67
+ headers["Content-Length"] = String(Buffer.byteLength(body, "utf-8"));
68
+ }
69
+ if (token?.trim()) {
70
+ headers.Authorization = `Bearer ${token.trim()}`;
71
+ }
72
+ return headers;
73
+ }
74
+ function normalizeBaseUrl(baseUrl) {
75
+ return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
76
+ }
77
+ async function apiFetch(params) {
78
+ const url = new URL(params.endpoint, normalizeBaseUrl(params.baseUrl)).toString();
79
+ const headers = buildHeaders(params.token, params.body);
80
+ const controller = new AbortController();
81
+ const timer = setTimeout(() => controller.abort(), params.timeoutMs);
82
+ try {
83
+ const res = await fetch(url, {
84
+ method: "POST",
85
+ headers,
86
+ body: params.body,
87
+ signal: controller.signal
88
+ });
89
+ clearTimeout(timer);
90
+ const text = await res.text();
91
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${text}`);
92
+ return text;
93
+ } catch (err) {
94
+ clearTimeout(timer);
95
+ throw err;
96
+ }
97
+ }
98
+ async function fetchQRCode(baseUrl) {
99
+ const url = new URL(
100
+ `ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(BOT_TYPE)}`,
101
+ normalizeBaseUrl(baseUrl)
102
+ );
103
+ const res = await fetch(url.toString());
104
+ if (!res.ok) throw new Error(`QR fetch failed: ${res.status}`);
105
+ return await res.json();
106
+ }
107
+ async function pollQRStatus(baseUrl, qrcode) {
108
+ const url = new URL(
109
+ `ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`,
110
+ normalizeBaseUrl(baseUrl)
111
+ );
112
+ const controller = new AbortController();
113
+ const timer = setTimeout(() => controller.abort(), QR_POLL_TIMEOUT_MS);
114
+ try {
115
+ const res = await fetch(url.toString(), {
116
+ headers: { "iLink-App-ClientVersion": "1" },
117
+ signal: controller.signal
118
+ });
119
+ clearTimeout(timer);
120
+ if (!res.ok) throw new Error(`QR status failed: ${res.status}`);
121
+ return await res.json();
122
+ } catch (err) {
123
+ clearTimeout(timer);
124
+ if (err instanceof Error && err.name === "AbortError") {
125
+ return { status: "wait" };
126
+ }
127
+ throw err;
128
+ }
129
+ }
130
+ async function getUpdates(baseUrl, token, getUpdatesBuf) {
131
+ try {
132
+ const raw = await apiFetch({
133
+ baseUrl,
134
+ endpoint: "ilink/bot/getupdates",
135
+ body: JSON.stringify({
136
+ get_updates_buf: getUpdatesBuf,
137
+ base_info: { channel_version: CHANNEL_VERSION }
138
+ }),
139
+ token,
140
+ timeoutMs: LONG_POLL_TIMEOUT_MS
141
+ });
142
+ return JSON.parse(raw);
143
+ } catch (err) {
144
+ if (err instanceof Error && err.name === "AbortError") {
145
+ return { ret: 0, msgs: [], get_updates_buf: getUpdatesBuf };
146
+ }
147
+ throw err;
148
+ }
149
+ }
150
+ function generateClientId() {
151
+ return `claude-wechat:${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
152
+ }
153
+ async function sendTextMessage(baseUrl, token, to, text, contextToken) {
154
+ const clientId = generateClientId();
155
+ await apiFetch({
156
+ baseUrl,
157
+ endpoint: "ilink/bot/sendmessage",
158
+ body: JSON.stringify({
159
+ msg: {
160
+ from_user_id: "",
161
+ to_user_id: to,
162
+ client_id: clientId,
163
+ message_type: MSG_TYPE_BOT,
164
+ message_state: MSG_STATE_FINISH,
165
+ item_list: [{ type: MSG_ITEM_TEXT, text_item: { text } }],
166
+ context_token: contextToken
167
+ },
168
+ base_info: { channel_version: CHANNEL_VERSION }
169
+ }),
170
+ token,
171
+ timeoutMs: SEND_TIMEOUT_MS
172
+ });
173
+ return clientId;
174
+ }
175
+ function extractTextFromMessage(msg) {
176
+ if (!msg.item_list?.length) return "";
177
+ for (const item of msg.item_list) {
178
+ if (item.type === MSG_ITEM_TEXT && item.text_item?.text) {
179
+ const text = item.text_item.text;
180
+ const ref = item.ref_msg;
181
+ if (!ref) return text;
182
+ const parts = [];
183
+ if (ref.title) parts.push(ref.title);
184
+ if (!parts.length) return text;
185
+ return `[Quote: ${parts.join(" | ")}]
186
+ ${text}`;
187
+ }
188
+ if (item.type === MSG_ITEM_VOICE && item.voice_item?.text) {
189
+ return item.voice_item.text;
190
+ }
191
+ }
192
+ return "";
193
+ }
194
+ export {
195
+ CHANNEL_NAME,
196
+ CHANNEL_VERSION,
197
+ CREDENTIALS_DIR,
198
+ CREDENTIALS_FILE,
199
+ DEFAULT_BASE_URL,
200
+ extractTextFromMessage,
201
+ fetchQRCode,
202
+ getUpdates,
203
+ loadCredentials,
204
+ pollQRStatus,
205
+ removeCredentials,
206
+ saveCredentials,
207
+ sendTextMessage
208
+ };
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@paean-ai/claude-code-wechat",
3
+ "version": "0.1.0",
4
+ "description": "WeChat channel plugin for Claude Code — bridge WeChat messages into Claude Code sessions via the official ilink API",
5
+ "type": "module",
6
+ "bin": {
7
+ "claude-wechat": "./dist/cli.js",
8
+ "claude-wechat-server": "./dist/channel.js"
9
+ },
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "files": [
13
+ "dist",
14
+ "LICENSE",
15
+ "README.md"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsup",
19
+ "dev": "tsup --watch",
20
+ "typecheck": "tsc --noEmit",
21
+ "prepublishOnly": "npm run build"
22
+ },
23
+ "dependencies": {
24
+ "@modelcontextprotocol/sdk": "^1.27.1",
25
+ "commander": "^13.0.0",
26
+ "qrcode-terminal": "^0.12.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^25.5.0",
30
+ "tsup": "^8.0.0",
31
+ "typescript": "^5.0.0"
32
+ },
33
+ "engines": {
34
+ "node": ">=18.0.0"
35
+ },
36
+ "keywords": [
37
+ "claude",
38
+ "claude-code",
39
+ "wechat",
40
+ "mcp",
41
+ "channel",
42
+ "weixin",
43
+ "chatbot",
44
+ "ai"
45
+ ],
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "https://github.com/paean-ai/claude-code-wechat.git"
49
+ },
50
+ "homepage": "https://github.com/paean-ai/claude-code-wechat#readme",
51
+ "bugs": {
52
+ "url": "https://github.com/paean-ai/claude-code-wechat/issues"
53
+ },
54
+ "author": "Paean AI",
55
+ "license": "MIT"
56
+ }