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.
@@ -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
+ };