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 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: 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: process.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
- 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}`);
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.1.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 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);
342795
342920
  if (!await ensureReady(options))
342796
342921
  return;
342797
- const chats2 = await daemonRequestExisting("/chats?unread=true");
342798
- const selected = filterChats(chats2, { ...options, limit: options.limitChats });
342799
- const messagesPerChat = parseNumber(options.messagesPerChat, 5);
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
- 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));
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.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