Content
# wu
WhatsApp CLI tool — like `gh` is to GitHub, `wu` is to WhatsApp.
Built on [@whiskeysockets/baileys](https://github.com/WhiskeySockets/Baileys).
## Install
```bash
npm install -g @ibrahimwithi/wu-cli
```
Requires Node.js 20+.
## Quick Start
```bash
# Authenticate (scan QR code)
wu login
# Or use pairing code instead
wu login --code 15551234567
# Start collecting messages
wu listen
# Search messages
wu messages search "meeting tomorrow"
# Send a message
wu messages send 1234567890@s.whatsapp.net "Hello!"
# List your groups
wu groups list --live
```
By default, wu operates in **opt-in mode** — no messages are collected until you allow specific chats:
```bash
# Allow all group messages (read-only)
wu config allow '*@g.us' --mode read
# Allow full access to a specific chat
wu config allow 1234567890@s.whatsapp.net
# Start collecting
wu listen
```
## Commands
### Authentication
| Command | Description |
|---|---|
| `wu login` | Authenticate with WhatsApp via QR code |
| `wu login --code <phone>` | Authenticate via pairing code |
| `wu logout` | Clear session |
| `wu status` | Show connection status and account info |
### Messages
| Command | Description |
|---|---|
| `wu messages list <jid>` | List messages in a chat |
| `wu messages search <query>` | Full-text search messages (FTS5 ranked) |
| `wu messages send <jid> [text]` | Send text, media, or poll |
| `wu messages react <jid> <id> <emoji>` | React to a message |
| `wu messages delete <jid> <id>` | Delete a message for everyone |
| `wu messages export <jid>` | Export messages to a file (jsonl/json/markdown/csv) |
```bash
# Send with media
wu messages send 1234567890@s.whatsapp.net --media photo.jpg --caption "Check this out"
# Create a poll
wu messages send 120363XXX@g.us --poll "Lunch?" --options "Pizza,Sushi,Tacos"
# Reply to a message
wu messages send 120363XXX@g.us "Agreed" --reply-to BAE5ABC123
# Search within a specific chat
wu messages search "budget" --chat 120363XXX@g.us --limit 20
# Download all undownloaded media in a chat
wu media download-batch 120363XXX@g.us --limit 50 --concurrency 4
# Export all messages since a timestamp to a file
wu messages export 120363XXX@g.us --after 1772548621 --format jsonl --output data/export.jsonl
# Export as readable markdown
wu messages export 120363XXX@g.us --format markdown --output data/chat.md
```
### Chats & Contacts
| Command | Description |
|---|---|
| `wu chats list` | List all chats |
| `wu chats search <query>` | Search chats by name |
| `wu contacts list` | List all contacts |
| `wu contacts search <query>` | Search contacts by name or phone |
| `wu contacts info <jid>` | Show contact details |
### Groups
| Command | Description |
|---|---|
| `wu groups list` | List groups with community tree and constraint status |
| `wu groups list --live` | Fetch from WhatsApp and refresh community linkage |
| `wu groups list --allowed-only` | Skip groups whose constraint is `none` |
| `wu groups info <jid>` | Show group details and participants |
| `wu groups create <name> [jids...]` | Create a new group |
| `wu groups invite <jid>` | Get invite link |
| `wu groups leave <jid>` | Leave a group |
| `wu groups rename <jid> <name>` | Rename a group |
| `wu groups join <code-or-url>` | Join a group by invite code or URL |
| `wu groups participants <jid>` | List group participants |
By default `wu groups list` shows every group you're in (jid + name + community shape) so you can see what's there before opting in. Description, full participant list, and messages stay constraint-gated. Set `whatsapp.group_discovery: false` in the config to revert to the old behavior where group metadata is only stored when the constraint allows.
### Communities
| Command | Description |
|---|---|
| `wu communities list` | List WhatsApp Communities (parent groups) |
| `wu communities list --with-subgroups` | Include linked subgroups under each parent |
### Direct messages (DMs)
| Command | Description |
|---|---|
| `wu dms list` | List 1:1 chats you've opted into |
| `wu dms list --all` | Include DMs blocked by constraints |
| `wu dms search <query>` | Search 1:1 chats by name |
DM JIDs contain the contact's phone number, so they're always constraint-gated regardless of `group_discovery`.
### Media
| Command | Description |
|---|---|
| `wu media download <msg-id>` | Download media from a message |
| `wu media download-batch [jid]` | Download undownloaded media in parallel (`--ids a,b,c` for specific messages) |
| `wu media send <jid> <path>` | Send a media file |
| `wu media transcribe <msg-id>` | Transcribe a voice/audio message to text |
| `wu media ocr <msg-id>` | Extract text from an image message |
| `wu media prune` | Delete downloaded media files (`--older-than 30d`, `--chat`, `--dry-run`) |
Transcripts and OCR text are stored on the message and indexed for `wu messages search`.
### Enrichment
Transcription and OCR run through pluggable backends — a local binary (default) or a hosted API. wu bundles no models; install the binary or set an API key.
```bash
# See which backends are configured and ready, with how to enable them
wu enrich status
```
| Capability | Local default | API option |
|---|---|---|
| `transcribe` | `whisper` (or whisper.cpp via a custom command) | any OpenAI-compatible audio API (Groq, OpenAI) |
| `ocr` | `tesseract` (with `ara+eng` data) | Anthropic vision, or any OpenAI-compatible vision API |
Configure under `enrich` in `config.yaml` (see Configuration below).
### History
| Command | Description |
|---|---|
| `wu history backfill <jid>` | Request older messages from WhatsApp |
```bash
# Backfill 50 older messages for a group
wu history backfill 120363XXX@g.us --count 50
# With JSON output
wu history backfill 120363XXX@g.us --count 20 --json
```
### Daemon
```bash
# Run as a foreground daemon — collects messages continuously
wu daemon
# Install as a systemd service (Linux)
wu daemon install
# Remove systemd service
wu daemon uninstall
# View daemon logs
wu daemon logs
```
The daemon auto-reconnects on connection drops, logs health every 5 minutes, and stores all messages to SQLite.
### Remote Sync
Run the daemon on a VPS collecting messages 24/7, query from your local machine. All communication over SSH — no exposed ports, no extra auth.
```bash
# --- On the VPS ---
npm i -g @ibrahimwithi/wu-cli
wu login
wu daemon install
# --- On your local machine ---
# Add the remote
wu remote add vps user@your-vps-ip
# Push your constraints to the server
wu remote setup vps --push
# Pull the database
wu sync pull
# Continuous sync every 30s
wu sync pull --watch --interval 30
# Install as a systemd timer (Linux)
wu sync install --interval 60
```
| Command | Description |
|---|---|
| `wu remote add <name> <host>` | Add a remote wu instance |
| `wu remote list` | List configured remotes |
| `wu remote remove <name>` | Remove a remote |
| `wu remote default <name>` | Set the default remote |
| `wu remote setup <name>` | Sync constraints between local and remote |
| `wu sync pull [name]` | Pull database from remote |
| `wu sync pull --watch` | Continuously sync on an interval |
| `wu sync install` | Install systemd timer for periodic sync |
| `wu sync uninstall` | Remove systemd sync timer |
When a remote is configured and no local daemon is running, `wu mcp` starts in **remote mode** — reads from the local synced DB, routes writes (send, react, etc.) through SSH to the VPS.
### MCP Server
```bash
# Start MCP server (stdio transport for AI agents)
wu mcp
```
Exposes WhatsApp as tools and resources for AI agents via the [Model Context Protocol](https://modelcontextprotocol.io). See [MCP setup guide](docs/mcp-setup.md) for Claude Code, Cursor, Codex CLI, and Gemini CLI configuration.
The MCP server operates in three modes:
| Mode | Condition | Reads | Writes |
|---|---|---|---|
| Full local | No daemon running, no remote | WhatsApp (live) | WhatsApp (live) |
| Read-only | Local daemon running | SQLite | Disabled |
| Remote | Remote configured + synced DB | Local SQLite | SSH to remote |
### Configuration
| Command | Description |
|---|---|
| `wu config show` | Print current config (YAML) |
| `wu config set <path> <value>` | Set a config value (dot-notation) |
| `wu config path` | Print config file path |
| `wu config allow <jid>` | Allow a chat (default: full access) |
| `wu config block <jid>` | Block a chat (drop all messages) |
| `wu config remove <jid>` | Remove a per-chat constraint |
| `wu config default [mode]` | Get/set default constraint mode |
| `wu config constraints` | Show all constraints |
### Database
| Command | Description |
|---|---|
| `wu db vacuum` | Run VACUUM and ANALYZE |
| `wu db reindex` | Rebuild the full-text search index (fixes `database disk image is malformed` on search) |
## Constraints
The constraint system controls what wu can do with each chat. Three modes:
| Mode | Collect messages | Send messages | Manage group |
|---|---|---|---|
| `full` | yes | yes | yes |
| `read` | yes | no | no |
| `none` | no | no | no |
Resolution order (most specific wins):
1. Exact JID match (`1234567890@s.whatsapp.net`)
2. Wildcard domain (`*@g.us` for all groups, `*@s.whatsapp.net` for all DMs)
3. Default constraint
4. Implicit fallback: `none`
```bash
# Set default to read-only for everything
wu config default read
# Full access for one group
wu config allow 120363XXX@g.us
# Block a specific chat
wu config block 1234567890@s.whatsapp.net
# Read-only for all groups
wu config allow '*@g.us' --mode read
```
## Configuration
Config lives at `~/.wu/config.yaml`:
```yaml
whatsapp:
read_receipts: false # Send read receipts (default: false)
media_max_mb: 50 # Max media auto-download size in MB
send_delay_ms: 1000 # Delay before sending messages (ms)
group_discovery: true # Cache group metadata even when constraint is 'none'
# (jid + name + community shape only; descriptions
# and participants stay constraint-gated). Set to
# false for strict mode.
constraints:
default: none # Default constraint mode
chats:
"*@g.us":
mode: read
"120363XXX@g.us":
mode: full
remotes:
vps:
host: user@your-vps-ip
wu_home: ~/.wu
default_remote: vps
db:
path: ~/.wu/wu.db # SQLite database path
log:
level: info # debug, info, warn, error
enrich: # Media enrichment backends (off until configured)
transcribe:
backend: local # local | api | off
local:
cmd: "whisper {input} --model base --output_format txt --output_dir {outdir}"
api: # used when backend: api
provider: openai # OpenAI-compatible audio (Groq, OpenAI, ...)
base_url: https://api.groq.com/openai/v1
key_env: GROQ_API_KEY
model: whisper-large-v3
ocr:
backend: local
local:
cmd: "tesseract {input} stdout -l ara+eng"
api:
provider: anthropic # anthropic (messages vision) | openai (chat vision)
base_url: https://api.anthropic.com/v1
key_env: ANTHROPIC_API_KEY
model: claude-haiku-4-5-20251001
```
The local `cmd` runs with `{input}` replaced by the media path; it must either print the text to stdout (e.g. tesseract) or write a `.txt` into `{outdir}` (e.g. whisper). Run `wu enrich status` to see what's detected and how to enable each backend.
All runtime data lives under `~/.wu/` (override with `WU_HOME` env var).
## JSON Output
Most commands support `--json` for machine-readable output. When piped, `wu listen` auto-detects and switches to JSON:
```bash
# Pipe messages to jq
wu listen | jq '.body'
# Export messages as JSON
wu messages list 120363XXX@g.us --json --limit 1000
```
## MCP Tools and Resources
When running `wu mcp`, the following are available to AI agents:
**Tools:** `wu_messages_send`, `wu_react`, `wu_media_download`, `wu_media_download_batch`, `wu_media_prune`, `wu_media_transcribe`, `wu_media_ocr`, `wu_enrich_status`, `wu_messages_search`, `wu_messages_list`, `wu_messages_context`, `wu_messages_count`, `wu_messages_export`, `wu_history_backfill`, `wu_chats_list`, `wu_chats_search`, `wu_dms_list`, `wu_contacts_list`, `wu_contacts_search`, `wu_groups_list`, `wu_groups_info`, `wu_groups_invite`, `wu_groups_create`, `wu_groups_leave`, `wu_groups_rename`, `wu_groups_join`, `wu_communities_list`, `wu_constraints_list`, `wu_constraints_set`, `wu_constraints_remove`, `wu_constraints_default`, `wu_config_show`, `wu_status`
`wu_messages_export` takes `download_media: true` to download the window's image/document media and write a `<output>.manifest.jsonl` mapping each item to its local file path. Add `enrich: true` to also OCR images and transcribe audio in the same pass, writing the text onto each message and into the manifest rows (`ocr_text`, `transcript`). Enrichment respects the configured backends — a disabled one is skipped and reported, never fatal.
**Resources:** `wu://chats`, `wu://chats/{jid}/messages`, `wu://contacts`, `wu://contacts/{jid}`, `wu://groups`, `wu://groups/{jid}`, `wu://status`
## Exit Codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General error |
| 2 | Constraint violation |
| 3 | Not authenticated |
| 4 | Connection failed |
| 5 | Not found |
## Notes
### Baileys pin policy
`@whiskeysockets/baileys` is pinned to an exact release candidate
(`7.0.0-rc13`) in `package.json` - no `^` or `~`. `@whiskeysockets/baileys` is
a reverse-engineered client for a protocol WhatsApp doesn't publish, so an
`-rc` build can be yanked or shift behavior between builds; an exact pin keeps
deploys deterministic instead of picking up a moving target on `npm install`.
Upgrade cadence: periodically check upstream for a GA `7.0.0` (or newer)
release and bump deliberately - never via `npm update`. Any bump must be
tested against a real WhatsApp session before merging (the characterization
tests in `tests/` can't simulate the live protocol) and followed by `npm
audit` (the `ws` advisory that rides this dependency clears on bump).
## License
MIT
Connection Info
You Might Also Like
cc-switch
All-in-One Assistant for Claude Code, Codex & Gemini CLI across platforms.
awesome-mcp-servers
A collection of MCP servers.
git
A Model Context Protocol server for Git automation and interaction.
oh-my-opencode
Background agents · Curated agents like oracle, librarians, frontend...
TrendRadar
TrendRadar: Your hotspot assistant for real news in just 30 seconds.
Appwrite
Build like a team of hundreds