whats-mcp 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/.dockerignore +12 -0
- package/.gitlab-ci.yml +54 -0
- package/CHANGELOG.md +38 -0
- package/README.md +205 -0
- package/TODO.md +6 -0
- package/config.json +19 -0
- package/package.json +46 -0
- package/src/.env.example +21 -0
- package/src/admin/cli.js +916 -0
- package/src/admin/service.js +271 -0
- package/src/admin/telegram.js +178 -0
- package/src/admin.js +12 -0
- package/src/config.js +147 -0
- package/src/connection.js +334 -0
- package/src/helpers.js +264 -0
- package/src/http_app.js +267 -0
- package/src/index.js +4 -0
- package/src/main.js +71 -0
- package/src/server.js +67 -0
- package/src/store.js +925 -0
- package/src/tools/analytics.js +157 -0
- package/src/tools/channels.js +215 -0
- package/src/tools/chats.js +291 -0
- package/src/tools/contacts.js +259 -0
- package/src/tools/digest.js +249 -0
- package/src/tools/groups.js +529 -0
- package/src/tools/history-support.js +114 -0
- package/src/tools/labels.js +168 -0
- package/src/tools/messaging.js +510 -0
- package/src/tools/overview.js +416 -0
- package/src/tools/profile.js +155 -0
- package/src/tools/registry.js +105 -0
- package/src/tools/tags.js +104 -0
- package/src/tools/utils.js +325 -0
- package/src/tools/watchlists.js +136 -0
package/src/admin/cli.js
ADDED
|
@@ -0,0 +1,916 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* whats-admin — CLI administration tool for whats-mcp.
|
|
4
|
+
*
|
|
5
|
+
* Manage WhatsApp authentication, server lifecycle, configuration, and logs.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* whats-admin status Overview of server + auth state
|
|
9
|
+
* whats-admin guide Shared operator capability summary
|
|
10
|
+
* whats-admin login [--code] Login via QR (default) or pairing code
|
|
11
|
+
* whats-admin logout [-f] Clear WhatsApp session
|
|
12
|
+
* whats-admin auth <sub> Manage auth state
|
|
13
|
+
* whats-admin server <sub> Manage MCP server lifecycle
|
|
14
|
+
* whats-admin config <sub> Manage configuration
|
|
15
|
+
* whats-admin logs <sub> View/manage logs
|
|
16
|
+
* whats-admin info Show version & environment info
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
"use strict";
|
|
20
|
+
|
|
21
|
+
const { Command } = require("commander");
|
|
22
|
+
const fs = require("fs");
|
|
23
|
+
const path = require("path");
|
|
24
|
+
const os = require("os");
|
|
25
|
+
const { spawn, execSync } = require("child_process");
|
|
26
|
+
const readline = require("readline");
|
|
27
|
+
const { adminHelpText } = require("./service");
|
|
28
|
+
|
|
29
|
+
// ── Paths ────────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
const PKG_ROOT = path.resolve(__dirname, "../..");
|
|
32
|
+
const PKG_JSON = require(path.join(PKG_ROOT, "package.json"));
|
|
33
|
+
const CONFIG_FILE = path.join(PKG_ROOT, "config.json");
|
|
34
|
+
|
|
35
|
+
function stateDir() {
|
|
36
|
+
const { loadConfig } = require("../config");
|
|
37
|
+
const cfg = loadConfig();
|
|
38
|
+
return (cfg.server?.state_directory || "~/.mcps/whatsapp").replace(
|
|
39
|
+
/^~/,
|
|
40
|
+
os.homedir(),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function authDir() { return path.join(stateDir(), "auth"); }
|
|
45
|
+
function pidFile() { return path.join(stateDir(), "whats-mcp.pid"); }
|
|
46
|
+
function logFile() { return path.join(stateDir(), "whats-mcp.log"); }
|
|
47
|
+
|
|
48
|
+
// ── Colors (zero-dep ANSI) ───────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
const isColor = process.stdout.isTTY !== false;
|
|
51
|
+
|
|
52
|
+
const c = {
|
|
53
|
+
green: (s) => (isColor ? `\x1b[32m${s}\x1b[0m` : s),
|
|
54
|
+
red: (s) => (isColor ? `\x1b[31m${s}\x1b[0m` : s),
|
|
55
|
+
yellow: (s) => (isColor ? `\x1b[33m${s}\x1b[0m` : s),
|
|
56
|
+
cyan: (s) => (isColor ? `\x1b[36m${s}\x1b[0m` : s),
|
|
57
|
+
bold: (s) => (isColor ? `\x1b[1m${s}\x1b[0m` : s),
|
|
58
|
+
dim: (s) => (isColor ? `\x1b[2m${s}\x1b[0m` : s),
|
|
59
|
+
magenta: (s) => (isColor ? `\x1b[35m${s}\x1b[0m` : s),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const ok = () => c.green("✓");
|
|
63
|
+
const fail = () => c.red("✗");
|
|
64
|
+
const warn = () => c.yellow("⚠");
|
|
65
|
+
|
|
66
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
function readPid() {
|
|
69
|
+
try {
|
|
70
|
+
return parseInt(fs.readFileSync(pidFile(), "utf-8").trim(), 10);
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isRunning(pid) {
|
|
77
|
+
if (!pid) return false;
|
|
78
|
+
try {
|
|
79
|
+
process.kill(pid, 0);
|
|
80
|
+
return true;
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function authExists() {
|
|
87
|
+
const dir = authDir();
|
|
88
|
+
return fs.existsSync(dir) && fs.readdirSync(dir).length > 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function prompt(question) {
|
|
92
|
+
const rl = readline.createInterface({
|
|
93
|
+
input: process.stdin,
|
|
94
|
+
output: process.stderr,
|
|
95
|
+
});
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
rl.question(question, (answer) => {
|
|
98
|
+
rl.close();
|
|
99
|
+
resolve(answer.trim());
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function confirm(question) {
|
|
105
|
+
return prompt(`${question} [y/N] `).then(
|
|
106
|
+
(a) => a.toLowerCase() === "y",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function normalizePhoneNumber(raw) {
|
|
111
|
+
return String(raw || "").replace(/[^\d]/g, "");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Read auth creds.json and return the "me" field if present. */
|
|
115
|
+
function readCredsMe() {
|
|
116
|
+
try {
|
|
117
|
+
const credsFile = path.join(authDir(), "creds.json");
|
|
118
|
+
const creds = JSON.parse(fs.readFileSync(credsFile, "utf-8"));
|
|
119
|
+
return creds.me || null;
|
|
120
|
+
} catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Shared Baileys socket creator for login flows.
|
|
127
|
+
* Returns { sock, saveCreds, version }.
|
|
128
|
+
*/
|
|
129
|
+
async function _makeLoginSocket() {
|
|
130
|
+
const {
|
|
131
|
+
default: makeWASocket,
|
|
132
|
+
useMultiFileAuthState,
|
|
133
|
+
makeCacheableSignalKeyStore,
|
|
134
|
+
fetchLatestBaileysVersion,
|
|
135
|
+
Browsers,
|
|
136
|
+
} = require("@whiskeysockets/baileys");
|
|
137
|
+
const pino = require("pino");
|
|
138
|
+
|
|
139
|
+
const logger = pino({ level: "silent" });
|
|
140
|
+
const { version } = await fetchLatestBaileysVersion();
|
|
141
|
+
const ap = authDir();
|
|
142
|
+
fs.mkdirSync(ap, { recursive: true });
|
|
143
|
+
|
|
144
|
+
const { state, saveCreds } = await useMultiFileAuthState(ap);
|
|
145
|
+
|
|
146
|
+
const sock = makeWASocket({
|
|
147
|
+
version,
|
|
148
|
+
auth: {
|
|
149
|
+
creds: state.creds,
|
|
150
|
+
keys: makeCacheableSignalKeyStore(state.keys, logger),
|
|
151
|
+
},
|
|
152
|
+
browser: Browsers.ubuntu("Chrome"),
|
|
153
|
+
logger,
|
|
154
|
+
markOnlineOnConnect: false,
|
|
155
|
+
generateHighQualityLinkPreview: false,
|
|
156
|
+
syncFullHistory: true,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
sock.ev.on("creds.update", saveCreds);
|
|
160
|
+
|
|
161
|
+
return { sock, saveCreds, version };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
165
|
+
// Program
|
|
166
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
167
|
+
|
|
168
|
+
const program = new Command();
|
|
169
|
+
program
|
|
170
|
+
.name("whats-admin")
|
|
171
|
+
.description(
|
|
172
|
+
"whats-mcp administration — manage WhatsApp auth, server, config & logs",
|
|
173
|
+
)
|
|
174
|
+
.version(PKG_JSON.version);
|
|
175
|
+
|
|
176
|
+
program
|
|
177
|
+
.command("guide")
|
|
178
|
+
.description("Show the shared operator capability summary")
|
|
179
|
+
.action(() => {
|
|
180
|
+
console.log(adminHelpText());
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// ── status ───────────────────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
program
|
|
186
|
+
.command("status")
|
|
187
|
+
.description("Show overview of server + WhatsApp auth state")
|
|
188
|
+
.action(() => {
|
|
189
|
+
console.log(c.bold("\n whats-mcp Status\n"));
|
|
190
|
+
|
|
191
|
+
// Server
|
|
192
|
+
const pid = readPid();
|
|
193
|
+
const running = isRunning(pid);
|
|
194
|
+
console.log(
|
|
195
|
+
` Server: ${running ? ok() + c.green(` Running (PID ${pid})`) : fail() + c.dim(" Stopped")}`,
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
// Auth
|
|
199
|
+
const hasAuth = authExists();
|
|
200
|
+
const me = hasAuth ? readCredsMe() : null;
|
|
201
|
+
console.log(
|
|
202
|
+
` Auth: ${hasAuth ? ok() + c.green(" Paired") : fail() + c.dim(" Not paired")}`,
|
|
203
|
+
);
|
|
204
|
+
if (me) {
|
|
205
|
+
console.log(
|
|
206
|
+
` Account: ${c.cyan(me.name || me.verifiedName || "?")} (${c.cyan(me.id?.split(":")[0] || "?")})`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Config
|
|
211
|
+
const hasConfig = fs.existsSync(CONFIG_FILE);
|
|
212
|
+
console.log(
|
|
213
|
+
` Config: ${hasConfig ? ok() + c.dim(" Custom config.json") : c.dim(" Defaults (no config.json)")}`,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// Hints
|
|
217
|
+
if (!hasAuth) {
|
|
218
|
+
console.log(
|
|
219
|
+
`\n ${warn()} Run ${c.bold("whats-admin login")} to pair WhatsApp.`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
if (!running && hasAuth) {
|
|
223
|
+
console.log(
|
|
224
|
+
`\n ${warn()} Server stopped. Your MCP client will start it automatically,`,
|
|
225
|
+
);
|
|
226
|
+
console.log(
|
|
227
|
+
` or configure it in your MCP client settings.`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
console.log();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// ── login ────────────────────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
program
|
|
236
|
+
.command("login")
|
|
237
|
+
.description("Login to WhatsApp (interactive QR or pairing code)")
|
|
238
|
+
.option("--code", "Use pairing code instead of QR (enter phone number)")
|
|
239
|
+
.option("--force", "Overwrite any existing auth credentials without prompting")
|
|
240
|
+
.option(
|
|
241
|
+
"--phone <number>",
|
|
242
|
+
"Phone number for pairing code (with country code, e.g. 33612345678)",
|
|
243
|
+
)
|
|
244
|
+
.action(async (opts) => {
|
|
245
|
+
if (authExists()) {
|
|
246
|
+
const me = readCredsMe();
|
|
247
|
+
const who = me ? ` (${me.name || "?"} / ${me.id?.split(":")[0] || "?"})` : "";
|
|
248
|
+
const overwrite = opts.force
|
|
249
|
+
? true
|
|
250
|
+
: await confirm(` ${warn()} Auth credentials already exist${who}. Overwrite?`);
|
|
251
|
+
if (!overwrite) {
|
|
252
|
+
console.log(" Aborted.");
|
|
253
|
+
process.exit(0);
|
|
254
|
+
}
|
|
255
|
+
fs.rmSync(authDir(), { recursive: true, force: true });
|
|
256
|
+
fs.mkdirSync(authDir(), { recursive: true });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
console.log(c.bold("\n WhatsApp Login\n"));
|
|
260
|
+
|
|
261
|
+
if (opts.code) {
|
|
262
|
+
let phone = opts.phone;
|
|
263
|
+
if (!phone) {
|
|
264
|
+
phone = await prompt(
|
|
265
|
+
" Phone number (with country code, e.g. 33612345678): ",
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
phone = normalizePhoneNumber(phone);
|
|
269
|
+
if (!phone || !/^\d{8,15}$/.test(phone)) {
|
|
270
|
+
console.error(
|
|
271
|
+
c.red(" Invalid phone number. Use country code; separators are allowed and will be stripped."),
|
|
272
|
+
);
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
console.log(`\n ${c.dim("Connecting to WhatsApp...")}\n`);
|
|
276
|
+
await _loginWithCode(phone);
|
|
277
|
+
} else {
|
|
278
|
+
console.log(
|
|
279
|
+
` ${c.dim("Connecting to WhatsApp... QR code will appear below.")}`,
|
|
280
|
+
);
|
|
281
|
+
console.log(
|
|
282
|
+
` ${c.dim("Open WhatsApp → Settings → Linked Devices → Link a Device")}\n`,
|
|
283
|
+
);
|
|
284
|
+
await _loginWithQR();
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* QR-based login: reuse the connection module which handles QR display,
|
|
290
|
+
* reconnects on 515, and writes creds. We poll for "open" state.
|
|
291
|
+
*/
|
|
292
|
+
async function _loginWithQR() {
|
|
293
|
+
const { connect, getConnectionInfo } = require("../connection");
|
|
294
|
+
const { loadConfig } = require("../config");
|
|
295
|
+
|
|
296
|
+
const maxWait = 120_000; // 2 minutes for user to scan QR
|
|
297
|
+
const start = Date.now();
|
|
298
|
+
|
|
299
|
+
connect(loadConfig()).catch((err) => {
|
|
300
|
+
console.error(c.red(`\n Connection error: ${err.message}`));
|
|
301
|
+
process.exit(1);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
return new Promise((resolve) => {
|
|
305
|
+
const check = setInterval(() => {
|
|
306
|
+
const info = getConnectionInfo();
|
|
307
|
+
|
|
308
|
+
if (info.state === "open") {
|
|
309
|
+
clearInterval(check);
|
|
310
|
+
console.log(
|
|
311
|
+
c.green(
|
|
312
|
+
`\n ${ok()} Connected as ${info.user?.name || "?"} (${info.user?.phone || "?"})`,
|
|
313
|
+
),
|
|
314
|
+
);
|
|
315
|
+
console.log(c.dim(` Auth saved to ${authDir()}\n`));
|
|
316
|
+
// Let creds flush to disk
|
|
317
|
+
setTimeout(() => process.exit(0), 2000);
|
|
318
|
+
resolve();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (Date.now() - start > maxWait) {
|
|
322
|
+
clearInterval(check);
|
|
323
|
+
console.error(
|
|
324
|
+
c.red("\n Timeout: QR code was not scanned within 2 minutes."),
|
|
325
|
+
);
|
|
326
|
+
console.error(c.dim(" Run the command again to get a fresh QR.\n"));
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
}, 1000);
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Pairing-code login: create a socket specifically for this flow,
|
|
335
|
+
* intercept the QR event to request a pairing code instead.
|
|
336
|
+
*/
|
|
337
|
+
async function _loginWithCode(phone) {
|
|
338
|
+
const {
|
|
339
|
+
DisconnectReason,
|
|
340
|
+
} = require("@whiskeysockets/baileys");
|
|
341
|
+
|
|
342
|
+
let attempt = 0;
|
|
343
|
+
const maxAttempts = 3;
|
|
344
|
+
|
|
345
|
+
async function tryLogin() {
|
|
346
|
+
attempt++;
|
|
347
|
+
const { sock } = await _makeLoginSocket();
|
|
348
|
+
let codeRequested = false;
|
|
349
|
+
|
|
350
|
+
return new Promise((resolve, reject) => {
|
|
351
|
+
const timeout = setTimeout(() => {
|
|
352
|
+
sock.end(undefined);
|
|
353
|
+
reject(new Error("Timeout waiting for connection (2 minutes)."));
|
|
354
|
+
}, 120_000);
|
|
355
|
+
|
|
356
|
+
sock.ev.on("connection.update", async (update) => {
|
|
357
|
+
const { connection, lastDisconnect, qr } = update;
|
|
358
|
+
|
|
359
|
+
// QR event = socket is ready → request pairing code instead
|
|
360
|
+
if (qr && !codeRequested) {
|
|
361
|
+
codeRequested = true;
|
|
362
|
+
try {
|
|
363
|
+
const code = await sock.requestPairingCode(phone);
|
|
364
|
+
console.log(
|
|
365
|
+
` ${c.bold("Pairing Code:")} ${c.green(c.bold(code))}`,
|
|
366
|
+
);
|
|
367
|
+
console.log();
|
|
368
|
+
console.log(
|
|
369
|
+
` ${c.dim("Go to WhatsApp → Linked Devices → Link with Phone Number")}`,
|
|
370
|
+
);
|
|
371
|
+
console.log(
|
|
372
|
+
` ${c.dim("Enter this code on your phone.")}\n`,
|
|
373
|
+
);
|
|
374
|
+
} catch (err) {
|
|
375
|
+
clearTimeout(timeout);
|
|
376
|
+
reject(
|
|
377
|
+
new Error(`Failed to get pairing code: ${err.message}`),
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (connection === "open") {
|
|
383
|
+
clearTimeout(timeout);
|
|
384
|
+
const user = sock.user;
|
|
385
|
+
console.log(
|
|
386
|
+
c.green(
|
|
387
|
+
` ${ok()} Connected as ${user?.name || "?"} (${user?.id?.split(":")[0] || "?"})`,
|
|
388
|
+
),
|
|
389
|
+
);
|
|
390
|
+
console.log(c.dim(` Auth saved to ${authDir()}\n`));
|
|
391
|
+
sock.end(undefined);
|
|
392
|
+
setTimeout(() => resolve(), 2000);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (connection === "close") {
|
|
396
|
+
const code = lastDisconnect?.error?.output?.statusCode;
|
|
397
|
+
|
|
398
|
+
// 515 = restartRequired — normal during first connect
|
|
399
|
+
if (code === DisconnectReason.restartRequired && attempt < maxAttempts) {
|
|
400
|
+
clearTimeout(timeout);
|
|
401
|
+
console.log(c.dim(` Restart required — reconnecting... (${attempt}/${maxAttempts})`));
|
|
402
|
+
tryLogin().then(resolve).catch(reject);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// 401 / loggedOut — pairing failed
|
|
407
|
+
if (code === DisconnectReason.loggedOut) {
|
|
408
|
+
clearTimeout(timeout);
|
|
409
|
+
reject(new Error("Pairing failed or session rejected."));
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// 440 = connectionReplaced — another login took over
|
|
414
|
+
if (code === DisconnectReason.connectionReplaced) {
|
|
415
|
+
clearTimeout(timeout);
|
|
416
|
+
reject(new Error("Connection replaced by another session."));
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Other transient errors during first login: just log and continue waiting
|
|
421
|
+
if (code === DisconnectReason.timedOut || code === DisconnectReason.connectionClosed) {
|
|
422
|
+
if (attempt < maxAttempts) {
|
|
423
|
+
clearTimeout(timeout);
|
|
424
|
+
console.log(c.dim(` Disconnected (${code}) — retrying... (${attempt}/${maxAttempts})`));
|
|
425
|
+
tryLogin().then(resolve).catch(reject);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
try {
|
|
434
|
+
await tryLogin();
|
|
435
|
+
process.exit(0);
|
|
436
|
+
} catch (err) {
|
|
437
|
+
console.error(c.red(`\n ${fail()} ${err.message}\n`));
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ── logout ───────────────────────────────────────────────────────────────────
|
|
443
|
+
|
|
444
|
+
program
|
|
445
|
+
.command("logout")
|
|
446
|
+
.description("Log out WhatsApp session and clear credentials")
|
|
447
|
+
.option("-f, --force", "Skip confirmation prompt")
|
|
448
|
+
.action(async (opts) => {
|
|
449
|
+
if (!authExists()) {
|
|
450
|
+
console.log(` ${c.dim("No auth credentials found. Nothing to do.")}`);
|
|
451
|
+
process.exit(0);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const me = readCredsMe();
|
|
455
|
+
const who = me ? ` (${me.name || "?"})` : "";
|
|
456
|
+
|
|
457
|
+
if (!opts.force) {
|
|
458
|
+
const yes = await confirm(
|
|
459
|
+
` ${warn()} This will delete your WhatsApp pairing${who}. Continue?`,
|
|
460
|
+
);
|
|
461
|
+
if (!yes) {
|
|
462
|
+
console.log(" Aborted.");
|
|
463
|
+
process.exit(0);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Try graceful logout (notify WhatsApp servers)
|
|
468
|
+
console.log(c.dim(" Attempting graceful logout..."));
|
|
469
|
+
try {
|
|
470
|
+
const { sock } = await _makeLoginSocket();
|
|
471
|
+
|
|
472
|
+
await new Promise((resolve) => {
|
|
473
|
+
const timeout = setTimeout(() => {
|
|
474
|
+
try { sock.end(undefined); } catch { /* ignore */ }
|
|
475
|
+
resolve();
|
|
476
|
+
}, 10_000);
|
|
477
|
+
|
|
478
|
+
sock.ev.on("connection.update", async ({ connection }) => {
|
|
479
|
+
if (connection === "open") {
|
|
480
|
+
clearTimeout(timeout);
|
|
481
|
+
try {
|
|
482
|
+
await sock.logout();
|
|
483
|
+
console.log(` ${ok()} Logged out from WhatsApp servers.`);
|
|
484
|
+
} catch {
|
|
485
|
+
console.log(c.dim(" Could not notify WhatsApp servers (session may already be invalid)."));
|
|
486
|
+
}
|
|
487
|
+
resolve();
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
} catch {
|
|
492
|
+
console.log(c.dim(" Graceful logout skipped (could not connect)."));
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Delete auth folder
|
|
496
|
+
fs.rmSync(authDir(), { recursive: true, force: true });
|
|
497
|
+
console.log(` ${ok()} Auth credentials deleted.`);
|
|
498
|
+
|
|
499
|
+
// Stop server if running
|
|
500
|
+
const pid = readPid();
|
|
501
|
+
if (isRunning(pid)) {
|
|
502
|
+
try {
|
|
503
|
+
process.kill(pid, "SIGTERM");
|
|
504
|
+
console.log(` ${ok()} MCP server stopped (PID ${pid}).`);
|
|
505
|
+
} catch { /* ignore */ }
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
console.log();
|
|
509
|
+
process.exit(0);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
513
|
+
// auth
|
|
514
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
515
|
+
|
|
516
|
+
const authCmd = program
|
|
517
|
+
.command("auth")
|
|
518
|
+
.description("Manage WhatsApp authentication");
|
|
519
|
+
|
|
520
|
+
authCmd
|
|
521
|
+
.command("status")
|
|
522
|
+
.description("Show detailed auth credential status")
|
|
523
|
+
.action(() => {
|
|
524
|
+
const dir = authDir();
|
|
525
|
+
const exists = authExists();
|
|
526
|
+
|
|
527
|
+
console.log(c.bold("\n Auth Status\n"));
|
|
528
|
+
console.log(` Path: ${c.cyan(dir)}`);
|
|
529
|
+
console.log(
|
|
530
|
+
` Paired: ${exists ? ok() + c.green(" Yes") : fail() + c.red(" No")}`,
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
if (exists) {
|
|
534
|
+
const files = fs.readdirSync(dir);
|
|
535
|
+
console.log(` Files: ${c.dim(files.length + " credential files")}`);
|
|
536
|
+
|
|
537
|
+
const me = readCredsMe();
|
|
538
|
+
if (me) {
|
|
539
|
+
console.log(` Account: ${c.green(me.name || me.verifiedName || "?")}`);
|
|
540
|
+
console.log(` Phone: ${c.cyan(me.id?.split(":")[0] || "?")}`);
|
|
541
|
+
console.log(` JID: ${c.dim(me.id || "?")}`);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Show creds.json age
|
|
545
|
+
try {
|
|
546
|
+
const stat = fs.statSync(path.join(dir, "creds.json"));
|
|
547
|
+
const age = Date.now() - stat.mtimeMs;
|
|
548
|
+
const hours = Math.floor(age / 3_600_000);
|
|
549
|
+
const days = Math.floor(hours / 24);
|
|
550
|
+
const ageStr =
|
|
551
|
+
days > 0
|
|
552
|
+
? `${days}d ${hours % 24}h ago`
|
|
553
|
+
: hours > 0
|
|
554
|
+
? `${hours}h ago`
|
|
555
|
+
: "just now";
|
|
556
|
+
console.log(` Last auth: ${c.dim(ageStr)}`);
|
|
557
|
+
} catch { /* ignore */ }
|
|
558
|
+
}
|
|
559
|
+
console.log();
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
authCmd
|
|
563
|
+
.command("clean")
|
|
564
|
+
.description("Delete auth credentials (force re-pair)")
|
|
565
|
+
.option("-f, --force", "Skip confirmation")
|
|
566
|
+
.action(async (opts) => {
|
|
567
|
+
if (!authExists()) {
|
|
568
|
+
console.log(` ${c.dim("No auth credentials found.")}`);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
if (!opts.force) {
|
|
572
|
+
const yes = await confirm(` ${warn()} Delete auth credentials?`);
|
|
573
|
+
if (!yes) {
|
|
574
|
+
console.log(" Aborted.");
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
fs.rmSync(authDir(), { recursive: true, force: true });
|
|
579
|
+
console.log(` ${ok()} Auth credentials deleted.`);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
authCmd
|
|
583
|
+
.command("path")
|
|
584
|
+
.description("Print auth folder path")
|
|
585
|
+
.action(() => {
|
|
586
|
+
console.log(authDir());
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
590
|
+
// server
|
|
591
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
592
|
+
|
|
593
|
+
const serverCmd = program
|
|
594
|
+
.command("server")
|
|
595
|
+
.description("Manage MCP server lifecycle");
|
|
596
|
+
|
|
597
|
+
serverCmd
|
|
598
|
+
.command("status")
|
|
599
|
+
.description("Check if MCP server is running")
|
|
600
|
+
.action(() => {
|
|
601
|
+
const pid = readPid();
|
|
602
|
+
const running = isRunning(pid);
|
|
603
|
+
if (running) {
|
|
604
|
+
console.log(
|
|
605
|
+
` ${ok()} Server is ${c.green("running")} (PID ${pid}).`,
|
|
606
|
+
);
|
|
607
|
+
} else {
|
|
608
|
+
console.log(` ${fail()} Server is ${c.red("stopped")}.`);
|
|
609
|
+
if (pid) {
|
|
610
|
+
console.log(c.dim(` (Stale PID file with PID ${pid})`));
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
serverCmd
|
|
616
|
+
.command("stop")
|
|
617
|
+
.description("Stop the running MCP server")
|
|
618
|
+
.action(() => {
|
|
619
|
+
const pid = readPid();
|
|
620
|
+
if (!isRunning(pid)) {
|
|
621
|
+
console.log(` ${c.dim("Server is not running.")}`);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
try {
|
|
625
|
+
process.kill(pid, "SIGTERM");
|
|
626
|
+
console.log(` ${ok()} Server stopped (PID ${pid}).`);
|
|
627
|
+
} catch (e) {
|
|
628
|
+
console.error(` ${fail()} Failed to stop server: ${e.message}`);
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
serverCmd
|
|
633
|
+
.command("restart")
|
|
634
|
+
.description("Stop the MCP server (it will be restarted by MCP client)")
|
|
635
|
+
.action(async () => {
|
|
636
|
+
const pid = readPid();
|
|
637
|
+
if (!isRunning(pid)) {
|
|
638
|
+
console.log(` ${c.dim("Server is not running.")}`);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
try {
|
|
642
|
+
process.kill(pid, "SIGTERM");
|
|
643
|
+
console.log(` ${ok()} Server stopped (PID ${pid}).`);
|
|
644
|
+
console.log(c.dim(" Your MCP client will restart it automatically on next request."));
|
|
645
|
+
} catch (e) {
|
|
646
|
+
console.error(` ${fail()} Failed to stop server: ${e.message}`);
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
serverCmd
|
|
651
|
+
.command("reconnect")
|
|
652
|
+
.description("Alias for restart, used when the WhatsApp runtime needs a fresh connection cycle")
|
|
653
|
+
.action(() => {
|
|
654
|
+
const pid = readPid();
|
|
655
|
+
if (!isRunning(pid)) {
|
|
656
|
+
console.log(` ${c.dim("Server is not running.")}`);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
try {
|
|
660
|
+
process.kill(pid, "SIGTERM");
|
|
661
|
+
console.log(` ${ok()} Server stopped (PID ${pid}).`);
|
|
662
|
+
console.log(c.dim(" The service will reconnect on next start or MCP request."));
|
|
663
|
+
} catch (e) {
|
|
664
|
+
console.error(` ${fail()} Failed to reconnect server: ${e.message}`);
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
serverCmd
|
|
669
|
+
.command("pid")
|
|
670
|
+
.description("Print server PID (empty if not running)")
|
|
671
|
+
.action(() => {
|
|
672
|
+
const pid = readPid();
|
|
673
|
+
console.log(pid && isRunning(pid) ? String(pid) : "");
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
serverCmd
|
|
677
|
+
.command("test")
|
|
678
|
+
.description("Test WhatsApp connection (connect & disconnect)")
|
|
679
|
+
.action(async () => {
|
|
680
|
+
if (!authExists()) {
|
|
681
|
+
console.log(
|
|
682
|
+
` ${fail()} No auth credentials. Run ${c.bold("whats-admin login")} first.`,
|
|
683
|
+
);
|
|
684
|
+
process.exit(1);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
console.log(c.dim(" Connecting to WhatsApp..."));
|
|
688
|
+
|
|
689
|
+
const { connect, getConnectionInfo } = require("../connection");
|
|
690
|
+
const { loadConfig } = require("../config");
|
|
691
|
+
|
|
692
|
+
connect(loadConfig()).catch((err) => {
|
|
693
|
+
console.error(c.red(` Connection error: ${err.message}`));
|
|
694
|
+
process.exit(1);
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
// Wait for connection (max 30 seconds)
|
|
698
|
+
const start = Date.now();
|
|
699
|
+
const check = setInterval(() => {
|
|
700
|
+
const info = getConnectionInfo();
|
|
701
|
+
if (info.state === "open") {
|
|
702
|
+
clearInterval(check);
|
|
703
|
+
console.log(
|
|
704
|
+
` ${ok()} ${c.green("Connected")} as ${c.cyan(info.user?.name || "?")} (${info.user?.phone || "?"})`,
|
|
705
|
+
);
|
|
706
|
+
const stats = info.store_stats;
|
|
707
|
+
if (stats) {
|
|
708
|
+
console.log(
|
|
709
|
+
c.dim(
|
|
710
|
+
` Store: ${stats.chats} chats, ${stats.contacts} contacts, ${stats.messages} messages`,
|
|
711
|
+
),
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
console.log(` ${ok()} Connection test passed.\n`);
|
|
715
|
+
process.exit(0);
|
|
716
|
+
}
|
|
717
|
+
if (Date.now() - start > 30_000) {
|
|
718
|
+
clearInterval(check);
|
|
719
|
+
console.error(
|
|
720
|
+
c.red(` ${fail()} Connection timeout (30s). State: ${info.state}`),
|
|
721
|
+
);
|
|
722
|
+
process.exit(1);
|
|
723
|
+
}
|
|
724
|
+
}, 1000);
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
728
|
+
// config
|
|
729
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
730
|
+
|
|
731
|
+
const configCmd = program
|
|
732
|
+
.command("config")
|
|
733
|
+
.description("Manage configuration");
|
|
734
|
+
|
|
735
|
+
configCmd
|
|
736
|
+
.command("show")
|
|
737
|
+
.description("Print current merged configuration")
|
|
738
|
+
.action(() => {
|
|
739
|
+
const { loadConfig } = require("../config");
|
|
740
|
+
console.log(JSON.stringify(loadConfig(), null, 2));
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
configCmd
|
|
744
|
+
.command("edit")
|
|
745
|
+
.description("Open config.json in $EDITOR")
|
|
746
|
+
.action(() => {
|
|
747
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
748
|
+
const { DEFAULTS } = require("../config");
|
|
749
|
+
fs.writeFileSync(
|
|
750
|
+
CONFIG_FILE,
|
|
751
|
+
JSON.stringify(DEFAULTS, null, 2) + "\n",
|
|
752
|
+
);
|
|
753
|
+
console.log(` ${c.dim("Created config.json with defaults.")}`);
|
|
754
|
+
}
|
|
755
|
+
const editor = process.env.EDITOR || process.env.VISUAL || "vi";
|
|
756
|
+
try {
|
|
757
|
+
execSync(`${editor} "${CONFIG_FILE}"`, { stdio: "inherit" });
|
|
758
|
+
} catch (e) {
|
|
759
|
+
console.error(` ${fail()} Failed to open editor: ${e.message}`);
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
configCmd
|
|
764
|
+
.command("reset")
|
|
765
|
+
.description("Reset config.json to defaults")
|
|
766
|
+
.option("-f, --force", "Skip confirmation")
|
|
767
|
+
.action(async (opts) => {
|
|
768
|
+
if (!opts.force && fs.existsSync(CONFIG_FILE)) {
|
|
769
|
+
const yes = await confirm(
|
|
770
|
+
` ${warn()} Overwrite config.json with defaults?`,
|
|
771
|
+
);
|
|
772
|
+
if (!yes) {
|
|
773
|
+
console.log(" Aborted.");
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
const { DEFAULTS } = require("../config");
|
|
778
|
+
fs.writeFileSync(
|
|
779
|
+
CONFIG_FILE,
|
|
780
|
+
JSON.stringify(DEFAULTS, null, 2) + "\n",
|
|
781
|
+
);
|
|
782
|
+
console.log(` ${ok()} Config reset to defaults.`);
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
configCmd
|
|
786
|
+
.command("path")
|
|
787
|
+
.description("Print config file path")
|
|
788
|
+
.action(() => {
|
|
789
|
+
console.log(CONFIG_FILE);
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
793
|
+
// logs
|
|
794
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
795
|
+
|
|
796
|
+
const logsCmd = program
|
|
797
|
+
.command("logs")
|
|
798
|
+
.description("View and manage logs");
|
|
799
|
+
|
|
800
|
+
logsCmd
|
|
801
|
+
.command("show")
|
|
802
|
+
.description("Display recent log lines")
|
|
803
|
+
.option("-n, --lines <n>", "Number of lines to show", "50")
|
|
804
|
+
.action((opts) => {
|
|
805
|
+
const lf = logFile();
|
|
806
|
+
if (!fs.existsSync(lf)) {
|
|
807
|
+
console.log(` ${c.dim("No log file found.")}`);
|
|
808
|
+
console.log(c.dim(` (Expected at ${lf})`));
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
const n = parseInt(opts.lines, 10) || 50;
|
|
812
|
+
try {
|
|
813
|
+
const content = execSync(`tail -n ${n} "${lf}"`, {
|
|
814
|
+
encoding: "utf-8",
|
|
815
|
+
});
|
|
816
|
+
process.stdout.write(content);
|
|
817
|
+
} catch {
|
|
818
|
+
// Fallback if tail is not available
|
|
819
|
+
const lines = fs.readFileSync(lf, "utf-8").split("\n");
|
|
820
|
+
console.log(lines.slice(-n).join("\n"));
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
logsCmd
|
|
825
|
+
.command("tail")
|
|
826
|
+
.description("Follow log output in real-time")
|
|
827
|
+
.action(() => {
|
|
828
|
+
const lf = logFile();
|
|
829
|
+
if (!fs.existsSync(lf)) {
|
|
830
|
+
fs.mkdirSync(path.dirname(lf), { recursive: true });
|
|
831
|
+
fs.writeFileSync(lf, "");
|
|
832
|
+
}
|
|
833
|
+
console.log(c.dim(` Tailing ${lf}... (Ctrl+C to stop)\n`));
|
|
834
|
+
const child = spawn("tail", ["-f", lf], { stdio: "inherit" });
|
|
835
|
+
process.on("SIGINT", () => {
|
|
836
|
+
child.kill();
|
|
837
|
+
process.exit(0);
|
|
838
|
+
});
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
logsCmd
|
|
842
|
+
.command("clean")
|
|
843
|
+
.description("Delete log files")
|
|
844
|
+
.option("-f, --force", "Skip confirmation")
|
|
845
|
+
.action(async (opts) => {
|
|
846
|
+
const lf = logFile();
|
|
847
|
+
if (!fs.existsSync(lf)) {
|
|
848
|
+
console.log(` ${c.dim("No log file found.")}`);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
const stat = fs.statSync(lf);
|
|
852
|
+
const sizeKb = Math.round(stat.size / 1024);
|
|
853
|
+
if (!opts.force) {
|
|
854
|
+
const yes = await confirm(
|
|
855
|
+
` ${warn()} Delete log file (${sizeKb} KB)?`,
|
|
856
|
+
);
|
|
857
|
+
if (!yes) {
|
|
858
|
+
console.log(" Aborted.");
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
fs.unlinkSync(lf);
|
|
863
|
+
console.log(` ${ok()} Log file deleted.`);
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
logsCmd
|
|
867
|
+
.command("path")
|
|
868
|
+
.description("Print log file path")
|
|
869
|
+
.action(() => {
|
|
870
|
+
console.log(logFile());
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
874
|
+
// info
|
|
875
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
876
|
+
|
|
877
|
+
program
|
|
878
|
+
.command("info")
|
|
879
|
+
.description("Show version and environment info")
|
|
880
|
+
.action(() => {
|
|
881
|
+
let baileysVer = "?";
|
|
882
|
+
try {
|
|
883
|
+
baileysVer = require(
|
|
884
|
+
path.join(PKG_ROOT, "node_modules/@whiskeysockets/baileys/package.json"),
|
|
885
|
+
).version;
|
|
886
|
+
} catch { /* ignore */ }
|
|
887
|
+
|
|
888
|
+
let mcpSdkVer = "?";
|
|
889
|
+
try {
|
|
890
|
+
mcpSdkVer = require(
|
|
891
|
+
path.join(PKG_ROOT, "node_modules/@modelcontextprotocol/sdk/package.json"),
|
|
892
|
+
).version;
|
|
893
|
+
} catch { /* ignore */ }
|
|
894
|
+
|
|
895
|
+
console.log(c.bold("\n whats-mcp Info\n"));
|
|
896
|
+
console.log(` Version: ${c.cyan(PKG_JSON.version)}`);
|
|
897
|
+
console.log(` Node.js: ${c.cyan(process.version)}`);
|
|
898
|
+
console.log(` Baileys: ${c.cyan(baileysVer)}`);
|
|
899
|
+
console.log(` MCP SDK: ${c.cyan(mcpSdkVer)}`);
|
|
900
|
+
console.log(` Platform: ${c.cyan(os.platform() + " " + os.arch())}`);
|
|
901
|
+
console.log(` State dir: ${c.cyan(stateDir())}`);
|
|
902
|
+
console.log(` Auth dir: ${c.cyan(authDir())}`);
|
|
903
|
+
console.log(` Config: ${c.cyan(CONFIG_FILE)}`);
|
|
904
|
+
console.log(` PID file: ${c.cyan(pidFile())}`);
|
|
905
|
+
console.log(` Log file: ${c.cyan(logFile())}`);
|
|
906
|
+
console.log(` Package: ${c.cyan(PKG_ROOT)}`);
|
|
907
|
+
console.log();
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
911
|
+
// Parse & run
|
|
912
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
913
|
+
|
|
914
|
+
module.exports = {
|
|
915
|
+
program,
|
|
916
|
+
};
|