weixin-mcp 1.3.0 → 1.3.1

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.
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Account management commands:
3
+ * npx weixin-mcp accounts list — list all accounts (default)
4
+ * npx weixin-mcp accounts remove <id> — remove a specific account
5
+ * npx weixin-mcp accounts clean — remove duplicate accounts (same userId), keep newest
6
+ * npx weixin-mcp accounts use <id> — print export WEIXIN_ACCOUNT_ID=<id>
7
+ */
8
+ export declare function manageAccounts(args: string[]): Promise<void>;
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Account management commands:
3
+ * npx weixin-mcp accounts list — list all accounts (default)
4
+ * npx weixin-mcp accounts remove <id> — remove a specific account
5
+ * npx weixin-mcp accounts clean — remove duplicate accounts (same userId), keep newest
6
+ * npx weixin-mcp accounts use <id> — print export WEIXIN_ACCOUNT_ID=<id>
7
+ */
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import { ACCOUNTS_DIR } from "./paths.js";
11
+ function listFiles() {
12
+ try {
13
+ return fs
14
+ .readdirSync(ACCOUNTS_DIR)
15
+ .filter((f) => f.endsWith(".json") && !f.endsWith(".sync.json") && !f.endsWith(".cursor.json"));
16
+ }
17
+ catch {
18
+ return [];
19
+ }
20
+ }
21
+ function loadAccount(file) {
22
+ const accountId = file.replace(".json", "");
23
+ const data = JSON.parse(fs.readFileSync(path.join(ACCOUNTS_DIR, file), "utf-8"));
24
+ return { ...data, accountId };
25
+ }
26
+ function removeAccount(accountId) {
27
+ const filePath = path.join(ACCOUNTS_DIR, `${accountId}.json`);
28
+ const cursorPath = path.join(ACCOUNTS_DIR, `${accountId}.cursor.json`);
29
+ let removed = false;
30
+ if (fs.existsSync(filePath)) {
31
+ fs.unlinkSync(filePath);
32
+ removed = true;
33
+ }
34
+ if (fs.existsSync(cursorPath)) {
35
+ fs.unlinkSync(cursorPath);
36
+ }
37
+ return removed;
38
+ }
39
+ export async function manageAccounts(args) {
40
+ const subcommand = args[0] ?? "list";
41
+ if (subcommand === "list") {
42
+ const files = listFiles();
43
+ if (files.length === 0) {
44
+ console.log("No accounts found. Run: npx weixin-mcp login");
45
+ return;
46
+ }
47
+ const active = process.env.WEIXIN_ACCOUNT_ID ?? files[0].replace(".json", "");
48
+ console.log(`Accounts (${files.length}):\n`);
49
+ for (const file of files) {
50
+ const acc = loadAccount(file);
51
+ const isActive = acc.accountId === active;
52
+ const savedAt = acc.savedAt ? new Date(acc.savedAt).toLocaleString() : "unknown";
53
+ console.log(`${isActive ? "● " : " "}${acc.accountId}`);
54
+ const displayId = acc.userId?.replace(/@im\.wechat(@im\.wechat)+$/, "@im.wechat") ?? "(unknown)";
55
+ console.log(` User: ${displayId}`);
56
+ console.log(` Saved: ${savedAt}`);
57
+ }
58
+ if (files.length > 1) {
59
+ console.log(`\nActive: ${active} (set WEIXIN_ACCOUNT_ID to change)`);
60
+ }
61
+ }
62
+ else if (subcommand === "remove") {
63
+ const accountId = args[1];
64
+ if (!accountId) {
65
+ console.error("Usage: accounts remove <accountId>");
66
+ process.exit(1);
67
+ }
68
+ if (removeAccount(accountId)) {
69
+ console.log(`✅ Removed: ${accountId}`);
70
+ }
71
+ else {
72
+ console.error(`❌ Account not found: ${accountId}`);
73
+ process.exit(1);
74
+ }
75
+ }
76
+ else if (subcommand === "clean") {
77
+ const files = listFiles();
78
+ const byUserId = new Map();
79
+ for (const file of files) {
80
+ const acc = loadAccount(file);
81
+ // Normalize userId: strip duplicate @im.wechat suffix
82
+ const rawId = acc.userId ?? acc.accountId;
83
+ const key = rawId.replace(/@im\.wechat(@im\.wechat)+$/, "@im.wechat");
84
+ if (!byUserId.has(key))
85
+ byUserId.set(key, []);
86
+ byUserId.get(key).push(acc);
87
+ }
88
+ let removed = 0;
89
+ for (const [userId, accounts] of byUserId) {
90
+ if (accounts.length <= 1)
91
+ continue;
92
+ // Sort by savedAt desc — keep newest
93
+ accounts.sort((a, b) => {
94
+ const ta = a.savedAt ? new Date(a.savedAt).getTime() : 0;
95
+ const tb = b.savedAt ? new Date(b.savedAt).getTime() : 0;
96
+ return tb - ta;
97
+ });
98
+ const [keep, ...remove] = accounts;
99
+ console.log(`UserId: ${userId}`);
100
+ console.log(` ✓ Keep: ${keep.accountId} (${keep.savedAt ?? "?"})`);
101
+ for (const acc of remove) {
102
+ removeAccount(acc.accountId);
103
+ console.log(` ✗ Removed: ${acc.accountId} (${acc.savedAt ?? "?"})`);
104
+ removed++;
105
+ }
106
+ }
107
+ if (removed === 0) {
108
+ console.log("✅ No duplicates found.");
109
+ }
110
+ else {
111
+ console.log(`\n✅ Cleaned ${removed} duplicate account(s).`);
112
+ }
113
+ }
114
+ else if (subcommand === "use") {
115
+ const accountId = args[1];
116
+ if (!accountId) {
117
+ console.error("Usage: accounts use <accountId>");
118
+ process.exit(1);
119
+ }
120
+ const filePath = path.join(ACCOUNTS_DIR, `${accountId}.json`);
121
+ if (!fs.existsSync(filePath)) {
122
+ console.error(`❌ Account not found: ${accountId}`);
123
+ process.exit(1);
124
+ }
125
+ console.log(`export WEIXIN_ACCOUNT_ID="${accountId}"`);
126
+ }
127
+ else {
128
+ console.error(`Unknown accounts subcommand: ${subcommand}`);
129
+ console.error("Usage: accounts [list|remove <id>|clean|use <id>]");
130
+ process.exit(1);
131
+ }
132
+ }
package/dist/cli.js CHANGED
@@ -41,6 +41,10 @@ else if (command === "logs") {
41
41
  const { showLogs } = await import("./daemon.js");
42
42
  showLogs(follow);
43
43
  }
44
+ else if (command === "accounts") {
45
+ const { manageAccounts } = await import("./accounts.js");
46
+ await manageAccounts(process.argv.slice(3)); // [subcommand, ...args]
47
+ }
44
48
  else if (command === undefined || command === "serve") {
45
49
  // Default: stdio MCP server (for Claude Desktop integration)
46
50
  await import("./index.js");
@@ -51,13 +55,17 @@ else {
51
55
  Usage: npx weixin-mcp [command]
52
56
 
53
57
  Commands:
54
- (no args) Start stdio MCP server (Claude Desktop mode)
55
- login QR code login
56
- status Show account and daemon status
57
- start [--port n] Start HTTP MCP daemon in background (default port: 3001)
58
- stop Stop daemon
59
- restart Restart daemon
60
- logs [-f] Show daemon logs (-f to follow)
58
+ (no args) Start stdio MCP server (Claude Desktop mode)
59
+ login QR code login
60
+ status Show account and daemon status
61
+ start [--port n] Start HTTP MCP daemon in background (default: 3001)
62
+ stop Stop daemon
63
+ restart Restart daemon
64
+ logs [-f] Show daemon logs (-f to follow)
65
+ accounts [list] List all accounts
66
+ accounts remove <id> Remove an account
67
+ accounts clean Remove duplicate accounts (same userId), keep newest
68
+ accounts use <id> Print export command for switching accounts
61
69
  `);
62
70
  process.exit(1);
63
71
  }
package/dist/login.js CHANGED
@@ -76,7 +76,8 @@ export async function main() {
76
76
  // Use bot_id or a random ID as account identifier
77
77
  const accountId = status.ilink_bot_id?.replace("@", "-").replace(".", "-")
78
78
  ?? crypto.randomBytes(6).toString("hex") + "-im-bot";
79
- saveAccount(accountId, token, baseUrl, userId ? `${userId}@im.wechat` : undefined);
79
+ // userId from API already contains @im.wechat suffix — don't append again
80
+ saveAccount(accountId, token, baseUrl, userId ?? undefined);
80
81
  console.log(`\n🎉 Logged in! Account: ${accountId}`);
81
82
  console.log(` UserId: ${userId ?? "(unknown)"}`);
82
83
  console.log("\nYou can now start the MCP server:");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weixin-mcp",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
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",
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Account management commands:
3
+ * npx weixin-mcp accounts list — list all accounts (default)
4
+ * npx weixin-mcp accounts remove <id> — remove a specific account
5
+ * npx weixin-mcp accounts clean — remove duplicate accounts (same userId), keep newest
6
+ * npx weixin-mcp accounts use <id> — print export WEIXIN_ACCOUNT_ID=<id>
7
+ */
8
+
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import { ACCOUNTS_DIR } from "./paths.js";
12
+
13
+ interface AccountData {
14
+ token?: string;
15
+ baseUrl?: string;
16
+ userId?: string;
17
+ savedAt?: string;
18
+ }
19
+
20
+ function listFiles(): string[] {
21
+ try {
22
+ return fs
23
+ .readdirSync(ACCOUNTS_DIR)
24
+ .filter((f) => f.endsWith(".json") && !f.endsWith(".sync.json") && !f.endsWith(".cursor.json"));
25
+ } catch {
26
+ return [];
27
+ }
28
+ }
29
+
30
+ function loadAccount(file: string): AccountData & { accountId: string } {
31
+ const accountId = file.replace(".json", "");
32
+ const data = JSON.parse(fs.readFileSync(path.join(ACCOUNTS_DIR, file), "utf-8")) as AccountData;
33
+ return { ...data, accountId };
34
+ }
35
+
36
+ function removeAccount(accountId: string) {
37
+ const filePath = path.join(ACCOUNTS_DIR, `${accountId}.json`);
38
+ const cursorPath = path.join(ACCOUNTS_DIR, `${accountId}.cursor.json`);
39
+ let removed = false;
40
+ if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); removed = true; }
41
+ if (fs.existsSync(cursorPath)) { fs.unlinkSync(cursorPath); }
42
+ return removed;
43
+ }
44
+
45
+ export async function manageAccounts(args: string[]) {
46
+ const subcommand = args[0] ?? "list";
47
+
48
+ if (subcommand === "list") {
49
+ const files = listFiles();
50
+ if (files.length === 0) {
51
+ console.log("No accounts found. Run: npx weixin-mcp login");
52
+ return;
53
+ }
54
+ const active = process.env.WEIXIN_ACCOUNT_ID ?? files[0].replace(".json", "");
55
+ console.log(`Accounts (${files.length}):\n`);
56
+ for (const file of files) {
57
+ const acc = loadAccount(file);
58
+ const isActive = acc.accountId === active;
59
+ const savedAt = acc.savedAt ? new Date(acc.savedAt).toLocaleString() : "unknown";
60
+ console.log(`${isActive ? "● " : " "}${acc.accountId}`);
61
+ const displayId = acc.userId?.replace(/@im\.wechat(@im\.wechat)+$/, "@im.wechat") ?? "(unknown)";
62
+ console.log(` User: ${displayId}`);
63
+ console.log(` Saved: ${savedAt}`);
64
+ }
65
+ if (files.length > 1) {
66
+ console.log(`\nActive: ${active} (set WEIXIN_ACCOUNT_ID to change)`);
67
+ }
68
+
69
+ } else if (subcommand === "remove") {
70
+ const accountId = args[1];
71
+ if (!accountId) { console.error("Usage: accounts remove <accountId>"); process.exit(1); }
72
+ if (removeAccount(accountId)) {
73
+ console.log(`✅ Removed: ${accountId}`);
74
+ } else {
75
+ console.error(`❌ Account not found: ${accountId}`);
76
+ process.exit(1);
77
+ }
78
+
79
+ } else if (subcommand === "clean") {
80
+ const files = listFiles();
81
+ const byUserId = new Map<string, Array<AccountData & { accountId: string }>>();
82
+
83
+ for (const file of files) {
84
+ const acc = loadAccount(file);
85
+ // Normalize userId: strip duplicate @im.wechat suffix
86
+ const rawId = acc.userId ?? acc.accountId;
87
+ const key = rawId.replace(/@im\.wechat(@im\.wechat)+$/, "@im.wechat");
88
+ if (!byUserId.has(key)) byUserId.set(key, []);
89
+ byUserId.get(key)!.push(acc);
90
+ }
91
+
92
+ let removed = 0;
93
+ for (const [userId, accounts] of byUserId) {
94
+ if (accounts.length <= 1) continue;
95
+ // Sort by savedAt desc — keep newest
96
+ accounts.sort((a, b) => {
97
+ const ta = a.savedAt ? new Date(a.savedAt).getTime() : 0;
98
+ const tb = b.savedAt ? new Date(b.savedAt).getTime() : 0;
99
+ return tb - ta;
100
+ });
101
+ const [keep, ...remove] = accounts;
102
+ console.log(`UserId: ${userId}`);
103
+ console.log(` ✓ Keep: ${keep.accountId} (${keep.savedAt ?? "?"})`);
104
+ for (const acc of remove) {
105
+ removeAccount(acc.accountId);
106
+ console.log(` ✗ Removed: ${acc.accountId} (${acc.savedAt ?? "?"})`);
107
+ removed++;
108
+ }
109
+ }
110
+
111
+ if (removed === 0) {
112
+ console.log("✅ No duplicates found.");
113
+ } else {
114
+ console.log(`\n✅ Cleaned ${removed} duplicate account(s).`);
115
+ }
116
+
117
+ } else if (subcommand === "use") {
118
+ const accountId = args[1];
119
+ if (!accountId) { console.error("Usage: accounts use <accountId>"); process.exit(1); }
120
+ const filePath = path.join(ACCOUNTS_DIR, `${accountId}.json`);
121
+ if (!fs.existsSync(filePath)) {
122
+ console.error(`❌ Account not found: ${accountId}`);
123
+ process.exit(1);
124
+ }
125
+ console.log(`export WEIXIN_ACCOUNT_ID="${accountId}"`);
126
+
127
+ } else {
128
+ console.error(`Unknown accounts subcommand: ${subcommand}`);
129
+ console.error("Usage: accounts [list|remove <id>|clean|use <id>]");
130
+ process.exit(1);
131
+ }
132
+ }
package/src/cli.ts CHANGED
@@ -43,6 +43,10 @@ if (command === "login") {
43
43
  const { showLogs } = await import("./daemon.js");
44
44
  showLogs(follow);
45
45
 
46
+ } else if (command === "accounts") {
47
+ const { manageAccounts } = await import("./accounts.js");
48
+ await manageAccounts(process.argv.slice(3)); // [subcommand, ...args]
49
+
46
50
  } else if (command === undefined || command === "serve") {
47
51
  // Default: stdio MCP server (for Claude Desktop integration)
48
52
  await import("./index.js");
@@ -53,13 +57,17 @@ if (command === "login") {
53
57
  Usage: npx weixin-mcp [command]
54
58
 
55
59
  Commands:
56
- (no args) Start stdio MCP server (Claude Desktop mode)
57
- login QR code login
58
- status Show account and daemon status
59
- start [--port n] Start HTTP MCP daemon in background (default port: 3001)
60
- stop Stop daemon
61
- restart Restart daemon
62
- logs [-f] Show daemon logs (-f to follow)
60
+ (no args) Start stdio MCP server (Claude Desktop mode)
61
+ login QR code login
62
+ status Show account and daemon status
63
+ start [--port n] Start HTTP MCP daemon in background (default: 3001)
64
+ stop Stop daemon
65
+ restart Restart daemon
66
+ logs [-f] Show daemon logs (-f to follow)
67
+ accounts [list] List all accounts
68
+ accounts remove <id> Remove an account
69
+ accounts clean Remove duplicate accounts (same userId), keep newest
70
+ accounts use <id> Print export command for switching accounts
63
71
  `);
64
72
  process.exit(1);
65
73
  }
package/src/login.ts CHANGED
@@ -104,7 +104,8 @@ export async function main() {
104
104
  // Use bot_id or a random ID as account identifier
105
105
  const accountId = status.ilink_bot_id?.replace("@", "-").replace(".", "-")
106
106
  ?? crypto.randomBytes(6).toString("hex") + "-im-bot";
107
- saveAccount(accountId, token, baseUrl, userId ? `${userId}@im.wechat` : undefined);
107
+ // userId from API already contains @im.wechat suffix — don't append again
108
+ saveAccount(accountId, token, baseUrl, userId ?? undefined);
108
109
  console.log(`\n🎉 Logged in! Account: ${accountId}`);
109
110
  console.log(` UserId: ${userId ?? "(unknown)"}`);
110
111
  console.log("\nYou can now start the MCP server:");