aboutsummaryrefslogtreecommitdiffstats

wa-cli

English

WhatsApp desde la terminal, con la misma forma que gws:

wa <recurso> <método> [--params JSON] [--format json|table|csv|yaml|ndjson|text|eml|mbox]
wa +<helper> ...
wa schema <recurso.método>

Usa 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).

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):

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

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 "<messageId>" dale
wa +watch --format text                      # mensajes entrantes en vivo
wa +watch --events message_create | jq -r .data.body
wa +download "<messageId>" -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/<evento>
                                              └──▶ 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/<cuenta>/
Media descargada ~/.local/share/wa-cli/<cuenta>/media/
Log (autostart) ~/.local/state/wa-cli/<cuenta>/daemon.log
Socket $XDG_RUNTIME_DIR/wa-cli/<cuenta>.sock
Config y hooks ~/.config/wa-cli/{config.json,hooks/}

config.json (todo opcional):

{
  "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/<recurso>.js. Queda disponible por el socket, en wa <recurso> <método>, 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:

{"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/<evento>, <evento>.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:

{
  "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/<chat>/{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 <stanzaId@wa>, 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

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:

# neomuttrc
folder-hook 'Mail/whatsapp' 'source ~/.config/neomutt/accounts/wa/default.muttrc'
# keys/bind.muttrc
macro index,pager <f6> '<enter-command>source ~/.config/neomutt/accounts/wa/default.muttrc<enter><change-folder>!<enter>' "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:

{ "wamail": { "backfillDays": 30, "backfillLimit": 100, "mediaMaxBytes": 16777216, "skipChats": ["120363...@g.us"] } }

Estado en ~/.local/state/wamail/<cuenta>.json (borrarlo junto con el Maildir fuerza un backfill limpio); envíos en ~/.local/state/wamail/send.log.

Exportar un chat sin sincronizar

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

npm test    # formatos, parser MIME (con mails reales de neomutt), sync/send de wamail contra un WhatsApp falso

Licencia

MIT