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 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 lives at:
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: 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: process.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
- throw new Error(`Timed out starting daemon. See ${paths.logs}`);
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.1.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 recent messages for bounded unread chats.").option("--limit-chats <count>", "Maximum unread chats to inspect.", "10").option("--messages-per-chat <count>", "Maximum messages per chat.", "5").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 (options) => {
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 chats2 = await daemonRequestExisting("/chats?unread=true");
342791
- const selected = filterChats(chats2, { ...options, limit: options.limitChats });
342792
- const messagesPerChat = parseNumber(options.messagesPerChat, 5);
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
- return chats.filter((chat) => options.includeStatus ? true : chat.id !== "status@broadcast").filter((chat) => options.groups ? chat.isGroup : true).filter((chat) => options.nonGroups ? !chat.isGroup : true).slice(0, limit);
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.1.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, list a bounded set of unread chats, fetch recent messages per chat, and save media only when needed. If an audio attachment is saved, use `wwa transcribe --message <messageId> --json`.
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