weixin-mcp 1.3.1 → 1.3.2

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/cli.js CHANGED
@@ -43,7 +43,15 @@ else if (command === "logs") {
43
43
  }
44
44
  else if (command === "accounts") {
45
45
  const { manageAccounts } = await import("./accounts.js");
46
- await manageAccounts(process.argv.slice(3)); // [subcommand, ...args]
46
+ await manageAccounts(process.argv.slice(3));
47
+ }
48
+ else if (command === "send") {
49
+ const { cliSend } = await import("./messaging.js");
50
+ await cliSend(process.argv.slice(3)); // <userId> <text...>
51
+ }
52
+ else if (command === "poll") {
53
+ const { cliPoll } = await import("./messaging.js");
54
+ await cliPoll(process.argv.slice(3)); // [--watch] [--reset]
47
55
  }
48
56
  else if (command === undefined || command === "serve") {
49
57
  // Default: stdio MCP server (for Claude Desktop integration)
@@ -62,6 +70,8 @@ Commands:
62
70
  stop Stop daemon
63
71
  restart Restart daemon
64
72
  logs [-f] Show daemon logs (-f to follow)
73
+ send <userId> <text> Send a message from CLI
74
+ poll [--watch|-w] [--reset] Poll messages once, or watch continuously
65
75
  accounts [list] List all accounts
66
76
  accounts remove <id> Remove an account
67
77
  accounts clean Remove duplicate accounts (same userId), keep newest
@@ -0,0 +1,7 @@
1
+ /**
2
+ * CLI messaging commands:
3
+ * npx weixin-mcp send <userId> <text> — send a message
4
+ * npx weixin-mcp poll [--watch] [--reset] — poll for messages (once or continuous)
5
+ */
6
+ export declare function cliSend(args: string[]): Promise<void>;
7
+ export declare function cliPoll(args: string[]): Promise<void>;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * CLI messaging commands:
3
+ * npx weixin-mcp send <userId> <text> — send a message
4
+ * npx weixin-mcp poll [--watch] [--reset] — poll for messages (once or continuous)
5
+ */
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ import { ACCOUNTS_DIR } from "./paths.js";
9
+ import { DEFAULT_BASE_URL, sendTextMessage, getUpdates, loadCursor, saveCursor, } from "./api.js";
10
+ function loadAccount() {
11
+ const files = fs.readdirSync(ACCOUNTS_DIR).filter((f) => f.endsWith(".json") && !f.endsWith(".sync.json") && !f.endsWith(".cursor.json"));
12
+ if (files.length === 0)
13
+ throw new Error("No account. Run: npx weixin-mcp login");
14
+ const accountId = process.env.WEIXIN_ACCOUNT_ID ?? files[0].replace(".json", "");
15
+ const data = JSON.parse(fs.readFileSync(path.join(ACCOUNTS_DIR, `${accountId}.json`), "utf-8"));
16
+ if (!data.token)
17
+ throw new Error(`No token for ${accountId}. Run: npx weixin-mcp login`);
18
+ return { ...data, accountId };
19
+ }
20
+ function formatMsg(msg) {
21
+ const from = String(msg.from_user_id ?? "?");
22
+ const items = msg.item_list ?? [];
23
+ const parts = [];
24
+ for (const item of items) {
25
+ if (item.type === 1 && item.text_item?.text)
26
+ parts.push(item.text_item.text);
27
+ else if (item.type === 2 && item.image_item?.url)
28
+ parts.push(`[image: ${item.image_item.url}]`);
29
+ else
30
+ parts.push(`[type:${item.type}]`);
31
+ }
32
+ const msgType = Number(msg.message_type);
33
+ const prefix = msgType === 1 ? "← " : "→ "; // incoming vs outgoing
34
+ return `${prefix}${from.slice(0, 20)}: ${parts.join(" ") || "(empty)"}`;
35
+ }
36
+ export async function cliSend(args) {
37
+ const [to, ...textParts] = args;
38
+ if (!to || textParts.length === 0) {
39
+ console.error("Usage: npx weixin-mcp send <userId> <message text>");
40
+ process.exit(1);
41
+ }
42
+ const text = textParts.join(" ");
43
+ const { token, baseUrl = DEFAULT_BASE_URL } = loadAccount();
44
+ process.stdout.write(`Sending to ${to}... `);
45
+ const result = await sendTextMessage(to, text, token, baseUrl);
46
+ const ret = result?.ret ?? result?.errcode;
47
+ if (ret === 0 || ret === undefined) {
48
+ console.log("✅ Sent");
49
+ }
50
+ else {
51
+ console.log(`❌ Failed (ret=${ret})`);
52
+ console.log(JSON.stringify(result, null, 2));
53
+ }
54
+ }
55
+ export async function cliPoll(args) {
56
+ const watch = args.includes("--watch") || args.includes("-w");
57
+ const reset = args.includes("--reset");
58
+ const { token, baseUrl = DEFAULT_BASE_URL, accountId } = loadAccount();
59
+ if (watch) {
60
+ console.log("Watching for messages (Ctrl+C to stop)...\n");
61
+ let cursor = reset ? "" : loadCursor(accountId);
62
+ while (true) {
63
+ try {
64
+ const resp = await getUpdates(token, baseUrl, cursor);
65
+ if (resp.get_updates_buf) {
66
+ cursor = resp.get_updates_buf;
67
+ saveCursor(accountId, cursor);
68
+ }
69
+ if (resp.msgs && resp.msgs.length > 0) {
70
+ const ts = new Date().toLocaleTimeString();
71
+ for (const msg of resp.msgs) {
72
+ console.log(`[${ts}] ${formatMsg(msg)}`);
73
+ }
74
+ }
75
+ }
76
+ catch (err) {
77
+ console.error("Poll error:", err instanceof Error ? err.message : String(err));
78
+ await new Promise((r) => setTimeout(r, 3000));
79
+ }
80
+ // getupdates is long-poll, no need for extra delay
81
+ }
82
+ }
83
+ else {
84
+ // One-shot poll
85
+ const cursor = reset ? "" : loadCursor(accountId);
86
+ const resp = await getUpdates(token, baseUrl, cursor);
87
+ if (resp.get_updates_buf)
88
+ saveCursor(accountId, resp.get_updates_buf);
89
+ const msgs = resp.msgs ?? [];
90
+ if (msgs.length === 0) {
91
+ console.log("No new messages.");
92
+ }
93
+ else {
94
+ console.log(`${msgs.length} message(s):\n`);
95
+ for (const msg of msgs) {
96
+ console.log(formatMsg(msg));
97
+ }
98
+ }
99
+ }
100
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weixin-mcp",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "description": "MCP server for WeChat (Weixin) — send messages via OpenClaw weixin plugin auth",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/cli.ts CHANGED
@@ -45,7 +45,15 @@ if (command === "login") {
45
45
 
46
46
  } else if (command === "accounts") {
47
47
  const { manageAccounts } = await import("./accounts.js");
48
- await manageAccounts(process.argv.slice(3)); // [subcommand, ...args]
48
+ await manageAccounts(process.argv.slice(3));
49
+
50
+ } else if (command === "send") {
51
+ const { cliSend } = await import("./messaging.js");
52
+ await cliSend(process.argv.slice(3)); // <userId> <text...>
53
+
54
+ } else if (command === "poll") {
55
+ const { cliPoll } = await import("./messaging.js");
56
+ await cliPoll(process.argv.slice(3)); // [--watch] [--reset]
49
57
 
50
58
  } else if (command === undefined || command === "serve") {
51
59
  // Default: stdio MCP server (for Claude Desktop integration)
@@ -64,6 +72,8 @@ Commands:
64
72
  stop Stop daemon
65
73
  restart Restart daemon
66
74
  logs [-f] Show daemon logs (-f to follow)
75
+ send <userId> <text> Send a message from CLI
76
+ poll [--watch|-w] [--reset] Poll messages once, or watch continuously
67
77
  accounts [list] List all accounts
68
78
  accounts remove <id> Remove an account
69
79
  accounts clean Remove duplicate accounts (same userId), keep newest
@@ -0,0 +1,110 @@
1
+ /**
2
+ * CLI messaging commands:
3
+ * npx weixin-mcp send <userId> <text> — send a message
4
+ * npx weixin-mcp poll [--watch] [--reset] — poll for messages (once or continuous)
5
+ */
6
+
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { ACCOUNTS_DIR } from "./paths.js";
10
+ import {
11
+ DEFAULT_BASE_URL,
12
+ sendTextMessage,
13
+ getUpdates,
14
+ loadCursor,
15
+ saveCursor,
16
+ } from "./api.js";
17
+
18
+ interface AccountData { token?: string; baseUrl?: string; userId?: string }
19
+
20
+ function loadAccount(): AccountData & { accountId: string } {
21
+ const files = fs.readdirSync(ACCOUNTS_DIR).filter(
22
+ (f) => f.endsWith(".json") && !f.endsWith(".sync.json") && !f.endsWith(".cursor.json"),
23
+ );
24
+ if (files.length === 0) throw new Error("No account. Run: npx weixin-mcp login");
25
+ const accountId = process.env.WEIXIN_ACCOUNT_ID ?? files[0].replace(".json", "");
26
+ const data = JSON.parse(fs.readFileSync(path.join(ACCOUNTS_DIR, `${accountId}.json`), "utf-8")) as AccountData;
27
+ if (!data.token) throw new Error(`No token for ${accountId}. Run: npx weixin-mcp login`);
28
+ return { ...data, accountId };
29
+ }
30
+
31
+ function formatMsg(msg: Record<string, unknown>): string {
32
+ const from = String(msg.from_user_id ?? "?");
33
+ const items = (msg.item_list as Array<{ type: number; text_item?: { text: string }; image_item?: { url: string } }>) ?? [];
34
+ const parts: string[] = [];
35
+ for (const item of items) {
36
+ if (item.type === 1 && item.text_item?.text) parts.push(item.text_item.text);
37
+ else if (item.type === 2 && item.image_item?.url) parts.push(`[image: ${item.image_item.url}]`);
38
+ else parts.push(`[type:${item.type}]`);
39
+ }
40
+ const msgType = Number(msg.message_type);
41
+ const prefix = msgType === 1 ? "← " : "→ "; // incoming vs outgoing
42
+ return `${prefix}${from.slice(0, 20)}: ${parts.join(" ") || "(empty)"}`;
43
+ }
44
+
45
+ export async function cliSend(args: string[]) {
46
+ const [to, ...textParts] = args;
47
+ if (!to || textParts.length === 0) {
48
+ console.error("Usage: npx weixin-mcp send <userId> <message text>");
49
+ process.exit(1);
50
+ }
51
+ const text = textParts.join(" ");
52
+ const { token, baseUrl = DEFAULT_BASE_URL } = loadAccount();
53
+
54
+ process.stdout.write(`Sending to ${to}... `);
55
+ const result = await sendTextMessage(to, text, token!, baseUrl) as Record<string, unknown>;
56
+ const ret = result?.ret ?? result?.errcode;
57
+ if (ret === 0 || ret === undefined) {
58
+ console.log("✅ Sent");
59
+ } else {
60
+ console.log(`❌ Failed (ret=${ret})`);
61
+ console.log(JSON.stringify(result, null, 2));
62
+ }
63
+ }
64
+
65
+ export async function cliPoll(args: string[]) {
66
+ const watch = args.includes("--watch") || args.includes("-w");
67
+ const reset = args.includes("--reset");
68
+
69
+ const { token, baseUrl = DEFAULT_BASE_URL, accountId } = loadAccount();
70
+
71
+ if (watch) {
72
+ console.log("Watching for messages (Ctrl+C to stop)...\n");
73
+ let cursor = reset ? "" : loadCursor(accountId);
74
+
75
+ while (true) {
76
+ try {
77
+ const resp = await getUpdates(token!, baseUrl, cursor);
78
+ if (resp.get_updates_buf) {
79
+ cursor = resp.get_updates_buf;
80
+ saveCursor(accountId, cursor);
81
+ }
82
+ if (resp.msgs && resp.msgs.length > 0) {
83
+ const ts = new Date().toLocaleTimeString();
84
+ for (const msg of resp.msgs) {
85
+ console.log(`[${ts}] ${formatMsg(msg as Record<string, unknown>)}`);
86
+ }
87
+ }
88
+ } catch (err) {
89
+ console.error("Poll error:", err instanceof Error ? err.message : String(err));
90
+ await new Promise((r) => setTimeout(r, 3000));
91
+ }
92
+ // getupdates is long-poll, no need for extra delay
93
+ }
94
+ } else {
95
+ // One-shot poll
96
+ const cursor = reset ? "" : loadCursor(accountId);
97
+ const resp = await getUpdates(token!, baseUrl, cursor);
98
+ if (resp.get_updates_buf) saveCursor(accountId, resp.get_updates_buf);
99
+
100
+ const msgs = resp.msgs ?? [];
101
+ if (msgs.length === 0) {
102
+ console.log("No new messages.");
103
+ } else {
104
+ console.log(`${msgs.length} message(s):\n`);
105
+ for (const msg of msgs) {
106
+ console.log(formatMsg(msg as Record<string, unknown>));
107
+ }
108
+ }
109
+ }
110
+ }