whatsapp-web-cli 0.1.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 +43 -1
- package/dist/wwa.js +369 -19
- package/package.json +2 -1
- package/skills/whatsapp-cli/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -40,6 +40,18 @@ Then run the command you actually need:
|
|
|
40
40
|
wwa chat-search "Name" --json
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
To install the bundled Codex skill for local agent use:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
wwa skill install --json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
By default this writes:
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
~/.codex/skills/whatsapp-cli/SKILL.md
|
|
53
|
+
```
|
|
54
|
+
|
|
43
55
|
If WhatsApp is not logged in, data commands return a JSON object like:
|
|
44
56
|
|
|
45
57
|
```json
|
|
@@ -62,6 +74,28 @@ wwa chat-search "Name" --json
|
|
|
62
74
|
There is no required `doctor` or `ready` step in normal usage. Data commands
|
|
63
75
|
auto-start the daemon and wait for readiness after auth is scanned.
|
|
64
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
|
+
|
|
65
99
|
## Common Flow
|
|
66
100
|
|
|
67
101
|
```bash
|
|
@@ -83,6 +117,8 @@ wwa auth logout --json
|
|
|
83
117
|
|
|
84
118
|
wwa ready --wait 120 --json
|
|
85
119
|
wwa doctor --json
|
|
120
|
+
wwa doctor --browser --json
|
|
121
|
+
wwa recover browser --json
|
|
86
122
|
|
|
87
123
|
wwa chat-search "name or text" --json
|
|
88
124
|
wwa message-search "message text" --limit-chats 20 --messages-per-chat 50 --json
|
|
@@ -169,10 +205,16 @@ You should only need to scan a QR again if WhatsApp invalidates the linked devic
|
|
|
169
205
|
|
|
170
206
|
## Codex Skill
|
|
171
207
|
|
|
172
|
-
The generic skill
|
|
208
|
+
The generic skill is included in the package at:
|
|
173
209
|
|
|
174
210
|
```text
|
|
175
211
|
skills/whatsapp-cli/SKILL.md
|
|
176
212
|
```
|
|
177
213
|
|
|
214
|
+
Install or update it for Codex with:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
wwa skill install --json
|
|
218
|
+
```
|
|
219
|
+
|
|
178
220
|
It teaches agents to use the CLI directly, without MCP, and keeps transcription/classification workflows outside the low-level CLI.
|
package/dist/wwa.js
CHANGED
|
@@ -326990,6 +326990,14 @@ var {
|
|
|
326990
326990
|
Help
|
|
326991
326991
|
} = import__.default;
|
|
326992
326992
|
|
|
326993
|
+
// src/cli.ts
|
|
326994
|
+
import { copyFile as copyFile2, mkdir as mkdir5 } from "node:fs/promises";
|
|
326995
|
+
import { existsSync } from "node:fs";
|
|
326996
|
+
import { homedir as homedir2 } from "node:os";
|
|
326997
|
+
import { dirname as dirname7, join as join2, resolve as resolve9 } from "node:path";
|
|
326998
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
326999
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
327000
|
+
|
|
326993
327001
|
// src/daemon.ts
|
|
326994
327002
|
import { createServer } from "node:http";
|
|
326995
327003
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
@@ -341280,7 +341288,6 @@ import { readFile as readFile2, writeFile as writeFile3 } from "node:fs/promises
|
|
|
341280
341288
|
import { basename as basename2, resolve as resolve5 } from "node:path";
|
|
341281
341289
|
import { promisify } from "node:util";
|
|
341282
341290
|
var { Client, LocalAuth, MessageMedia } = import_whatsapp_web.default;
|
|
341283
|
-
var HEADLESS = process.env.WWA_HEADLESS !== "false";
|
|
341284
341291
|
var execFileAsync = promisify(execFile);
|
|
341285
341292
|
|
|
341286
341293
|
class WhatsAppClient {
|
|
@@ -341299,7 +341306,7 @@ class WhatsAppClient {
|
|
|
341299
341306
|
authTimeoutMs: 120000,
|
|
341300
341307
|
...process.env.WWA_USER_AGENT ? { userAgent: process.env.WWA_USER_AGENT } : {},
|
|
341301
341308
|
puppeteer: {
|
|
341302
|
-
headless:
|
|
341309
|
+
headless: true,
|
|
341303
341310
|
args: ["--no-sandbox", "--disable-setuid-sandbox"]
|
|
341304
341311
|
}
|
|
341305
341312
|
});
|
|
@@ -341414,6 +341421,35 @@ class WhatsAppClient {
|
|
|
341414
341421
|
const messages = chat.messages;
|
|
341415
341422
|
return messages.map((message) => this.serializeMessage(message, chatId));
|
|
341416
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
|
+
}
|
|
341417
341453
|
async getMessage(messageId) {
|
|
341418
341454
|
await this.assertReady();
|
|
341419
341455
|
const message = await this.client.getMessageById(messageId);
|
|
@@ -341563,6 +341599,70 @@ class WhatsAppClient {
|
|
|
341563
341599
|
throw firstError;
|
|
341564
341600
|
}
|
|
341565
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
|
+
}
|
|
341566
341666
|
async findChat(chatId) {
|
|
341567
341667
|
try {
|
|
341568
341668
|
return await this.client.getChatById(chatId);
|
|
@@ -341629,12 +341729,17 @@ function recommendationFor(phase, lastError) {
|
|
|
341629
341729
|
return "Authenticate with `wwa auth login --image --json` and render the returned imagePath.";
|
|
341630
341730
|
if (phase === "authenticated_loading")
|
|
341631
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.";
|
|
341632
341734
|
if (phase === "failed" && lastError?.includes("already running"))
|
|
341633
341735
|
return "Browser profile lock detected. Run `wwa daemon stop --json`, then retry.";
|
|
341634
341736
|
if (phase === "failed")
|
|
341635
341737
|
return "Inspect `wwa doctor --json`, then restart with `wwa daemon stop --json && wwa daemon start --json`.";
|
|
341636
341738
|
return "Starting. Wait with `wwa ready --wait 120 --json`.";
|
|
341637
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
|
+
}
|
|
341638
341743
|
async function mediaFromFile(filePath, options) {
|
|
341639
341744
|
const resolved = options.asVoice ? await ensureVoiceNoteAudio(filePath) : resolve5(filePath);
|
|
341640
341745
|
const data = await readFile2(resolved);
|
|
@@ -341791,6 +341896,9 @@ async function runDaemon() {
|
|
|
341791
341896
|
const limit = Number(url2.searchParams.get("limit") ?? 50);
|
|
341792
341897
|
return json2(await client.listMessages(chatId, Number.isFinite(limit) ? limit : 50));
|
|
341793
341898
|
}
|
|
341899
|
+
if (request.method === "GET" && url2.pathname === "/unread/messages") {
|
|
341900
|
+
return json2(await client.listUnreadMessages());
|
|
341901
|
+
}
|
|
341794
341902
|
if (request.method === "GET" && url2.pathname === "/message-search") {
|
|
341795
341903
|
const query = url2.searchParams.get("query");
|
|
341796
341904
|
if (!query)
|
|
@@ -342077,7 +342185,7 @@ async function startDaemon() {
|
|
|
342077
342185
|
const child = spawn(process.execPath, [...process.execArgv, process.argv[1] ?? "./bin/wwa.ts", "__daemon"], {
|
|
342078
342186
|
detached: true,
|
|
342079
342187
|
stdio: ["ignore", logFd, logFd],
|
|
342080
|
-
env:
|
|
342188
|
+
env: daemonEnv()
|
|
342081
342189
|
});
|
|
342082
342190
|
child.unref();
|
|
342083
342191
|
const deadline = Date.now() + START_TIMEOUT_MS;
|
|
@@ -342087,7 +342195,9 @@ async function startDaemon() {
|
|
|
342087
342195
|
return status;
|
|
342088
342196
|
await sleep(250);
|
|
342089
342197
|
}
|
|
342090
|
-
|
|
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}`);
|
|
342091
342201
|
});
|
|
342092
342202
|
}
|
|
342093
342203
|
async function stopDaemon() {
|
|
@@ -342218,6 +342328,26 @@ async function removeStartLock() {
|
|
|
342218
342328
|
await unlink2(paths.daemonLock);
|
|
342219
342329
|
} catch {}
|
|
342220
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
|
+
}
|
|
342221
342351
|
function listRuntimeProcesses() {
|
|
342222
342352
|
const output = spawnSync("ps", ["axo", "pid=,pgid=,command="], {
|
|
342223
342353
|
encoding: "utf8"
|
|
@@ -342481,13 +342611,14 @@ async function generateSpeech(options) {
|
|
|
342481
342611
|
}
|
|
342482
342612
|
|
|
342483
342613
|
// src/cli.ts
|
|
342614
|
+
var requireFromHere = createRequire2(import.meta.url);
|
|
342484
342615
|
async function main(argv) {
|
|
342485
342616
|
if (argv[2] === "__daemon") {
|
|
342486
342617
|
await runDaemon();
|
|
342487
342618
|
return;
|
|
342488
342619
|
}
|
|
342489
342620
|
const program2 = new Command;
|
|
342490
|
-
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");
|
|
342491
342622
|
const daemon = program2.command("daemon").description("Manage the local WhatsApp daemon.");
|
|
342492
342623
|
daemon.command("start").description("Start the local daemon.").option("--json", "Print JSON output.").action(async (options) => {
|
|
342493
342624
|
const status = await startDaemon();
|
|
@@ -342784,20 +342915,13 @@ async function main(argv) {
|
|
|
342784
342915
|
const result = filterChats(chats2, options).map(summarizeChat);
|
|
342785
342916
|
output(options, result, result);
|
|
342786
342917
|
});
|
|
342787
|
-
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);
|
|
342788
342920
|
if (!await ensureReady(options))
|
|
342789
342921
|
return;
|
|
342790
|
-
const
|
|
342791
|
-
const selected =
|
|
342792
|
-
const
|
|
342793
|
-
const result = [];
|
|
342794
|
-
for (const chat of selected) {
|
|
342795
|
-
const messages2 = await daemonRequestExisting(`/messages?chatId=${encodeURIComponent(chat.id)}&limit=${messagesPerChat}`);
|
|
342796
|
-
result.push({
|
|
342797
|
-
chat: summarizeChat(chat),
|
|
342798
|
-
messages: messages2.map(summarizeMessage)
|
|
342799
|
-
});
|
|
342800
|
-
}
|
|
342922
|
+
const unreadResults = await daemonRequestExisting("/unread/messages");
|
|
342923
|
+
const selected = filterUnreadMessageResults(unreadResults, { ...options, limit: options.limitChats });
|
|
342924
|
+
const result = selected.map(summarizeUnreadMessagesResult);
|
|
342801
342925
|
output(options, result, result);
|
|
342802
342926
|
});
|
|
342803
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) => {
|
|
@@ -342809,13 +342933,14 @@ async function main(argv) {
|
|
|
342809
342933
|
if (!result.ready)
|
|
342810
342934
|
process.exitCode = 1;
|
|
342811
342935
|
});
|
|
342812
|
-
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) => {
|
|
342813
342937
|
const status = await daemonStatus();
|
|
342814
342938
|
const browserLocks = listBrowserLocks();
|
|
342815
342939
|
const result = {
|
|
342816
342940
|
ok: Boolean(status?.ready),
|
|
342817
342941
|
daemon: status ?? { running: false },
|
|
342818
342942
|
browserLocks,
|
|
342943
|
+
...options.browser ? { browser: await browserDiagnostics(false) } : {},
|
|
342819
342944
|
paths,
|
|
342820
342945
|
nextCommand: nextCommand(status, browserLocks.length)
|
|
342821
342946
|
};
|
|
@@ -342824,6 +342949,27 @@ async function main(argv) {
|
|
|
342824
342949
|
program2.command("paths").description("Show local runtime paths.").option("--json", "Print JSON output.").action((options) => {
|
|
342825
342950
|
output(options, paths, paths);
|
|
342826
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
|
+
});
|
|
342968
|
+
const skill = program2.command("skill").description("Install helper files for agent integrations.");
|
|
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) => {
|
|
342970
|
+
const result = await installCodexSkill(options);
|
|
342971
|
+
output(options, result, `Installed ${result.name} skill at ${result.installedPath}`);
|
|
342972
|
+
});
|
|
342827
342973
|
try {
|
|
342828
342974
|
await program2.parseAsync(argv);
|
|
342829
342975
|
} catch (error51) {
|
|
@@ -342842,6 +342988,34 @@ function output(options, data, human) {
|
|
|
342842
342988
|
function isTimeoutResult(value) {
|
|
342843
342989
|
return Boolean(value && typeof value === "object" && "timedOut" in value && value.timedOut === true);
|
|
342844
342990
|
}
|
|
342991
|
+
async function installCodexSkill(options) {
|
|
342992
|
+
const sourcePath = resolveBundledSkillPath();
|
|
342993
|
+
const skillRoot = resolve9(options.dir);
|
|
342994
|
+
const installedDir = join2(skillRoot, options.name);
|
|
342995
|
+
const installedPath = join2(installedDir, "SKILL.md");
|
|
342996
|
+
await mkdir5(installedDir, { recursive: true });
|
|
342997
|
+
await copyFile2(sourcePath, installedPath);
|
|
342998
|
+
return {
|
|
342999
|
+
ok: true,
|
|
343000
|
+
name: options.name,
|
|
343001
|
+
sourcePath,
|
|
343002
|
+
installedPath,
|
|
343003
|
+
skillsDir: skillRoot
|
|
343004
|
+
};
|
|
343005
|
+
}
|
|
343006
|
+
function resolveBundledSkillPath() {
|
|
343007
|
+
const moduleDir = dirname7(fileURLToPath2(import.meta.url));
|
|
343008
|
+
const candidates = [
|
|
343009
|
+
join2(moduleDir, "..", "skills", "whatsapp-cli", "SKILL.md"),
|
|
343010
|
+
join2(moduleDir, "skills", "whatsapp-cli", "SKILL.md"),
|
|
343011
|
+
join2(process.cwd(), "skills", "whatsapp-cli", "SKILL.md")
|
|
343012
|
+
];
|
|
343013
|
+
const found = candidates.find((candidate) => existsSync(candidate));
|
|
343014
|
+
if (!found) {
|
|
343015
|
+
throw new Error(`Could not find bundled whatsapp-cli skill. Checked: ${candidates.join(", ")}`);
|
|
343016
|
+
}
|
|
343017
|
+
return found;
|
|
343018
|
+
}
|
|
342845
343019
|
async function ensureReady(options, waitSeconds = 60) {
|
|
342846
343020
|
let status;
|
|
342847
343021
|
try {
|
|
@@ -342938,7 +343112,27 @@ function mediaCommandBody(options) {
|
|
|
342938
343112
|
}
|
|
342939
343113
|
function filterChats(chats, options) {
|
|
342940
343114
|
const limit = options.limit ? parseNumber(options.limit, chats.length) : chats.length;
|
|
342941
|
-
|
|
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));
|
|
342942
343136
|
}
|
|
342943
343137
|
function summarizeChat(chat) {
|
|
342944
343138
|
return {
|
|
@@ -342967,6 +343161,16 @@ function summarizeMessage(message) {
|
|
|
342967
343161
|
timestamp: message.timestamp
|
|
342968
343162
|
};
|
|
342969
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
|
+
}
|
|
342970
343174
|
function summarizeMessageSearchResult(result) {
|
|
342971
343175
|
return {
|
|
342972
343176
|
chat: summarizeChat(result.chat),
|
|
@@ -343011,6 +343215,152 @@ function readinessMessage(status, browserLockCount) {
|
|
|
343011
343215
|
return "WhatsApp is not healthy. Run `wwa daemon stop --json && wwa auth login --image --json`.";
|
|
343012
343216
|
return "WhatsApp is still loading. Run the same wwa command again in a moment.";
|
|
343013
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
|
+
}
|
|
343014
343364
|
|
|
343015
343365
|
// bin/wwa.ts
|
|
343016
343366
|
await main(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whatsapp-web-cli",
|
|
3
|
-
"version": "0.
|
|
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
|
|