cli-wechat-bridge 1.0.5

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.
Files changed (54) hide show
  1. package/LICENSE.txt +21 -0
  2. package/README.md +637 -0
  3. package/bin/_run-entry.mjs +35 -0
  4. package/bin/wechat-bridge-claude.mjs +5 -0
  5. package/bin/wechat-bridge-codex.mjs +5 -0
  6. package/bin/wechat-bridge-opencode.mjs +5 -0
  7. package/bin/wechat-bridge-shell.mjs +5 -0
  8. package/bin/wechat-bridge.mjs +5 -0
  9. package/bin/wechat-check-update.mjs +5 -0
  10. package/bin/wechat-claude-start.mjs +5 -0
  11. package/bin/wechat-claude.mjs +5 -0
  12. package/bin/wechat-codex-start.mjs +5 -0
  13. package/bin/wechat-codex.mjs +5 -0
  14. package/bin/wechat-daemon.mjs +5 -0
  15. package/bin/wechat-opencode-start.mjs +5 -0
  16. package/bin/wechat-opencode.mjs +5 -0
  17. package/bin/wechat-setup.mjs +5 -0
  18. package/dist/bridge/bridge-adapter-common.js +95 -0
  19. package/dist/bridge/bridge-adapters.claude.js +829 -0
  20. package/dist/bridge/bridge-adapters.codex.js +2228 -0
  21. package/dist/bridge/bridge-adapters.core.js +717 -0
  22. package/dist/bridge/bridge-adapters.js +26 -0
  23. package/dist/bridge/bridge-adapters.opencode.js +2129 -0
  24. package/dist/bridge/bridge-adapters.shared.js +1005 -0
  25. package/dist/bridge/bridge-adapters.shell.js +363 -0
  26. package/dist/bridge/bridge-controller.js +48 -0
  27. package/dist/bridge/bridge-final-reply.js +46 -0
  28. package/dist/bridge/bridge-process-reaper.js +348 -0
  29. package/dist/bridge/bridge-state.js +362 -0
  30. package/dist/bridge/bridge-types.js +1 -0
  31. package/dist/bridge/bridge-utils.js +1240 -0
  32. package/dist/bridge/claude-hook.js +82 -0
  33. package/dist/bridge/claude-hooks.js +267 -0
  34. package/dist/bridge/wechat-bridge.js +1026 -0
  35. package/dist/commands/check-update.js +30 -0
  36. package/dist/companion/codex-panel-link.js +72 -0
  37. package/dist/companion/codex-panel.js +179 -0
  38. package/dist/companion/codex-remote-client.js +124 -0
  39. package/dist/companion/local-companion-link.js +240 -0
  40. package/dist/companion/local-companion-start.js +420 -0
  41. package/dist/companion/local-companion.js +424 -0
  42. package/dist/daemon/daemon-link.js +175 -0
  43. package/dist/daemon/wechat-daemon.js +1202 -0
  44. package/dist/media/media-types.js +1 -0
  45. package/dist/runtime/create-runtime-host.js +12 -0
  46. package/dist/runtime/legacy-adapter-runtime.js +46 -0
  47. package/dist/runtime/runtime-types.js +5 -0
  48. package/dist/utils/version-checker.js +161 -0
  49. package/dist/wechat/channel-config.js +196 -0
  50. package/dist/wechat/setup.js +283 -0
  51. package/dist/wechat/standalone-bot.js +355 -0
  52. package/dist/wechat/wechat-channel.js +492 -0
  53. package/dist/wechat/wechat-transport.js +1213 -0
  54. package/package.json +101 -0
@@ -0,0 +1,492 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * WeChat MCP server.
4
+ *
5
+ * This server exposes standard MCP tools instead of relying on
6
+ * Claude's preview-only channel push API.
7
+ */
8
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
9
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
11
+ import { formatByteSize, WeChatTransport, } from "./wechat-transport.js";
12
+ const SERVER_NAME = "wechat";
13
+ const SERVER_VERSION = "0.3.0";
14
+ const DEFAULT_POLL_TIMEOUT_MS = 1_000;
15
+ const MAX_POLL_TIMEOUT_MS = 35_000;
16
+ function log(message) {
17
+ process.stderr.write(`[wechat-mcp] ${message}\n`);
18
+ }
19
+ function logError(message) {
20
+ process.stderr.write(`[wechat-mcp] ERROR: ${message}\n`);
21
+ }
22
+ const transport = new WeChatTransport({ log, logError });
23
+ function asObject(args) {
24
+ return args && typeof args === "object"
25
+ ? args
26
+ : {};
27
+ }
28
+ function clampTimeoutMs(value, fallbackMs) {
29
+ if (typeof value !== "number" || !Number.isFinite(value)) {
30
+ return fallbackMs;
31
+ }
32
+ const rounded = Math.floor(value);
33
+ return Math.min(Math.max(rounded, 1_000), MAX_POLL_TIMEOUT_MS);
34
+ }
35
+ function parseFetchMessagesArgs(args) {
36
+ const record = asObject(args);
37
+ const waitForNew = typeof record.wait_for_new === "boolean" ? record.wait_for_new : false;
38
+ const fallbackTimeout = waitForNew ? 15_000 : DEFAULT_POLL_TIMEOUT_MS;
39
+ return {
40
+ waitForNew,
41
+ timeoutMs: clampTimeoutMs(record.timeout_ms, fallbackTimeout),
42
+ };
43
+ }
44
+ function parseResetSyncArgs(args) {
45
+ const record = asObject(args);
46
+ return {
47
+ clearContextCache: typeof record.clear_context_cache === "boolean"
48
+ ? record.clear_context_cache
49
+ : false,
50
+ };
51
+ }
52
+ function parseReplyArgs(args) {
53
+ const record = asObject(args);
54
+ const senderId = record.sender_id;
55
+ const text = record.text;
56
+ if (typeof senderId !== "string" || !senderId.trim()) {
57
+ return { error: "sender_id must be a non-empty string." };
58
+ }
59
+ if (typeof text !== "string" || !text.trim()) {
60
+ return { error: "text must be a non-empty string." };
61
+ }
62
+ return {
63
+ value: {
64
+ senderId: senderId.trim(),
65
+ text: text.trim(),
66
+ },
67
+ };
68
+ }
69
+ function parseNotifyArgs(args) {
70
+ const record = asObject(args);
71
+ const message = record.message;
72
+ const recipientId = record.recipient_id;
73
+ if (typeof message !== "string" || !message.trim()) {
74
+ return { error: "message must be a non-empty string." };
75
+ }
76
+ if (recipientId !== undefined && typeof recipientId !== "string") {
77
+ return { error: "recipient_id must be a string when provided." };
78
+ }
79
+ return {
80
+ value: {
81
+ message: message.trim(),
82
+ recipientId: typeof recipientId === "string" ? recipientId.trim() : undefined,
83
+ },
84
+ };
85
+ }
86
+ function parseSendImageArgs(args) {
87
+ const record = asObject(args);
88
+ const imagePath = record.image_path;
89
+ const caption = record.caption;
90
+ const recipientId = record.recipient_id;
91
+ if (typeof imagePath !== "string" || !imagePath.trim()) {
92
+ return { error: "image_path must be a non-empty string." };
93
+ }
94
+ if (caption !== undefined && typeof caption !== "string") {
95
+ return { error: "caption must be a string when provided." };
96
+ }
97
+ if (recipientId !== undefined && typeof recipientId !== "string") {
98
+ return { error: "recipient_id must be a string when provided." };
99
+ }
100
+ return {
101
+ value: {
102
+ imagePath: imagePath.trim(),
103
+ caption: typeof caption === "string" ? caption.trim() : undefined,
104
+ recipientId: typeof recipientId === "string" ? recipientId.trim() : undefined,
105
+ },
106
+ };
107
+ }
108
+ function parseSendFileArgs(args) {
109
+ const record = asObject(args);
110
+ const filePath = record.file_path;
111
+ const title = record.title;
112
+ const recipientId = record.recipient_id;
113
+ if (typeof filePath !== "string" || !filePath.trim()) {
114
+ return { error: "file_path must be a non-empty string." };
115
+ }
116
+ if (title !== undefined && typeof title !== "string") {
117
+ return { error: "title must be a string when provided." };
118
+ }
119
+ if (recipientId !== undefined && typeof recipientId !== "string") {
120
+ return { error: "recipient_id must be a string when provided." };
121
+ }
122
+ return {
123
+ value: {
124
+ filePath: filePath.trim(),
125
+ title: typeof title === "string" ? title.trim() : undefined,
126
+ recipientId: typeof recipientId === "string" ? recipientId.trim() : undefined,
127
+ },
128
+ };
129
+ }
130
+ function parseSendVoiceArgs(args) {
131
+ const record = asObject(args);
132
+ const voicePath = record.voice_path;
133
+ const recipientId = record.recipient_id;
134
+ if (typeof voicePath !== "string" || !voicePath.trim()) {
135
+ return { error: "voice_path must be a non-empty string." };
136
+ }
137
+ if (recipientId !== undefined && typeof recipientId !== "string") {
138
+ return { error: "recipient_id must be a string when provided." };
139
+ }
140
+ return {
141
+ value: {
142
+ voicePath: voicePath.trim(),
143
+ recipientId: typeof recipientId === "string" ? recipientId.trim() : undefined,
144
+ },
145
+ };
146
+ }
147
+ function parseSendVideoArgs(args) {
148
+ const record = asObject(args);
149
+ const videoPath = record.video_path;
150
+ const title = record.title;
151
+ const recipientId = record.recipient_id;
152
+ if (typeof videoPath !== "string" || !videoPath.trim()) {
153
+ return { error: "video_path must be a non-empty string." };
154
+ }
155
+ if (title !== undefined && typeof title !== "string") {
156
+ return { error: "title must be a string when provided." };
157
+ }
158
+ if (recipientId !== undefined && typeof recipientId !== "string") {
159
+ return { error: "recipient_id must be a string when provided." };
160
+ }
161
+ return {
162
+ value: {
163
+ videoPath: videoPath.trim(),
164
+ title: typeof title === "string" ? title.trim() : undefined,
165
+ recipientId: typeof recipientId === "string" ? recipientId.trim() : undefined,
166
+ },
167
+ };
168
+ }
169
+ async function fetchMessages(args) {
170
+ const result = await transport.pollMessages({
171
+ timeoutMs: args.timeoutMs,
172
+ });
173
+ if (!result.messages.length) {
174
+ return "No new WeChat messages.";
175
+ }
176
+ return formatFetchedMessages(result.messages);
177
+ }
178
+ function formatFetchedMessages(messages) {
179
+ const blocks = messages.map((message, index) => {
180
+ const lines = [
181
+ `[${index + 1}]`,
182
+ `sender_id: ${message.senderId}`,
183
+ `sender: ${message.sender}`,
184
+ `session_id: ${message.sessionId || "(unknown)"}`,
185
+ `created_at: ${message.createdAt}`,
186
+ "text:",
187
+ message.text,
188
+ ];
189
+ if (message.attachments.length > 0) {
190
+ lines.push("attachments:", ...message.attachments.map((attachment) => [
191
+ `- kind: ${attachment.kind}`,
192
+ ` name: ${attachment.fileName}`,
193
+ ` size: ${formatByteSize(attachment.sizeBytes)}`,
194
+ ` path: ${attachment.path}`,
195
+ ].join("\n")));
196
+ }
197
+ return lines.join("\n");
198
+ });
199
+ return [
200
+ `Fetched ${messages.length} new WeChat message${messages.length === 1 ? "" : "s"}.`,
201
+ "",
202
+ ...blocks,
203
+ ].join("\n");
204
+ }
205
+ async function replyToMessage(args) {
206
+ await transport.sendText(args.senderId, args.text);
207
+ return `Sent reply to ${args.senderId}.`;
208
+ }
209
+ async function sendNotification(args) {
210
+ const recipientId = await transport.sendNotification(args.message, args.recipientId);
211
+ return `Sent message to ${recipientId}.`;
212
+ }
213
+ async function sendImage(args) {
214
+ const recipientId = await transport.sendImage(args.imagePath, {
215
+ recipientId: args.recipientId,
216
+ caption: args.caption,
217
+ });
218
+ return `Sent image to ${recipientId}.`;
219
+ }
220
+ async function sendFile(args) {
221
+ const recipientId = await transport.sendFile(args.filePath, {
222
+ recipientId: args.recipientId,
223
+ title: args.title,
224
+ });
225
+ return `Sent file to ${recipientId}.`;
226
+ }
227
+ async function sendVoice(args) {
228
+ const recipientId = await transport.sendVoice(args.voicePath, args.recipientId);
229
+ return `Sent voice to ${recipientId}.`;
230
+ }
231
+ async function sendVideo(args) {
232
+ const recipientId = await transport.sendVideo(args.videoPath, {
233
+ recipientId: args.recipientId,
234
+ title: args.title,
235
+ });
236
+ return `Sent video to ${recipientId}.`;
237
+ }
238
+ function textResult(text) {
239
+ return {
240
+ content: [{ type: "text", text }],
241
+ };
242
+ }
243
+ async function executeTextAction(action) {
244
+ try {
245
+ return textResult(await action());
246
+ }
247
+ catch (err) {
248
+ return textResult(`error: ${err instanceof Error ? err.message : String(err)}`);
249
+ }
250
+ }
251
+ const mcp = new Server({ name: SERVER_NAME, version: SERVER_VERSION }, {
252
+ capabilities: {
253
+ tools: {},
254
+ },
255
+ instructions: [
256
+ "Use wechat_fetch_messages to pull new inbound WeChat messages.",
257
+ "Use wechat_reply to send a plain-text reply to a sender_id.",
258
+ "Use wechat_notify for proactive outbound text notifications.",
259
+ "Use wechat_send_image, wechat_send_file, wechat_send_voice, and wechat_send_video for outbound media.",
260
+ "If recipient_id is omitted for outbound tools, the most recently active cached recipient is used.",
261
+ "Use wechat_get_status to inspect auth and local state.",
262
+ "Use wechat_reset_sync if you need to clear local sync state.",
263
+ ].join("\n"),
264
+ });
265
+ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
266
+ tools: [
267
+ {
268
+ name: "wechat_get_status",
269
+ description: "Show saved account information and local MCP state files.",
270
+ inputSchema: {
271
+ type: "object",
272
+ properties: {},
273
+ },
274
+ },
275
+ {
276
+ name: "wechat_fetch_messages",
277
+ description: "Pull new WeChat messages using the saved sync cursor.",
278
+ inputSchema: {
279
+ type: "object",
280
+ properties: {
281
+ wait_for_new: {
282
+ type: "boolean",
283
+ description: "If true, long-poll briefly for new messages before returning.",
284
+ },
285
+ timeout_ms: {
286
+ type: "number",
287
+ description: "Polling timeout in milliseconds. Max 35000.",
288
+ },
289
+ },
290
+ },
291
+ },
292
+ {
293
+ name: "wechat_reply",
294
+ description: "Send a plain-text reply to a WeChat sender_id.",
295
+ inputSchema: {
296
+ type: "object",
297
+ properties: {
298
+ sender_id: {
299
+ type: "string",
300
+ description: "The sender_id from wechat_fetch_messages output.",
301
+ },
302
+ text: {
303
+ type: "string",
304
+ description: "The plain-text message to send.",
305
+ },
306
+ },
307
+ required: ["sender_id", "text"],
308
+ },
309
+ },
310
+ {
311
+ name: "wechat_notify",
312
+ description: "Send a proactive plain-text message. If recipient_id is omitted, sends to the most recently active cached recipient.",
313
+ inputSchema: {
314
+ type: "object",
315
+ properties: {
316
+ message: {
317
+ type: "string",
318
+ description: "The plain-text message to send.",
319
+ },
320
+ recipient_id: {
321
+ type: "string",
322
+ description: "Optional WeChat user ID (xxx@im.wechat). Defaults to the most recently active cached recipient.",
323
+ },
324
+ },
325
+ required: ["message"],
326
+ },
327
+ },
328
+ {
329
+ name: "wechat_send_image",
330
+ description: "Send an image file. The image is encrypted, uploaded to WeChat CDN, and sent as an image message.",
331
+ inputSchema: {
332
+ type: "object",
333
+ properties: {
334
+ image_path: {
335
+ type: "string",
336
+ description: "Absolute path to the image file on disk.",
337
+ },
338
+ caption: {
339
+ type: "string",
340
+ description: "Optional plain-text caption sent before the image.",
341
+ },
342
+ recipient_id: {
343
+ type: "string",
344
+ description: "Optional WeChat user ID (xxx@im.wechat). Defaults to the most recently active cached recipient.",
345
+ },
346
+ },
347
+ required: ["image_path"],
348
+ },
349
+ },
350
+ {
351
+ name: "wechat_send_file",
352
+ description: "Send any file. The file is encrypted, uploaded to WeChat CDN, and sent as a WeChat file message.",
353
+ inputSchema: {
354
+ type: "object",
355
+ properties: {
356
+ file_path: {
357
+ type: "string",
358
+ description: "Absolute path to the file on disk.",
359
+ },
360
+ title: {
361
+ type: "string",
362
+ description: "Optional display name for the file. Defaults to the filename.",
363
+ },
364
+ recipient_id: {
365
+ type: "string",
366
+ description: "Optional WeChat user ID (xxx@im.wechat). Defaults to the most recently active cached recipient.",
367
+ },
368
+ },
369
+ required: ["file_path"],
370
+ },
371
+ },
372
+ {
373
+ name: "wechat_send_voice",
374
+ description: "Send a voice or audio file. The file is encrypted, uploaded to WeChat CDN, and sent as a voice message.",
375
+ inputSchema: {
376
+ type: "object",
377
+ properties: {
378
+ voice_path: {
379
+ type: "string",
380
+ description: "Absolute path to the audio file on disk.",
381
+ },
382
+ recipient_id: {
383
+ type: "string",
384
+ description: "Optional WeChat user ID (xxx@im.wechat). Defaults to the most recently active cached recipient.",
385
+ },
386
+ },
387
+ required: ["voice_path"],
388
+ },
389
+ },
390
+ {
391
+ name: "wechat_send_video",
392
+ description: "Send a video file. The file is encrypted, uploaded to WeChat CDN, and sent as a video message.",
393
+ inputSchema: {
394
+ type: "object",
395
+ properties: {
396
+ video_path: {
397
+ type: "string",
398
+ description: "Absolute path to the video file on disk.",
399
+ },
400
+ title: {
401
+ type: "string",
402
+ description: "Optional plain-text caption sent before the video.",
403
+ },
404
+ recipient_id: {
405
+ type: "string",
406
+ description: "Optional WeChat user ID (xxx@im.wechat). Defaults to the most recently active cached recipient.",
407
+ },
408
+ },
409
+ required: ["video_path"],
410
+ },
411
+ },
412
+ {
413
+ name: "wechat_reset_sync",
414
+ description: "Clear saved sync state so future fetches restart from a fresh cursor.",
415
+ inputSchema: {
416
+ type: "object",
417
+ properties: {
418
+ clear_context_cache: {
419
+ type: "boolean",
420
+ description: "If true, also clear cached reply context tokens.",
421
+ },
422
+ },
423
+ },
424
+ },
425
+ ],
426
+ }));
427
+ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
428
+ switch (req.params.name) {
429
+ case "wechat_get_status":
430
+ return textResult(transport.getStatusText());
431
+ case "wechat_fetch_messages":
432
+ return executeTextAction(() => fetchMessages(parseFetchMessagesArgs(req.params.arguments)));
433
+ case "wechat_reset_sync":
434
+ return textResult(transport.resetSyncState(parseResetSyncArgs(req.params.arguments)));
435
+ case "wechat_reply": {
436
+ const parsed = parseReplyArgs(req.params.arguments);
437
+ if ("error" in parsed) {
438
+ return textResult(`error: ${parsed.error}`);
439
+ }
440
+ return executeTextAction(() => replyToMessage(parsed.value));
441
+ }
442
+ case "wechat_notify": {
443
+ const parsed = parseNotifyArgs(req.params.arguments);
444
+ if ("error" in parsed) {
445
+ return textResult(`error: ${parsed.error}`);
446
+ }
447
+ return executeTextAction(() => sendNotification(parsed.value));
448
+ }
449
+ case "wechat_send_image": {
450
+ const parsed = parseSendImageArgs(req.params.arguments);
451
+ if ("error" in parsed) {
452
+ return textResult(`error: ${parsed.error}`);
453
+ }
454
+ return executeTextAction(() => sendImage(parsed.value));
455
+ }
456
+ case "wechat_send_file": {
457
+ const parsed = parseSendFileArgs(req.params.arguments);
458
+ if ("error" in parsed) {
459
+ return textResult(`error: ${parsed.error}`);
460
+ }
461
+ return executeTextAction(() => sendFile(parsed.value));
462
+ }
463
+ case "wechat_send_voice": {
464
+ const parsed = parseSendVoiceArgs(req.params.arguments);
465
+ if ("error" in parsed) {
466
+ return textResult(`error: ${parsed.error}`);
467
+ }
468
+ return executeTextAction(() => sendVoice(parsed.value));
469
+ }
470
+ case "wechat_send_video": {
471
+ const parsed = parseSendVideoArgs(req.params.arguments);
472
+ if ("error" in parsed) {
473
+ return textResult(`error: ${parsed.error}`);
474
+ }
475
+ return executeTextAction(() => sendVideo(parsed.value));
476
+ }
477
+ default:
478
+ throw new Error(`unknown tool: ${req.params.name}`);
479
+ }
480
+ });
481
+ async function main() {
482
+ if (process.argv.includes("--check")) {
483
+ log(transport.getStatusText());
484
+ process.exit(0);
485
+ }
486
+ await mcp.connect(new StdioServerTransport());
487
+ log("WeChat MCP server is ready.");
488
+ }
489
+ main().catch((err) => {
490
+ logError(`Fatal: ${String(err)}`);
491
+ process.exit(1);
492
+ });