whatsapp: fix history sync + session corruption, Baileys 7.0.0-rc14 (v7 / 2.1.0)
Diagnosed from the live server (two users, two accounts, one shared log). - history sync was gated off entirely on Baileys 6.7.x: the library derives shouldSyncHistoryMessage from syncFullHistory when unset, and the connector shipped syncFullHistory: false. Now both are passed explicitly, so behaviour no longer depends on the resolved Baileys version. browser -> ['Mac OS', ...] because PLATFORM_MAP only grants a desktop-grade sync to Mac OS / Windows. - reconnects leaked their socket, leaving two writers over auth/: no end(), no removeAllListeners(). ~15 reconnects/day produced 1422 Bad MAC lines, all on the account's own LID device 0. Added teardownSock(), per-socket generation guards, and a single-slot reconnect timer with exponential backoff. - undecryptable messages (stubType CIPHERTEXT) were stored with empty text and rendered as blank transcript lines; now labelled and counted. Text-less protocol frames are dropped. - fetchLatestBaileysVersion() ran on every reconnect; now cached for 6h. - libsignal's direct console.error spam collapsed into one counted line, log lines tagged with the linked account, console.log redirected off stdout. Baileys pinned ^6.7.9 -> 7.0.0-rc14 (the range resolved to 6.7.24 for one user and 6.17.16 for the other). RC is ESM-only, so index.js is now ESM. Verified on skald-runtime:v4 (Node 20.19.2): initialize, tools/list, status, list_chats, version fetch, QR, and logout -> teardown -> single reconnect. Remaining gap: the store is still in-memory only, so synced history is lost on the next process restart.
This commit is contained in:
@@ -16,19 +16,20 @@
|
||||
"qr"
|
||||
],
|
||||
"dependencies": [
|
||||
"@whiskeysockets/baileys>=6.7.9",
|
||||
"qrcode>=1.5.4"
|
||||
"@whiskeysockets/baileys@7.0.0-rc14",
|
||||
"qrcode@1.5.4"
|
||||
],
|
||||
"setup_instructions": [
|
||||
"Skald installs dependencies automatically (npm install).",
|
||||
"Open the connector in Skald and scan the QR code with your WhatsApp phone.",
|
||||
"The session is persisted and survives restarts."
|
||||
"The session is persisted and survives restarts.",
|
||||
"The phone lists this device as \"Mac OS Chrome\": that browser identity is what makes WhatsApp deliver the full history sync."
|
||||
],
|
||||
"docs": [
|
||||
{
|
||||
"lang": "en",
|
||||
"description": "Send and read WhatsApp messages from your own linked account. Uses Baileys multi-device protocol \u2014 no browser, no Chromium needed. Link via QR code.",
|
||||
"llm_short_description": "WhatsApp \u2014 read and send messages, manage chats, search contacts and messages, and download media from the user's linked WhatsApp session. QR-based authentication."
|
||||
"description": "Send and read WhatsApp messages from your own linked account. Uses Baileys multi-device protocol — no browser, no Chromium needed. Link via QR code.",
|
||||
"llm_short_description": "WhatsApp — read and send messages, manage chats, search contacts and messages, and download media from the user's linked WhatsApp session. QR-based authentication."
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
@@ -44,7 +45,7 @@
|
||||
"homepage": "https://github.com/WhiskeySockets/Baileys",
|
||||
"icon_small": "icon_sm.png",
|
||||
"icon_large": "icon_lg.png",
|
||||
"version": 3,
|
||||
"version_string": "2.0.3",
|
||||
"version_release_date": "2026-07-21"
|
||||
}
|
||||
"version": 7,
|
||||
"version_string": "2.1.0",
|
||||
"version_release_date": "2026-08-23"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"scope": "user",
|
||||
"icon_small": "whatsapp/icon_sm.png",
|
||||
"icon_large": "whatsapp/icon_lg.png",
|
||||
"user_description": "Send and read WhatsApp messages from your own linked account. Link via QR code \u2014 no browser needed.",
|
||||
"user_description": "Send and read WhatsApp messages from your own linked account. Link via QR code — no browser needed.",
|
||||
"requires": [
|
||||
"NODE"
|
||||
],
|
||||
@@ -20,7 +20,7 @@
|
||||
"type": "qr"
|
||||
},
|
||||
"folder": "whatsapp",
|
||||
"version": 6,
|
||||
"version_string": "2.0.6",
|
||||
"version_release_date": "2026-07-21"
|
||||
}
|
||||
"version": 7,
|
||||
"version_string": "2.1.0",
|
||||
"version_release_date": "2026-08-23"
|
||||
}
|
||||
|
||||
+219
-60
@@ -1,5 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* WhatsApp MCP Server (JSON-RPC 2.0 over stdio) — Baileys edition.
|
||||
@@ -22,31 +21,26 @@
|
||||
* `auth_state` to `ready`. WhatsApp's credential is the persisted session on disk
|
||||
* (`./auth/`, under the bind-mounted home → survives a container recreate), not a
|
||||
* token — so there is nothing to paste back, only a QR to scan.
|
||||
*
|
||||
* ── Why this file is ESM ────────────────────────────────────────────────────────
|
||||
* Baileys 7.x ships ESM-only (`"type": "module"`, engines node >= 20). Importing it
|
||||
* from CommonJS would rely on Node's `require(esm)` bridge, which is a moving target
|
||||
* across Node versions — so the connector is ESM too, like the other node connectors.
|
||||
*/
|
||||
|
||||
// Baileys uses the Web Crypto global (`crypto.subtle`), which Node only exposes as
|
||||
// `globalThis.crypto` from v20+. The container ships Node 18 (Debian bookworm), so
|
||||
// polyfill it from `node:crypto` — without this, the socket dies on connect with
|
||||
// "crypto is not defined" and never reaches the QR.
|
||||
const nodeCrypto = require('crypto');
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import readline from 'node:readline';
|
||||
import nodeCrypto from 'node:crypto';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import qrcode from 'qrcode';
|
||||
|
||||
// Baileys uses the Web Crypto global (`crypto.subtle`). Node exposes it as
|
||||
// `globalThis.crypto` from v19+, but keep the polyfill so an older runtime still
|
||||
// reaches the QR instead of dying on connect with "crypto is not defined".
|
||||
if (!globalThis.crypto) globalThis.crypto = nodeCrypto.webcrypto;
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const readline = require('readline');
|
||||
const qrcode = require('qrcode');
|
||||
|
||||
let makeWASocket, useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion, jidNormalizedUser;
|
||||
try {
|
||||
const baileys = require('@whiskeysockets/baileys');
|
||||
makeWASocket = baileys.default || baileys.makeWASocket;
|
||||
useMultiFileAuthState = baileys.useMultiFileAuthState;
|
||||
DisconnectReason = baileys.DisconnectReason;
|
||||
fetchLatestBaileysVersion = baileys.fetchLatestBaileysVersion;
|
||||
jidNormalizedUser = baileys.jidNormalizedUser;
|
||||
} catch (e) {
|
||||
process.stderr.write(`[whatsapp_mcp] FATAL: baileys not installed (${e.message}). Run npm install.\n`);
|
||||
}
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// ── Paths ──────────────────────────────────────────────────────────────────
|
||||
// Everything hangs off __dirname (the connector dir inside the container home,
|
||||
@@ -54,7 +48,12 @@ try {
|
||||
const AUTH_DIR = path.join(__dirname, 'auth'); // multi-file auth state (the "session")
|
||||
const MEDIA_DIR = path.join(__dirname, 'media');
|
||||
|
||||
function log(msg) { process.stderr.write(`[whatsapp_mcp] ${msg}\n`); }
|
||||
// ── Logging ────────────────────────────────────────────────────────────────────
|
||||
// Skald funnels every user's whatsapp stderr into ONE shared log file, so each line
|
||||
// carries the linked account once we know it — otherwise two users' failures are
|
||||
// indistinguishable after the fact.
|
||||
let logTag = 'whatsapp_mcp';
|
||||
function log(msg) { process.stderr.write(`[${logTag}] ${msg}\n`); }
|
||||
|
||||
// A silent logger: Baileys requires one, and anything it prints must never reach
|
||||
// stdout (that channel is reserved for JSON-RPC framing).
|
||||
@@ -65,17 +64,67 @@ const silentLogger = (() => {
|
||||
return l;
|
||||
})();
|
||||
|
||||
// `libsignal` bypasses the Baileys logger and writes straight to the console: one
|
||||
// "Failed to decrypt…" line plus a full stack trace *per candidate session* for every
|
||||
// message it cannot open. That turned ~30 real failures into 1400+ log lines. Collapse
|
||||
// the burst into a single counted line, and force any stray console.log to stderr —
|
||||
// stdout belongs to the JSON-RPC framing and a dependency printing there corrupts it.
|
||||
const rawConsoleError = console.error.bind(console);
|
||||
let decryptFailures = 0;
|
||||
console.error = (...args) => {
|
||||
const first = typeof args[0] === 'string' ? args[0] : '';
|
||||
if (first.startsWith('Session error:')) return; // per-session stack spam
|
||||
if (first.startsWith('Failed to decrypt message with any known session')) {
|
||||
decryptFailures++;
|
||||
log(`could not decrypt an incoming message (${decryptFailures} since start)`);
|
||||
return;
|
||||
}
|
||||
rawConsoleError(...args);
|
||||
};
|
||||
console.log = (...args) => console.error(...args);
|
||||
console.info = console.warn = console.error;
|
||||
|
||||
// ── Baileys (loaded dynamically so a missing install degrades gracefully) ───────
|
||||
let makeWASocket, useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion,
|
||||
jidNormalizedUser, WAMessageStubType;
|
||||
|
||||
async function loadBaileys() {
|
||||
try {
|
||||
const baileys = await import('@whiskeysockets/baileys');
|
||||
makeWASocket = baileys.default || baileys.makeWASocket;
|
||||
useMultiFileAuthState = baileys.useMultiFileAuthState;
|
||||
DisconnectReason = baileys.DisconnectReason;
|
||||
fetchLatestBaileysVersion = baileys.fetchLatestBaileysVersion;
|
||||
jidNormalizedUser = baileys.jidNormalizedUser;
|
||||
WAMessageStubType = baileys.WAMessageStubType;
|
||||
return true;
|
||||
} catch (e) {
|
||||
log(`FATAL: baileys not installed (${e.message}). Run npm install.`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Connection state ─────────────────────────────────────────────────────────
|
||||
// connecting – socket starting or reconnecting
|
||||
// need_scan – a QR is available; the user must scan it
|
||||
// ready – authenticated and connected; tools operational
|
||||
// logged_out – the phone unlinked this device; a fresh QR + scan is required
|
||||
let state = 'connecting';
|
||||
let sock = null;
|
||||
let curQr = null; // latest raw QR string (null once scanned / connected)
|
||||
let meJid = null;
|
||||
let state = 'connecting';
|
||||
let sock = null;
|
||||
let curQr = null; // latest raw QR string (null once scanned / connected)
|
||||
let meJid = null;
|
||||
let starting = false;
|
||||
|
||||
// Every socket gets a generation number. Events arriving from a superseded socket are
|
||||
// dropped, so a late `close` from a dead socket can never schedule a second reconnect
|
||||
// or stomp on the live one's state.
|
||||
let generation = 0;
|
||||
let reconnectTimer = null;
|
||||
|
||||
const RECONNECT_BASE_MS = 1500;
|
||||
const RECONNECT_MAX_MS = 60_000;
|
||||
let backoffMs = RECONNECT_BASE_MS;
|
||||
|
||||
// ── Lightweight in-memory store ───────────────────────────────────────────────
|
||||
// Baileys keeps no chat/contact store of its own; we build a minimal one from the
|
||||
// history-sync event and live upserts. It lives for the process lifetime — enough
|
||||
@@ -120,38 +169,104 @@ function textOf(msg) {
|
||||
);
|
||||
}
|
||||
|
||||
// ── WhatsApp protocol version (cached) ─────────────────────────────────────────
|
||||
// `fetchLatestBaileysVersion` is a network call. Doing it on every reconnect meant
|
||||
// ~15 outbound requests a day, each able to hand us a protocol version the installed
|
||||
// library cannot speak. Fetch once, reuse for a few hours, fall back to the last good
|
||||
// value (or the library default) when the fetch fails.
|
||||
const VERSION_TTL_MS = 6 * 60 * 60 * 1000;
|
||||
let cachedVersion = null;
|
||||
let cachedVersionAt = 0;
|
||||
|
||||
async function getWAVersion() {
|
||||
const now = Date.now();
|
||||
if (cachedVersion && now - cachedVersionAt < VERSION_TTL_MS) return cachedVersion;
|
||||
try {
|
||||
const { version } = await fetchLatestBaileysVersion();
|
||||
cachedVersion = version;
|
||||
cachedVersionAt = now;
|
||||
log(`WhatsApp protocol version ${version.join('.')}`);
|
||||
} catch (e) {
|
||||
log(`version fetch failed (${e.message}) — using ${cachedVersion ? 'cached value' : 'library default'}`);
|
||||
}
|
||||
return cachedVersion || undefined;
|
||||
}
|
||||
|
||||
// ── WhatsApp socket lifecycle ──────────────────────────────────────────────────
|
||||
|
||||
// Detach and close a socket for good. Baileys keeps its own keepalive and, more
|
||||
// importantly, a `creds.update → saveCreds` handler bound to the auth-state snapshot
|
||||
// it was built with. Leaving an old socket alive meant two writers over the same
|
||||
// `auth/` directory on every reconnect — the way Signal sessions end up inconsistent
|
||||
// and messages start failing with "Bad MAC".
|
||||
function teardownSock(s) {
|
||||
if (!s) return;
|
||||
try { s.ev.removeAllListeners(); } catch { /* already gone */ }
|
||||
try { s.ws?.removeAllListeners?.(); } catch { /* already gone */ }
|
||||
try { s.end(undefined); } catch { /* already closed */ }
|
||||
}
|
||||
|
||||
function scheduleReconnect(delayMs) {
|
||||
if (reconnectTimer) return; // one pending reconnect at a time
|
||||
reconnectTimer = setTimeout(() => {
|
||||
reconnectTimer = null;
|
||||
startSock().catch((e) => log(`reconnect failed: ${e.message}`));
|
||||
}, delayMs);
|
||||
}
|
||||
|
||||
function disconnectName(code) {
|
||||
const name = DisconnectReason?.[code];
|
||||
return typeof name === 'string' ? name : 'unknown';
|
||||
}
|
||||
|
||||
async function startSock() {
|
||||
if (starting) return;
|
||||
if (starting) { scheduleReconnect(2000); return; }
|
||||
starting = true;
|
||||
const myGen = ++generation;
|
||||
try {
|
||||
if (!makeWASocket) { state = 'connecting'; return; }
|
||||
|
||||
// Retire the previous socket BEFORE building a new one.
|
||||
teardownSock(sock);
|
||||
sock = null;
|
||||
|
||||
fs.mkdirSync(AUTH_DIR, { recursive: true });
|
||||
|
||||
const { state: authState, saveCreds } = await useMultiFileAuthState(AUTH_DIR);
|
||||
let version;
|
||||
try { ({ version } = await fetchLatestBaileysVersion()); } catch (_) { /* baileys default */ }
|
||||
const version = await getWAVersion();
|
||||
|
||||
sock = makeWASocket({
|
||||
const s = makeWASocket({
|
||||
version,
|
||||
auth: authState,
|
||||
logger: silentLogger,
|
||||
browser: ['Skald', 'Chrome', '1.0.0'],
|
||||
syncFullHistory: false,
|
||||
// The browser identity is not cosmetic: `getWebInfo` only asks the phone for a
|
||||
// desktop-grade history sync when browser[0] is 'Mac OS' or 'Windows'. With any
|
||||
// other name the sub-platform stays WEB_BROWSER and `syncFullHistory` is inert.
|
||||
// Trade-off: the phone lists this device as "Mac OS Chrome", not "Skald".
|
||||
browser: ['Mac OS', 'Chrome', '121.0.0'],
|
||||
syncFullHistory: true,
|
||||
// Set explicitly rather than left to the default. Baileys 6.7.x derives this from
|
||||
// `syncFullHistory` and would otherwise gate history processing off entirely;
|
||||
// 6.17.x and 7.x default it to true. Pinning it here makes the behaviour the same
|
||||
// whichever version is installed.
|
||||
shouldSyncHistoryMessage: () => true,
|
||||
markOnlineOnConnect: false,
|
||||
generateHighQualityLinkPreview: false,
|
||||
});
|
||||
sock = s;
|
||||
|
||||
sock.ev.on('creds.update', saveCreds);
|
||||
s.ev.on('creds.update', saveCreds);
|
||||
|
||||
sock.ev.on('connection.update', (u) => {
|
||||
s.ev.on('connection.update', (u) => {
|
||||
if (myGen !== generation) return; // event from a superseded socket
|
||||
const { connection, lastDisconnect, qr } = u;
|
||||
if (qr) { curQr = qr; state = 'need_scan'; log('QR ready — awaiting scan'); }
|
||||
if (connection === 'open') {
|
||||
curQr = null;
|
||||
state = 'ready';
|
||||
meJid = sock?.user?.id ? jidNormalizedUser(sock.user.id) : null;
|
||||
backoffMs = RECONNECT_BASE_MS;
|
||||
meJid = s.user?.id ? jidNormalizedUser(s.user.id) : null;
|
||||
const who = s.user?.name || (meJid ? meJid.split('@')[0] : null);
|
||||
if (who) logTag = `whatsapp_mcp ${who}`;
|
||||
log('connection open — ready');
|
||||
}
|
||||
if (connection === 'close') {
|
||||
@@ -160,21 +275,26 @@ async function startSock() {
|
||||
state = 'logged_out';
|
||||
curQr = null;
|
||||
log('logged out by phone — clearing session');
|
||||
try { fs.rmSync(AUTH_DIR, { recursive: true, force: true }); } catch (_) {}
|
||||
// Re-init so a fresh QR is produced immediately.
|
||||
teardownSock(s);
|
||||
sock = null;
|
||||
try { fs.rmSync(AUTH_DIR, { recursive: true, force: true }); } catch { /* nothing to clear */ }
|
||||
starting = false;
|
||||
setTimeout(() => startSock(), 500);
|
||||
scheduleReconnect(500); // produce a fresh QR immediately
|
||||
} else {
|
||||
state = 'connecting';
|
||||
log(`connection closed (code ${code ?? '?'}) — reconnecting`);
|
||||
log(`connection closed (${code ?? '?'} ${disconnectName(code)}) — reconnecting in ${Math.round(backoffMs / 1000)}s`);
|
||||
teardownSock(s);
|
||||
sock = null;
|
||||
starting = false;
|
||||
setTimeout(() => startSock(), 1500);
|
||||
scheduleReconnect(backoffMs);
|
||||
backoffMs = Math.min(backoffMs * 2, RECONNECT_MAX_MS);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Initial history sync: chats, contacts and a batch of messages.
|
||||
sock.ev.on('messaging-history.set', ({ chats: hc, contacts: hcs, messages: hm }) => {
|
||||
s.ev.on('messaging-history.set', ({ chats: hc, contacts: hcs, messages: hm, syncType, progress }) => {
|
||||
if (myGen !== generation) return;
|
||||
for (const c of hc || []) {
|
||||
chats.set(c.id, {
|
||||
id: c.id,
|
||||
@@ -187,31 +307,40 @@ async function startSock() {
|
||||
contacts.set(c.id, { id: c.id, name: c.name || c.notify || c.verifiedName || null });
|
||||
}
|
||||
for (const m of hm || []) ingestMessage(m, false);
|
||||
log(`history sync (type ${syncType ?? '?'}${progress != null ? `, ${progress}%` : ''}): ` +
|
||||
`+${(hc || []).length} chats, +${(hcs || []).length} contacts, +${(hm || []).length} messages ` +
|
||||
`→ ${chats.size} chats / ${contacts.size} contacts known`);
|
||||
});
|
||||
|
||||
sock.ev.on('chats.upsert', (cs) => {
|
||||
s.ev.on('chats.upsert', (cs) => {
|
||||
if (myGen !== generation) return;
|
||||
for (const c of cs) chats.set(c.id, {
|
||||
id: c.id, name: c.name || c.subject || null,
|
||||
unread: c.unreadCount || 0,
|
||||
conversationTimestamp: Number(c.conversationTimestamp) || 0,
|
||||
});
|
||||
});
|
||||
sock.ev.on('contacts.upsert', (cs) => {
|
||||
s.ev.on('contacts.upsert', (cs) => {
|
||||
if (myGen !== generation) return;
|
||||
for (const c of cs) contacts.set(c.id, { id: c.id, name: c.name || c.notify || c.verifiedName || null });
|
||||
});
|
||||
sock.ev.on('contacts.update', (cs) => {
|
||||
s.ev.on('contacts.update', (cs) => {
|
||||
if (myGen !== generation) return;
|
||||
for (const c of cs) {
|
||||
const prev = contacts.get(c.id) || { id: c.id };
|
||||
contacts.set(c.id, { ...prev, name: c.name || c.notify || prev.name || null });
|
||||
}
|
||||
});
|
||||
|
||||
sock.ev.on('messages.upsert', ({ messages: ms, type }) => {
|
||||
s.ev.on('messages.upsert', ({ messages: ms, type }) => {
|
||||
if (myGen !== generation) return;
|
||||
for (const m of ms) ingestMessage(m, type === 'notify');
|
||||
});
|
||||
} catch (e) {
|
||||
log(`startSock error: ${e.message}`);
|
||||
state = 'connecting';
|
||||
scheduleReconnect(backoffMs);
|
||||
backoffMs = Math.min(backoffMs * 2, RECONNECT_MAX_MS);
|
||||
} finally {
|
||||
starting = false;
|
||||
}
|
||||
@@ -221,7 +350,19 @@ function ingestMessage(m, live) {
|
||||
try {
|
||||
const jid = m.key?.remoteJid;
|
||||
if (!jid || jid === 'status@broadcast') return;
|
||||
const text = textOf(m);
|
||||
|
||||
// A message Signal could not open arrives with no `message` payload and the
|
||||
// CIPHERTEXT stub type. It used to be stored with an empty text, so `get_messages`
|
||||
// rendered it as a blank line and the agent had no way to tell a silent gap from a
|
||||
// genuinely empty message. Keep it, but say what it is.
|
||||
const undecryptable = WAMessageStubType != null &&
|
||||
m.messageStubType === WAMessageStubType.CIPHERTEXT;
|
||||
const text = undecryptable ? '[undecryptable message]' : textOf(m);
|
||||
|
||||
// Protocol/system frames (reactions, receipts, key distribution…) carry no text
|
||||
// and are pure noise in a transcript.
|
||||
if (!text) return;
|
||||
|
||||
pushMessage(jid, {
|
||||
id: m.key?.id,
|
||||
fromMe: !!m.key?.fromMe,
|
||||
@@ -236,7 +377,7 @@ function ingestMessage(m, live) {
|
||||
ch.conversationTimestamp = Number(m.messageTimestamp) || ch.conversationTimestamp;
|
||||
if (m.pushName && !ch.name) ch.name = m.pushName;
|
||||
}
|
||||
} catch (_) {}
|
||||
} catch { /* one malformed frame must not stop the stream */ }
|
||||
}
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────────
|
||||
@@ -263,7 +404,7 @@ function requireReady() {
|
||||
async function toolLoginStatus() {
|
||||
let qrDataUrl = null;
|
||||
if (state === 'need_scan' && curQr) {
|
||||
try { qrDataUrl = await qrcode.toDataURL(curQr, { width: 320, margin: 2 }); } catch (_) {}
|
||||
try { qrDataUrl = await qrcode.toDataURL(curQr, { width: 320, margin: 2 }); } catch { /* no QR to render */ }
|
||||
}
|
||||
const message = {
|
||||
connecting: 'Connecting to WhatsApp…',
|
||||
@@ -278,17 +419,25 @@ async function toolLoginStatus() {
|
||||
async function toolStatus() {
|
||||
const s = await toolLoginStatus();
|
||||
const { state: st, message } = JSON.parse(s);
|
||||
const chatCount = chats.size;
|
||||
return `WhatsApp status: ${st.toUpperCase()}\n${message}` +
|
||||
(st === 'ready' ? `\nKnown chats: ${chatCount}` : '');
|
||||
(st === 'ready'
|
||||
? `\nKnown chats: ${chats.size}` +
|
||||
`\nKnown contacts: ${contacts.size}` +
|
||||
(decryptFailures ? `\nUndecryptable messages since start: ${decryptFailures}` : '')
|
||||
: '');
|
||||
}
|
||||
|
||||
async function toolLogout() {
|
||||
try { if (sock) await sock.logout(); } catch (_) {}
|
||||
try { fs.rmSync(AUTH_DIR, { recursive: true, force: true }); } catch (_) {}
|
||||
try { if (sock) await sock.logout(); } catch { /* already gone */ }
|
||||
generation++; // orphan any in-flight socket events
|
||||
teardownSock(sock);
|
||||
sock = null;
|
||||
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
||||
try { fs.rmSync(AUTH_DIR, { recursive: true, force: true }); } catch { /* nothing to clear */ }
|
||||
chats.clear(); contacts.clear(); messages.clear();
|
||||
curQr = null; state = 'connecting'; starting = false; meJid = null;
|
||||
setTimeout(() => startSock(), 500);
|
||||
backoffMs = RECONNECT_BASE_MS;
|
||||
scheduleReconnect(500);
|
||||
return 'Logged out and cleared the session. A new QR code will be generated — open the connector in Skald and scan it.';
|
||||
}
|
||||
|
||||
@@ -447,7 +596,7 @@ async function handleRequest(msg) {
|
||||
return okResponse(id, {
|
||||
protocolVersion: '2024-11-05',
|
||||
capabilities: { tools: {} },
|
||||
serverInfo: { name: 'whatsapp', version: '2.0.0' },
|
||||
serverInfo: { name: 'whatsapp', version: '2.1.0' },
|
||||
});
|
||||
}
|
||||
if (method === 'notifications/initialized') return null;
|
||||
@@ -485,7 +634,10 @@ async function handleRequest(msg) {
|
||||
async function main() {
|
||||
log('Starting WhatsApp MCP server (Baileys)');
|
||||
fs.mkdirSync(MEDIA_DIR, { recursive: true });
|
||||
startSock().catch((e) => log(`initial startSock failed: ${e.message}`));
|
||||
|
||||
if (await loadBaileys()) {
|
||||
startSock().catch((e) => log(`initial startSock failed: ${e.message}`));
|
||||
}
|
||||
|
||||
const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
|
||||
rl.on('line', async (line) => {
|
||||
@@ -496,10 +648,17 @@ async function main() {
|
||||
const resp = await handleRequest(msg);
|
||||
if (resp !== null) process.stdout.write(resp + '\n');
|
||||
});
|
||||
rl.on('close', () => { log('stdin closed, shutting down'); process.exit(0); });
|
||||
rl.on('close', () => { log('stdin closed, shutting down'); shutdown(0); });
|
||||
|
||||
process.on('SIGTERM', () => { log('SIGTERM'); process.exit(0); });
|
||||
process.on('SIGINT', () => { log('SIGINT'); process.exit(0); });
|
||||
process.on('SIGTERM', () => { log('SIGTERM'); shutdown(0); });
|
||||
process.on('SIGINT', () => { log('SIGINT'); shutdown(0); });
|
||||
}
|
||||
|
||||
function shutdown(code) {
|
||||
generation++;
|
||||
teardownSock(sock);
|
||||
sock = null;
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
main().catch((e) => { log(`Fatal: ${e.message}`); process.exit(1); });
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
{
|
||||
"name": "skald-whatsapp-mcp",
|
||||
"version": "2.0.0",
|
||||
"version": "2.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "WhatsApp MCP connector for Skald (Baileys, no browser).",
|
||||
"main": "index.js",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=20"
|
||||
},
|
||||
"dependencies": {
|
||||
"@whiskeysockets/baileys": "^6.7.9",
|
||||
"qrcode": "^1.5.4"
|
||||
"@whiskeysockets/baileys": "7.0.0-rc14",
|
||||
"qrcode": "1.5.4"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user