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 +11 -1
- package/dist/messaging.d.ts +7 -0
- package/dist/messaging.js +100 -0
- package/package.json +1 -1
- package/src/cli.ts +11 -1
- package/src/messaging.ts +110 -0
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));
|
|
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
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));
|
|
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
|
package/src/messaging.ts
ADDED
|
@@ -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
|
+
}
|