whatsapp-web-cli 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/dist/wwa.js +329 -19
- package/package.json +2 -1
- package/skills/whatsapp-cli/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -74,6 +74,28 @@ wwa chat-search "Name" --json
|
|
|
74
74
|
There is no required `doctor` or `ready` step in normal usage. Data commands
|
|
75
75
|
auto-start the daemon and wait for readiness after auth is scanned.
|
|
76
76
|
|
|
77
|
+
If the first run fails before a QR appears because Puppeteer cannot find or
|
|
78
|
+
launch Chrome-for-Testing, run the browser recovery command recommended by the
|
|
79
|
+
CLI:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
wwa recover browser --json
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
On macOS, if that reports a launch failure for the downloaded
|
|
86
|
+
Chrome-for-Testing app, explicitly allow the CLI to clear quarantine metadata
|
|
87
|
+
from that isolated test browser:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
wwa recover browser --clear-quarantine --json
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Then retry:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
wwa auth login --image --json
|
|
97
|
+
```
|
|
98
|
+
|
|
77
99
|
## Common Flow
|
|
78
100
|
|
|
79
101
|
```bash
|
|
@@ -95,6 +117,8 @@ wwa auth logout --json
|
|
|
95
117
|
|
|
96
118
|
wwa ready --wait 120 --json
|
|
97
119
|
wwa doctor --json
|
|
120
|
+
wwa doctor --browser --json
|
|
121
|
+
wwa recover browser --json
|
|
98
122
|
|
|
99
123
|
wwa chat-search "name or text" --json
|
|
100
124
|
wwa message-search "message text" --limit-chats 20 --messages-per-chat 50 --json
|
package/dist/wwa.js
CHANGED
|
@@ -326995,6 +326995,7 @@ import { copyFile as copyFile2, mkdir as mkdir5 } from "node:fs/promises";
|
|
|
326995
326995
|
import { existsSync } from "node:fs";
|
|
326996
326996
|
import { homedir as homedir2 } from "node:os";
|
|
326997
326997
|
import { dirname as dirname7, join as join2, resolve as resolve9 } from "node:path";
|
|
326998
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
326998
326999
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
326999
327000
|
|
|
327000
327001
|
// src/daemon.ts
|
|
@@ -341287,7 +341288,6 @@ import { readFile as readFile2, writeFile as writeFile3 } from "node:fs/promises
|
|
|
341287
341288
|
import { basename as basename2, resolve as resolve5 } from "node:path";
|
|
341288
341289
|
import { promisify } from "node:util";
|
|
341289
341290
|
var { Client, LocalAuth, MessageMedia } = import_whatsapp_web.default;
|
|
341290
|
-
var HEADLESS = process.env.WWA_HEADLESS !== "false";
|
|
341291
341291
|
var execFileAsync = promisify(execFile);
|
|
341292
341292
|
|
|
341293
341293
|
class WhatsAppClient {
|
|
@@ -341306,7 +341306,7 @@ class WhatsAppClient {
|
|
|
341306
341306
|
authTimeoutMs: 120000,
|
|
341307
341307
|
...process.env.WWA_USER_AGENT ? { userAgent: process.env.WWA_USER_AGENT } : {},
|
|
341308
341308
|
puppeteer: {
|
|
341309
|
-
headless:
|
|
341309
|
+
headless: true,
|
|
341310
341310
|
args: ["--no-sandbox", "--disable-setuid-sandbox"]
|
|
341311
341311
|
}
|
|
341312
341312
|
});
|
|
@@ -341421,6 +341421,35 @@ class WhatsAppClient {
|
|
|
341421
341421
|
const messages = chat.messages;
|
|
341422
341422
|
return messages.map((message) => this.serializeMessage(message, chatId));
|
|
341423
341423
|
}
|
|
341424
|
+
async listUnreadMessages() {
|
|
341425
|
+
await this.assertReady();
|
|
341426
|
+
const chats = (await this.client.getChats()).filter((chat) => chat.unreadCount > 0);
|
|
341427
|
+
const results2 = [];
|
|
341428
|
+
for (const chat of chats) {
|
|
341429
|
+
const serializedChat = this.serializeChat(chat);
|
|
341430
|
+
try {
|
|
341431
|
+
const messages = await this.getUnreadMessagesFromPage(chat.id._serialized, chat.unreadCount);
|
|
341432
|
+
results2.push({
|
|
341433
|
+
chat: serializedChat,
|
|
341434
|
+
messages,
|
|
341435
|
+
unreadCount: chat.unreadCount,
|
|
341436
|
+
returnedCount: messages.length,
|
|
341437
|
+
complete: messages.length >= chat.unreadCount
|
|
341438
|
+
});
|
|
341439
|
+
} catch (error51) {
|
|
341440
|
+
const fallback = chat.lastMessage && !chat.lastMessage.fromMe ? [this.serializeMessage(chat.lastMessage, chat.id._serialized)] : [];
|
|
341441
|
+
results2.push({
|
|
341442
|
+
chat: serializedChat,
|
|
341443
|
+
messages: fallback,
|
|
341444
|
+
unreadCount: chat.unreadCount,
|
|
341445
|
+
returnedCount: fallback.length,
|
|
341446
|
+
complete: fallback.length >= chat.unreadCount,
|
|
341447
|
+
error: error51 instanceof Error ? error51.message : String(error51)
|
|
341448
|
+
});
|
|
341449
|
+
}
|
|
341450
|
+
}
|
|
341451
|
+
return results2;
|
|
341452
|
+
}
|
|
341424
341453
|
async getMessage(messageId) {
|
|
341425
341454
|
await this.assertReady();
|
|
341426
341455
|
const message = await this.client.getMessageById(messageId);
|
|
@@ -341570,6 +341599,70 @@ class WhatsAppClient {
|
|
|
341570
341599
|
throw firstError;
|
|
341571
341600
|
}
|
|
341572
341601
|
}
|
|
341602
|
+
async getUnreadMessagesFromPage(chatId, unreadCount) {
|
|
341603
|
+
const client = this.client;
|
|
341604
|
+
return client.pupPage.evaluate(async (rawChatId, rawUnreadCount) => {
|
|
341605
|
+
const chatId2 = String(rawChatId);
|
|
341606
|
+
const unreadCount2 = Number(rawUnreadCount);
|
|
341607
|
+
const toSerialized = (value) => {
|
|
341608
|
+
if (!value)
|
|
341609
|
+
return null;
|
|
341610
|
+
if (typeof value === "string")
|
|
341611
|
+
return value;
|
|
341612
|
+
if (typeof value === "object" && "_serialized" in value && typeof value._serialized === "string")
|
|
341613
|
+
return value._serialized;
|
|
341614
|
+
return null;
|
|
341615
|
+
};
|
|
341616
|
+
const messageId = (message) => {
|
|
341617
|
+
if (message.id?._serialized)
|
|
341618
|
+
return message.id._serialized;
|
|
341619
|
+
const remote = toSerialized(message.id?.remote);
|
|
341620
|
+
return `${message.id?.fromMe ? "true" : "false"}_${remote ?? chatId2}_${message.id?.id ?? ""}`;
|
|
341621
|
+
};
|
|
341622
|
+
const serializeMessage = (message) => {
|
|
341623
|
+
const id = message.id;
|
|
341624
|
+
const hasMedia = Boolean(message.directPath);
|
|
341625
|
+
return {
|
|
341626
|
+
id: messageId({ id }),
|
|
341627
|
+
from: toSerialized(message.from) ?? "",
|
|
341628
|
+
to: toSerialized(message.to) ?? "",
|
|
341629
|
+
author: toSerialized(message.author),
|
|
341630
|
+
body: String(hasMedia ? message.caption ?? "" : message.body ?? message.pollName ?? message.eventName ?? ""),
|
|
341631
|
+
timestamp: Number(message.t ?? 0),
|
|
341632
|
+
type: String(message.type ?? "unknown"),
|
|
341633
|
+
fromMe: Boolean(id?.fromMe),
|
|
341634
|
+
hasMedia,
|
|
341635
|
+
duration: message.duration ?? null,
|
|
341636
|
+
chatId: chatId2
|
|
341637
|
+
};
|
|
341638
|
+
};
|
|
341639
|
+
const isIncomingUserMessage = (message) => {
|
|
341640
|
+
return !message.isNotification && !message.id?.fromMe;
|
|
341641
|
+
};
|
|
341642
|
+
const readModels = (chat2) => {
|
|
341643
|
+
return chat2.msgs?.getModelsArray?.() ?? [];
|
|
341644
|
+
};
|
|
341645
|
+
const store = window.Store;
|
|
341646
|
+
const chatWid = store.WidFactory.createWid(chatId2);
|
|
341647
|
+
const chat = store.Chat.get(chatWid) || store.Chat.get(chatId2) || await store.Chat.find(chatWid) || await store.Chat.find(chatId2);
|
|
341648
|
+
if (!chat?.msgs)
|
|
341649
|
+
throw new Error(`Chat messages are not available for ${chatId2}`);
|
|
341650
|
+
let messages = readModels(chat).filter(isIncomingUserMessage);
|
|
341651
|
+
let previousLength = -1;
|
|
341652
|
+
while (messages.length < unreadCount2 && messages.length !== previousLength) {
|
|
341653
|
+
previousLength = messages.length;
|
|
341654
|
+
const loader = store.ConversationMsgs;
|
|
341655
|
+
const loadEarlier = loader?.loadEarlierMsgs || loader?.loadEarlierMsgsFromChat || loader?.loadEarlierMessages || loader?.loadEarlier;
|
|
341656
|
+
if (typeof loadEarlier !== "function")
|
|
341657
|
+
break;
|
|
341658
|
+
const loaded = await loadEarlier(chat, chat.msgs);
|
|
341659
|
+
if (!loaded?.length)
|
|
341660
|
+
break;
|
|
341661
|
+
messages = readModels(chat).filter(isIncomingUserMessage);
|
|
341662
|
+
}
|
|
341663
|
+
return messages.sort((a, b) => Number(a.t ?? 0) - Number(b.t ?? 0)).slice(-unreadCount2).map(serializeMessage);
|
|
341664
|
+
}, chatId, unreadCount);
|
|
341665
|
+
}
|
|
341573
341666
|
async findChat(chatId) {
|
|
341574
341667
|
try {
|
|
341575
341668
|
return await this.client.getChatById(chatId);
|
|
@@ -341636,12 +341729,17 @@ function recommendationFor(phase, lastError) {
|
|
|
341636
341729
|
return "Authenticate with `wwa auth login --image --json` and render the returned imagePath.";
|
|
341637
341730
|
if (phase === "authenticated_loading")
|
|
341638
341731
|
return "Wait with `wwa ready --wait 120 --json` before reading chats or messages.";
|
|
341732
|
+
if (phase === "failed" && isBrowserLaunchError(lastError))
|
|
341733
|
+
return "Browser automation failed. Run `wwa recover browser --json`, then retry.";
|
|
341639
341734
|
if (phase === "failed" && lastError?.includes("already running"))
|
|
341640
341735
|
return "Browser profile lock detected. Run `wwa daemon stop --json`, then retry.";
|
|
341641
341736
|
if (phase === "failed")
|
|
341642
341737
|
return "Inspect `wwa doctor --json`, then restart with `wwa daemon stop --json && wwa daemon start --json`.";
|
|
341643
341738
|
return "Starting. Wait with `wwa ready --wait 120 --json`.";
|
|
341644
341739
|
}
|
|
341740
|
+
function isBrowserLaunchError(lastError) {
|
|
341741
|
+
return Boolean(lastError && /Could not find Chrome|Browser was not found|Failed to launch|chrome-headless-shell|Chrome for Testing|executablePath|puppeteer/i.test(lastError));
|
|
341742
|
+
}
|
|
341645
341743
|
async function mediaFromFile(filePath, options) {
|
|
341646
341744
|
const resolved = options.asVoice ? await ensureVoiceNoteAudio(filePath) : resolve5(filePath);
|
|
341647
341745
|
const data = await readFile2(resolved);
|
|
@@ -341798,6 +341896,9 @@ async function runDaemon() {
|
|
|
341798
341896
|
const limit = Number(url2.searchParams.get("limit") ?? 50);
|
|
341799
341897
|
return json2(await client.listMessages(chatId, Number.isFinite(limit) ? limit : 50));
|
|
341800
341898
|
}
|
|
341899
|
+
if (request.method === "GET" && url2.pathname === "/unread/messages") {
|
|
341900
|
+
return json2(await client.listUnreadMessages());
|
|
341901
|
+
}
|
|
341801
341902
|
if (request.method === "GET" && url2.pathname === "/message-search") {
|
|
341802
341903
|
const query = url2.searchParams.get("query");
|
|
341803
341904
|
if (!query)
|
|
@@ -342084,7 +342185,7 @@ async function startDaemon() {
|
|
|
342084
342185
|
const child = spawn(process.execPath, [...process.execArgv, process.argv[1] ?? "./bin/wwa.ts", "__daemon"], {
|
|
342085
342186
|
detached: true,
|
|
342086
342187
|
stdio: ["ignore", logFd, logFd],
|
|
342087
|
-
env:
|
|
342188
|
+
env: daemonEnv()
|
|
342088
342189
|
});
|
|
342089
342190
|
child.unref();
|
|
342090
342191
|
const deadline = Date.now() + START_TIMEOUT_MS;
|
|
@@ -342094,7 +342195,9 @@ async function startDaemon() {
|
|
|
342094
342195
|
return status;
|
|
342095
342196
|
await sleep(250);
|
|
342096
342197
|
}
|
|
342097
|
-
|
|
342198
|
+
const logTail = await readLogTail();
|
|
342199
|
+
const recovery = isBrowserLaunchError2(logTail) ? " Run `wwa recover browser --json`, then retry." : "";
|
|
342200
|
+
throw new Error(`Timed out starting daemon. See ${paths.logs}.${recovery}`);
|
|
342098
342201
|
});
|
|
342099
342202
|
}
|
|
342100
342203
|
async function stopDaemon() {
|
|
@@ -342225,6 +342328,26 @@ async function removeStartLock() {
|
|
|
342225
342328
|
await unlink2(paths.daemonLock);
|
|
342226
342329
|
} catch {}
|
|
342227
342330
|
}
|
|
342331
|
+
function daemonEnv() {
|
|
342332
|
+
const env2 = { ...process.env };
|
|
342333
|
+
if (env2.WWA_ALLOW_SYSTEM_CHROME !== "true") {
|
|
342334
|
+
delete env2.PUPPETEER_EXECUTABLE_PATH;
|
|
342335
|
+
delete env2.PUPPETEER_BROWSER_REVISION;
|
|
342336
|
+
}
|
|
342337
|
+
delete env2.WWA_HEADLESS;
|
|
342338
|
+
return env2;
|
|
342339
|
+
}
|
|
342340
|
+
async function readLogTail() {
|
|
342341
|
+
try {
|
|
342342
|
+
const raw = await readFile4(paths.logs, "utf8");
|
|
342343
|
+
return raw.slice(-8000);
|
|
342344
|
+
} catch {
|
|
342345
|
+
return "";
|
|
342346
|
+
}
|
|
342347
|
+
}
|
|
342348
|
+
function isBrowserLaunchError2(text) {
|
|
342349
|
+
return /Could not find Chrome|Browser was not found|Failed to launch|chrome-headless-shell|Chrome for Testing|executablePath|puppeteer/i.test(text);
|
|
342350
|
+
}
|
|
342228
342351
|
function listRuntimeProcesses() {
|
|
342229
342352
|
const output = spawnSync("ps", ["axo", "pid=,pgid=,command="], {
|
|
342230
342353
|
encoding: "utf8"
|
|
@@ -342488,13 +342611,14 @@ async function generateSpeech(options) {
|
|
|
342488
342611
|
}
|
|
342489
342612
|
|
|
342490
342613
|
// src/cli.ts
|
|
342614
|
+
var requireFromHere = createRequire2(import.meta.url);
|
|
342491
342615
|
async function main(argv) {
|
|
342492
342616
|
if (argv[2] === "__daemon") {
|
|
342493
342617
|
await runDaemon();
|
|
342494
342618
|
return;
|
|
342495
342619
|
}
|
|
342496
342620
|
const program2 = new Command;
|
|
342497
|
-
program2.name("wwa").description("Local WhatsApp Web CLI for agents and scripts.").version("0.
|
|
342621
|
+
program2.name("wwa").description("Local WhatsApp Web CLI for agents and scripts.").version("0.2.2");
|
|
342498
342622
|
const daemon = program2.command("daemon").description("Manage the local WhatsApp daemon.");
|
|
342499
342623
|
daemon.command("start").description("Start the local daemon.").option("--json", "Print JSON output.").action(async (options) => {
|
|
342500
342624
|
const status = await startDaemon();
|
|
@@ -342791,20 +342915,13 @@ async function main(argv) {
|
|
|
342791
342915
|
const result = filterChats(chats2, options).map(summarizeChat);
|
|
342792
342916
|
output(options, result, result);
|
|
342793
342917
|
});
|
|
342794
|
-
unread.command("messages").description("List
|
|
342918
|
+
unread.command("messages").description("List unread messages grouped by unread chat.").option("--limit-chats <count>", "Maximum unread chats to inspect.", "10").option("--groups", "Only inspect group chats.").option("--non-groups", "Only inspect direct chats.").option("--include-status", "Include status broadcast chats if present.").option("--json", "Print JSON output.").action(async (rawOptions, command2) => {
|
|
342919
|
+
const options = mergeCommandOptions(rawOptions, command2);
|
|
342795
342920
|
if (!await ensureReady(options))
|
|
342796
342921
|
return;
|
|
342797
|
-
const
|
|
342798
|
-
const selected =
|
|
342799
|
-
const
|
|
342800
|
-
const result = [];
|
|
342801
|
-
for (const chat of selected) {
|
|
342802
|
-
const messages2 = await daemonRequestExisting(`/messages?chatId=${encodeURIComponent(chat.id)}&limit=${messagesPerChat}`);
|
|
342803
|
-
result.push({
|
|
342804
|
-
chat: summarizeChat(chat),
|
|
342805
|
-
messages: messages2.map(summarizeMessage)
|
|
342806
|
-
});
|
|
342807
|
-
}
|
|
342922
|
+
const unreadResults = await daemonRequestExisting("/unread/messages");
|
|
342923
|
+
const selected = filterUnreadMessageResults(unreadResults, { ...options, limit: options.limitChats });
|
|
342924
|
+
const result = selected.map(summarizeUnreadMessagesResult);
|
|
342808
342925
|
output(options, result, result);
|
|
342809
342926
|
});
|
|
342810
342927
|
program2.command("ready").description("Wait until WhatsApp is connected and ready for chat/message commands.").option("--wait <seconds>", "Maximum seconds to wait.", "120").option("--json", "Print JSON output.").action(async (options) => {
|
|
@@ -342816,13 +342933,14 @@ async function main(argv) {
|
|
|
342816
342933
|
if (!result.ready)
|
|
342817
342934
|
process.exitCode = 1;
|
|
342818
342935
|
});
|
|
342819
|
-
program2.command("doctor").description("Diagnose daemon/auth readiness and recommend the next command.").option("--json", "Print JSON output.").action(async (options) => {
|
|
342936
|
+
program2.command("doctor").description("Diagnose daemon/auth readiness and recommend the next command.").option("--browser", "Also check the Puppeteer Chrome-for-Testing setup.").option("--json", "Print JSON output.").action(async (options) => {
|
|
342820
342937
|
const status = await daemonStatus();
|
|
342821
342938
|
const browserLocks = listBrowserLocks();
|
|
342822
342939
|
const result = {
|
|
342823
342940
|
ok: Boolean(status?.ready),
|
|
342824
342941
|
daemon: status ?? { running: false },
|
|
342825
342942
|
browserLocks,
|
|
342943
|
+
...options.browser ? { browser: await browserDiagnostics(false) } : {},
|
|
342826
342944
|
paths,
|
|
342827
342945
|
nextCommand: nextCommand(status, browserLocks.length)
|
|
342828
342946
|
};
|
|
@@ -342831,6 +342949,22 @@ async function main(argv) {
|
|
|
342831
342949
|
program2.command("paths").description("Show local runtime paths.").option("--json", "Print JSON output.").action((options) => {
|
|
342832
342950
|
output(options, paths, paths);
|
|
342833
342951
|
});
|
|
342952
|
+
const recover = program2.command("recover").description("Run targeted recovery steps after a wwa failure.");
|
|
342953
|
+
recover.command("browser").description("Recover Puppeteer's Chrome-for-Testing install after a browser launch failure.").option("--clear-quarantine", "On macOS, clear quarantine metadata from the downloaded test browser before the smoke test.").option("--json", "Print JSON output.").action(async (options) => {
|
|
342954
|
+
const install = installPuppeteerBrowser();
|
|
342955
|
+
const quarantine = options.clearQuarantine ? clearBrowserQuarantine() : null;
|
|
342956
|
+
const diagnostics = await browserDiagnostics(true);
|
|
342957
|
+
const result = {
|
|
342958
|
+
ok: install.ok && diagnostics.launch.ok,
|
|
342959
|
+
install,
|
|
342960
|
+
quarantine,
|
|
342961
|
+
browser: diagnostics,
|
|
342962
|
+
nextCommand: diagnostics.launch.ok ? "wwa auth login --image --json" : browserSetupNextCommand(diagnostics)
|
|
342963
|
+
};
|
|
342964
|
+
output(options, result, result);
|
|
342965
|
+
if (!result.ok)
|
|
342966
|
+
process.exitCode = 1;
|
|
342967
|
+
});
|
|
342834
342968
|
const skill = program2.command("skill").description("Install helper files for agent integrations.");
|
|
342835
342969
|
skill.command("install").description("Install the bundled Codex skill into a local Codex skills directory.").option("--dir <path>", "Codex skills directory.", join2(homedir2(), ".codex", "skills")).option("--name <name>", "Installed skill directory name.", "whatsapp-cli").option("--json", "Print JSON output.").action(async (options) => {
|
|
342836
342970
|
const result = await installCodexSkill(options);
|
|
@@ -342978,7 +343112,27 @@ function mediaCommandBody(options) {
|
|
|
342978
343112
|
}
|
|
342979
343113
|
function filterChats(chats, options) {
|
|
342980
343114
|
const limit = options.limit ? parseNumber(options.limit, chats.length) : chats.length;
|
|
342981
|
-
|
|
343115
|
+
const groups = booleanOption(options, "groups");
|
|
343116
|
+
const nonGroups = booleanOption(options, "nonGroups");
|
|
343117
|
+
const includeStatus = booleanOption(options, "includeStatus");
|
|
343118
|
+
return chats.filter((chat) => includeStatus ? true : chat.id !== "status@broadcast").filter((chat) => groups ? chat.isGroup : true).filter((chat) => nonGroups ? !chat.isGroup : true).slice(0, limit);
|
|
343119
|
+
}
|
|
343120
|
+
function booleanOption(options, key2) {
|
|
343121
|
+
return Boolean(options[key2] ?? options[key2.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)]);
|
|
343122
|
+
}
|
|
343123
|
+
function mergeCommandOptions(options, command2) {
|
|
343124
|
+
return {
|
|
343125
|
+
...options.parent?.opts?.() ?? {},
|
|
343126
|
+
...command2?.parent?.opts?.() ?? {},
|
|
343127
|
+
...options.opts?.() ?? {},
|
|
343128
|
+
...command2?.opts?.() ?? {},
|
|
343129
|
+
...options
|
|
343130
|
+
};
|
|
343131
|
+
}
|
|
343132
|
+
function filterUnreadMessageResults(results2, options) {
|
|
343133
|
+
const chats = filterChats(results2.map((result) => result.chat), options);
|
|
343134
|
+
const allowedChatIds = new Set(chats.map((chat) => chat.id));
|
|
343135
|
+
return results2.filter((result) => allowedChatIds.has(result.chat.id));
|
|
342982
343136
|
}
|
|
342983
343137
|
function summarizeChat(chat) {
|
|
342984
343138
|
return {
|
|
@@ -343007,6 +343161,16 @@ function summarizeMessage(message) {
|
|
|
343007
343161
|
timestamp: message.timestamp
|
|
343008
343162
|
};
|
|
343009
343163
|
}
|
|
343164
|
+
function summarizeUnreadMessagesResult(result) {
|
|
343165
|
+
return {
|
|
343166
|
+
chat: summarizeChat(result.chat),
|
|
343167
|
+
unreadCount: result.unreadCount,
|
|
343168
|
+
returnedCount: result.returnedCount,
|
|
343169
|
+
complete: result.complete,
|
|
343170
|
+
...result.error ? { error: result.error } : {},
|
|
343171
|
+
messages: result.messages.map(summarizeMessage)
|
|
343172
|
+
};
|
|
343173
|
+
}
|
|
343010
343174
|
function summarizeMessageSearchResult(result) {
|
|
343011
343175
|
return {
|
|
343012
343176
|
chat: summarizeChat(result.chat),
|
|
@@ -343051,6 +343215,152 @@ function readinessMessage(status, browserLockCount) {
|
|
|
343051
343215
|
return "WhatsApp is not healthy. Run `wwa daemon stop --json && wwa auth login --image --json`.";
|
|
343052
343216
|
return "WhatsApp is still loading. Run the same wwa command again in a moment.";
|
|
343053
343217
|
}
|
|
343218
|
+
function puppeteerChromeRevision() {
|
|
343219
|
+
const puppeteer = getPuppeteer();
|
|
343220
|
+
const revisions = puppeteer.PUPPETEER_REVISIONS;
|
|
343221
|
+
return revisions?.chrome ?? revisionFromExecutablePath(puppeteerExecutablePath()) ?? "stable";
|
|
343222
|
+
}
|
|
343223
|
+
function puppeteerExecutablePath() {
|
|
343224
|
+
try {
|
|
343225
|
+
return withSanitizedPuppeteerEnv(() => getPuppeteer().executablePath());
|
|
343226
|
+
} catch (error51) {
|
|
343227
|
+
return error51 instanceof Error ? error51.message : String(error51);
|
|
343228
|
+
}
|
|
343229
|
+
}
|
|
343230
|
+
function puppeteerCliPath() {
|
|
343231
|
+
return requireFromHere.resolve("puppeteer/lib/cjs/puppeteer/node/cli.js");
|
|
343232
|
+
}
|
|
343233
|
+
function installPuppeteerBrowser() {
|
|
343234
|
+
const revision = puppeteerChromeRevision();
|
|
343235
|
+
const cliPath = puppeteerCliPath();
|
|
343236
|
+
const result = spawnSync2(process.execPath, [cliPath, "browsers", "install", `chrome@${revision}`], {
|
|
343237
|
+
encoding: "utf8",
|
|
343238
|
+
env: sanitizedPuppeteerEnv()
|
|
343239
|
+
});
|
|
343240
|
+
return {
|
|
343241
|
+
ok: result.status === 0,
|
|
343242
|
+
revision,
|
|
343243
|
+
command: `${process.execPath} ${cliPath} browsers install chrome@${revision}`,
|
|
343244
|
+
stdout: trimOutput(result.stdout),
|
|
343245
|
+
stderr: trimOutput(result.stderr),
|
|
343246
|
+
exitCode: result.status
|
|
343247
|
+
};
|
|
343248
|
+
}
|
|
343249
|
+
async function browserDiagnostics(smoke) {
|
|
343250
|
+
const revision = puppeteerChromeRevision();
|
|
343251
|
+
const executablePath = puppeteerExecutablePath();
|
|
343252
|
+
const exists = typeof executablePath === "string" && existsSync(executablePath);
|
|
343253
|
+
const appPath = typeof executablePath === "string" ? chromeAppPath(executablePath) : null;
|
|
343254
|
+
return {
|
|
343255
|
+
revision,
|
|
343256
|
+
executablePath,
|
|
343257
|
+
exists,
|
|
343258
|
+
appPath,
|
|
343259
|
+
platform: process.platform,
|
|
343260
|
+
launch: smoke ? await smokeTestPuppeteerLaunch() : { ok: null, skipped: true }
|
|
343261
|
+
};
|
|
343262
|
+
}
|
|
343263
|
+
async function smokeTestPuppeteerLaunch() {
|
|
343264
|
+
const puppeteer = getPuppeteer();
|
|
343265
|
+
let browser = null;
|
|
343266
|
+
try {
|
|
343267
|
+
browser = await withSanitizedPuppeteerEnv(() => puppeteer.launch({
|
|
343268
|
+
headless: true,
|
|
343269
|
+
args: ["--no-sandbox", "--disable-setuid-sandbox"]
|
|
343270
|
+
}));
|
|
343271
|
+
return { ok: true, error: null };
|
|
343272
|
+
} catch (error51) {
|
|
343273
|
+
return {
|
|
343274
|
+
ok: false,
|
|
343275
|
+
error: error51 instanceof Error ? error51.message : String(error51)
|
|
343276
|
+
};
|
|
343277
|
+
} finally {
|
|
343278
|
+
await browser?.close().catch(() => {
|
|
343279
|
+
return;
|
|
343280
|
+
});
|
|
343281
|
+
}
|
|
343282
|
+
}
|
|
343283
|
+
function getPuppeteer() {
|
|
343284
|
+
return withSanitizedPuppeteerEnv(() => requireFromHere("puppeteer"));
|
|
343285
|
+
}
|
|
343286
|
+
function sanitizedPuppeteerEnv() {
|
|
343287
|
+
const env2 = { ...process.env };
|
|
343288
|
+
if (env2.WWA_ALLOW_SYSTEM_CHROME !== "true") {
|
|
343289
|
+
delete env2.PUPPETEER_EXECUTABLE_PATH;
|
|
343290
|
+
delete env2.PUPPETEER_BROWSER_REVISION;
|
|
343291
|
+
}
|
|
343292
|
+
delete env2.WWA_HEADLESS;
|
|
343293
|
+
return env2;
|
|
343294
|
+
}
|
|
343295
|
+
function withSanitizedPuppeteerEnv(fn) {
|
|
343296
|
+
if (process.env.WWA_ALLOW_SYSTEM_CHROME === "true")
|
|
343297
|
+
return fn();
|
|
343298
|
+
const executablePath = process.env.PUPPETEER_EXECUTABLE_PATH;
|
|
343299
|
+
const browserRevision = process.env.PUPPETEER_BROWSER_REVISION;
|
|
343300
|
+
const headless = process.env.WWA_HEADLESS;
|
|
343301
|
+
delete process.env.PUPPETEER_EXECUTABLE_PATH;
|
|
343302
|
+
delete process.env.PUPPETEER_BROWSER_REVISION;
|
|
343303
|
+
delete process.env.WWA_HEADLESS;
|
|
343304
|
+
try {
|
|
343305
|
+
return fn();
|
|
343306
|
+
} finally {
|
|
343307
|
+
if (executablePath === undefined)
|
|
343308
|
+
delete process.env.PUPPETEER_EXECUTABLE_PATH;
|
|
343309
|
+
else
|
|
343310
|
+
process.env.PUPPETEER_EXECUTABLE_PATH = executablePath;
|
|
343311
|
+
if (browserRevision === undefined)
|
|
343312
|
+
delete process.env.PUPPETEER_BROWSER_REVISION;
|
|
343313
|
+
else
|
|
343314
|
+
process.env.PUPPETEER_BROWSER_REVISION = browserRevision;
|
|
343315
|
+
if (headless === undefined)
|
|
343316
|
+
delete process.env.WWA_HEADLESS;
|
|
343317
|
+
else
|
|
343318
|
+
process.env.WWA_HEADLESS = headless;
|
|
343319
|
+
}
|
|
343320
|
+
}
|
|
343321
|
+
function chromeAppPath(executablePath) {
|
|
343322
|
+
const marker = ".app/Contents/MacOS/";
|
|
343323
|
+
const index = executablePath.indexOf(marker);
|
|
343324
|
+
if (index === -1)
|
|
343325
|
+
return null;
|
|
343326
|
+
return executablePath.slice(0, index + ".app".length);
|
|
343327
|
+
}
|
|
343328
|
+
function revisionFromExecutablePath(executablePath) {
|
|
343329
|
+
const match = executablePath.match(/\/chrome\/[^/]+-(\d+\.\d+\.\d+\.\d+)\//);
|
|
343330
|
+
return match?.[1] ?? null;
|
|
343331
|
+
}
|
|
343332
|
+
function clearBrowserQuarantine() {
|
|
343333
|
+
if (process.platform !== "darwin") {
|
|
343334
|
+
return { ok: false, skipped: true, reason: "Quarantine metadata is only a macOS concern." };
|
|
343335
|
+
}
|
|
343336
|
+
const executablePath = puppeteerExecutablePath();
|
|
343337
|
+
const appPath = typeof executablePath === "string" ? chromeAppPath(executablePath) : null;
|
|
343338
|
+
if (!appPath)
|
|
343339
|
+
return { ok: false, skipped: true, reason: "Could not locate Chrome-for-Testing .app path.", executablePath };
|
|
343340
|
+
const result = spawnSync2("xattr", ["-dr", "com.apple.quarantine", appPath], {
|
|
343341
|
+
encoding: "utf8"
|
|
343342
|
+
});
|
|
343343
|
+
return {
|
|
343344
|
+
ok: result.status === 0,
|
|
343345
|
+
appPath,
|
|
343346
|
+
command: `xattr -dr com.apple.quarantine ${JSON.stringify(appPath)}`,
|
|
343347
|
+
stdout: trimOutput(result.stdout),
|
|
343348
|
+
stderr: trimOutput(result.stderr),
|
|
343349
|
+
exitCode: result.status
|
|
343350
|
+
};
|
|
343351
|
+
}
|
|
343352
|
+
function browserSetupNextCommand(diagnostics) {
|
|
343353
|
+
if (process.platform === "darwin" && diagnostics.appPath)
|
|
343354
|
+
return "wwa recover browser --clear-quarantine --json";
|
|
343355
|
+
return "wwa doctor --browser --json";
|
|
343356
|
+
}
|
|
343357
|
+
function trimOutput(value) {
|
|
343358
|
+
const text = (value ?? "").trim();
|
|
343359
|
+
if (text.length <= 4000)
|
|
343360
|
+
return text;
|
|
343361
|
+
return `${text.slice(0, 4000)}
|
|
343362
|
+
...truncated...`;
|
|
343363
|
+
}
|
|
343054
343364
|
|
|
343055
343365
|
// bin/wwa.ts
|
|
343056
343366
|
await main(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whatsapp-web-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "A local WhatsApp Web CLI for agents and scripts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"commander": "^14.0.2",
|
|
37
37
|
"mime-types": "^3.0.2",
|
|
38
|
+
"puppeteer": "^24.38.0",
|
|
38
39
|
"qrcode": "^1.5.4",
|
|
39
40
|
"qrcode-terminal": "^0.12.0",
|
|
40
41
|
"whatsapp-web.js": "1.34.6",
|
|
@@ -65,7 +65,7 @@ wwa reply audio --message <messageId> --file ./note.ogg --voice
|
|
|
65
65
|
|
|
66
66
|
## Workflow Guidance
|
|
67
67
|
|
|
68
|
-
To review unread messages,
|
|
68
|
+
To review unread messages, use `wwa unread messages --json` so the CLI returns unread messages grouped by chat with `unreadCount`, `returnedCount`, and `complete`. If `complete` is false, WhatsApp Web did not expose every unread message to the local session; treat the row as partial instead of assuming the latest message is the whole unread burst. If an audio attachment is returned, use `wwa transcribe --message <messageId> --json`.
|
|
69
69
|
|
|
70
70
|
To find a person or conversation, prefer:
|
|
71
71
|
|