# wa-cli [English](README.md) WhatsApp desde la terminal, con la misma forma que `gws`: ``` wa [--params JSON] [--format json|table|csv|yaml|ndjson|text|eml|mbox] wa + ... wa schema ``` Usa [whatsapp-web.js](https://github.com/wwebjs/whatsapp-web.js): un Chrome headless con WhatsApp Web, vinculado a tu teléfono como un dispositivo más. **No es un cliente oficial**: WhatsApp puede bloquear cuentas que usan clientes no oficiales. Usarlo con moderación (nada de envíos masivos). ## Instalación Conviene tener el proyecto fuera de `/tmp` (systemd-tmpfiles borra lo que no se usa en 10 días). ```sh git clone https://git.all.ar/pub/wa-cli.git && cd wa-cli npm install # baja Chrome para puppeteer si no está en ~/.cache/puppeteer ln -s "$PWD/bin/wa.js" ~/.local/bin/wa wa auth login # QR en la terminal wa auth login --phone 549... # o código de 8 caracteres ``` Opcional, daemon permanente (necesario para recibir mensajes con la terminal cerrada): ```sh cp systemd/wa-daemon@.service ~/.config/systemd/user/ systemctl --user enable --now wa-daemon@default ``` Sin systemd, el primer comando `wa` levanta el daemon solo (`WA_CLI_AUTOSTART=0` lo desactiva; con systemd conviene `"autostart": false` en `config.json` para no tener dos compitiendo). ## Uso ```sh wa +triage # chats con no leídos wa +read "Familia" -n 50 --mark-read # conversación legible wa +send "Mamá" llego en 10 # por nombre exacto, número o JID wa +send +5491122334455 -f informe.pdf "ahí va" wa +reply "" dale wa +watch --format text # mensajes entrantes en vivo wa +watch --events message_create | jq -r .data.body wa +download "" -d ~/Descargas wa chats list --params '{"unread": true}' --format table wa messages list --params '{"chatId": "Familia", "limit": 200, "since": 1788000000}' wa messages search --params '{"query": "factura"}' --format table wa messages react --params '{"messageId": "...", "emoji": "👍"}' wa contacts resolve --params '{"number": "+54 9 11 2233-4455"}' wa schema messages.send # parámetros de un método wa messages send --help ``` Varias cuentas (como `gws` con varias cuentas): `-a trabajo` o `WA_CLI_ACCOUNT=trabajo`. Cada cuenta tiene su sesión, su socket y su daemon. Códigos de salida: `0` ok, `1` error de WhatsApp / no encontrado, `2` sin sesión, `3` validación, `4` daemon, `5` interno. ## Arquitectura ``` wa (CLI) ──JSON por línea──▶ socket Unix ──▶ daemon ──▶ whatsapp-web.js ──▶ Chrome headless │ ├──▶ hooks ~/.config/wa-cli/hooks/ └──▶ events.watch (stream NDJSON) ``` WhatsApp Web tarda 10–30 s en arrancar y solo recibe mensajes mientras está abierto, por eso hay un daemon por cuenta y el CLI es un cliente liviano. | Qué | Dónde | | ------------------- | ---------------------------------------------- | | Sesión (perfil) | `~/.local/share/wa-cli//` | | Media descargada | `~/.local/share/wa-cli//media/` | | Log (autostart) | `~/.local/state/wa-cli//daemon.log` | | Socket | `$XDG_RUNTIME_DIR/wa-cli/.sock` | | Config y hooks | `~/.config/wa-cli/{config.json,hooks/}` | `config.json` (todo opcional): ```json { "autostart": true, "readyTimeoutMs": 120000, "hooks": true, "puppeteer": { "headless": true, "executablePath": "/usr/bin/chromium" }, "accounts": { "trabajo": { "hooks": false } } } ``` ### Código | Archivo | Rol | | ----------------------- | -------------------------------------------------------------------- | | `src/api/*.js` | Métodos `recurso.método`: parámetros, columnas y handler | | `src/serialize.js` | Objetos de whatsapp-web.js → JSON estable (el contrato hacia afuera) | | `src/daemon.js` | Sesión, socket, eventos, hooks, resolución de chats por nombre | | `src/cli.js` | Parser estilo gws, ayuda, `auth login`, `daemon` | | `src/helpers.js` | `+send`, `+read`, `+watch`, ... | | `src/format/` | json/table/csv/yaml/ndjson/text/eml/mbox | | `src/compat.js` | Parches para cambios de WhatsApp Web (ver Problemas conocidos) | | `src/wamail/` | Puente Maildir: sync, send (parser MIME), lista de buzones | **Agregar un método** = sumar una entrada en `src/api/.js`. Queda disponible por el socket, en `wa `, en `--help` y en `wa schema`. Los parámetros de tipo `chat` aceptan JID, número o nombre exacto; los de tipo `path` se resuelven contra el cwd del CLI. **Agregar un formato** = una función en `FORMATS` (`src/format/index.js`). ## Eventos y hooks `wa events watch --params '{"events": ["all"]}'` (o `wa +watch`) emite una línea por evento: ```json {"event": "message", "account": "default", "time": "2026-09-16T21:30:00.000Z", "data": { ...mensaje }} ``` | Evento | `data` | | ---------------------------------------------------- | ------------------------------------------- | | `message` (entrantes), `message_create` (todos) | mensaje | | `message_ack` | `{message, ack}` (1 servidor, 2 entregado, 3 leído) | | `message_edit` | `{message, newBody, prevBody}` | | `message_revoke_everyone` | `{message, revoked}` | | `message_reaction` | `{messageId, chatId, senderId, reaction}` | | `unread_count`, `chat_removed` | chat | | `group_join`, `group_leave`, `group_update`, ... | notificación de grupo | | `qr`, `code`, `authenticated`, `ready`, `disconnected` | estado de la sesión | Los mismos eventos disparan **hooks**: ejecutables en `~/.config/wa-cli/hooks/`, `.d/*` o `all.d/*`. Reciben el evento por stdin, el nombre como `$1` y `WA_CLI_ACCOUNT` / `WA_CLI_SOCKET` en el entorno (pueden llamar a `wa`). Se ejecutan en orden por hook y se re-escanean en cada evento (no hace falta reiniciar). Un mensaje serializado: ```json { "id": "false_5491122334455@c.us_3EB0C4...", "stanzaId": "3EB0C4...", "chatId": "5491122334455@c.us", "chatName": "Juan Pérez", "fromMe": false, "sender": "5491122334455@c.us", "senderName": "Juan Pérez", "timestamp": 1789500000, "date": "2026-09-15T19:20:00.000Z", "type": "chat", "body": "hola", "hasMedia": false, "media": null, "hasQuotedMsg": false, "quotedStanzaId": null, "ack": 1, ... } ``` ## Integración con neomutt: wamail `wamail` es el puente WhatsApp ↔ Maildir, análogo a `gwsmail` para Gmail: una **carpeta por chat** en `~/Mail/whatsapp/`, y neomutt la lee como cualquier cuenta. ``` wa-daemon ──hook (tiempo real)──▶ wamail sync --chat X ──▶ ~/Mail/whatsapp//{new,cur} ◀──────────────────── wamail sync (timer 5 min: leídos, estrellas, borrados) neomutt ──sendmail──▶ wamail send ──▶ wa messages send ``` | En neomutt | En WhatsApp | | ------------------------------------- | -------------------------------------------------------------- | | carpeta del sidebar | chat (orden de WhatsApp: fijados, recientes; archivados al final) | | leer todos los no leídos de un chat | chat marcado como leído (confirmación de lectura) | | marcar un mensaje como no leído | chat marcado como no leído | | flag (`F`) | mensaje destacado | | borrar | **solo local** (no se vuelve a bajar) | | `r` dejando la cita (`> ...`) | respuesta citada; si borrás la cita, mensaje normal | | `r` / `g` en un grupo | va solo al grupo (nunca en privado al autor) | | adjuntos | un solo adjunto imagen/video: con el texto de caption; si no, texto + documentos | | `gR` / `gX` / `gA` / `gG` | reaccionar / borrar (todos o para mí) / archivar chat / sincronizar ya | | `Tab` en To: | buscar contactos y grupos (`wamail query`) | | `F6` | cambiar a la cuenta WhatsApp | Recibido en WhatsApp → mail: remitente = JID (`5491122334455@c.us`, grupos `...@g.us`), `Message-ID `, `In-Reply-To` para respuestas citadas, `X-WA-Id`/`X-WA-Chat-Id` para las acciones, media como adjunto (hasta `mediaMaxBytes`; si no, una nota con el comando para bajarla). Los mensajes borrados para todos desaparecen del Maildir (hook `message_revoke_everyone`). ### Instalación ```sh ln -s "$PWD/bin/wamail.js" ~/.local/bin/wamail mkdir -p ~/.config/wa-cli/hooks/all.d ln -s "$PWD/contrib/hooks/wamail" ~/.config/wa-cli/hooks/all.d/50-wamail cp systemd/wamail-sync.{service,timer} ~/.config/systemd/user/ systemctl --user daemon-reload && systemctl --user enable --now wamail-sync.timer wamail sync default # primera vez: últimos 30 días, hasta 100 mensajes por chat mkdir -p ~/.config/neomutt/accounts/wa cp contrib/neomutt/wa-default.muttrc ~/.config/neomutt/accounts/wa/default.muttrc ``` y en la config de neomutt: ```muttrc # neomuttrc folder-hook 'Mail/whatsapp' 'source ~/.config/neomutt/accounts/wa/default.muttrc' # keys/bind.muttrc macro index,pager 'source ~/.config/neomutt/accounts/wa/default.muttrc!' "WhatsApp (wamail)" # switch.muttrc (lo que la cuenta WhatsApp cambia) unmacro index,pager gR unmacro index,pager gX set query_command = "khard email --parsable --search-in-source-files '%s'" ``` Config opcional en `~/.config/wa-cli/config.json`: ```json { "wamail": { "backfillDays": 30, "backfillLimit": 100, "mediaMaxBytes": 16777216, "skipChats": ["120363...@g.us"] } } ``` Estado en `~/.local/state/wamail/.json` (borrarlo junto con el Maildir fuerza un backfill limpio); envíos en `~/.local/state/wamail/send.log`. ### Exportar un chat sin sincronizar ```sh wa +export "Familia" -n 500 --media -o /tmp/familia.mbox && neomutt -R -f /tmp/familia.mbox ``` ## Problemas conocidos WhatsApp Web cambia seguido y whatsapp-web.js tarda en seguirlo. `src/compat.js` parchea en caliente, sin tocar `node_modules`: - **`_serialized` → `$1`** en IDs de mensaje (wwebjs#201830). Síntoma: `r: r` en `chats list`, `sendMessage` devolviendo `undefined`. - **Re-inyección en navegaciones SPA** (wwebjs#201653, arreglado en `main`, sin release). - **Iframe de otro origen** (`pdf-viewer`): con site isolation es un proceso aparte y, si navega mientras se registran los bindings, `exposeFunction` falla con `Target closed` y `ready` no llega nunca. El daemon lanza Chrome con `--disable-site-isolation-trials`. Si igual se cuelga, el watchdog del daemon sale con código 1 (60 s en `authenticated`, `startTimeoutMs` en `starting`) y systemd lo relanza. Para diagnosticar: `WA_CLI_TRACE=1` (o `"trace": true` en `config.json`) loguea llamadas a Chrome pendientes, excepciones dentro de bindings y navegaciones (`journalctl --user -u wa-daemon@default`). Chrome headless usa ~1 GB (picos de 2 GB en la sincronización inicial). ## Tests ```sh npm test # formatos, parser MIME (con mails reales de neomutt), sync/send de wamail contra un WhatsApp falso ``` ## Licencia MIT