Metadata-Version: 2.5
Name: wa-agent
Version: 0.2.0
Summary: A client for the WhatsApp Agent Platform: send, receive, media and transcription, as a library and a command.
Project-URL: Homepage, https://github.com/mhmzdev/whatsapp-agent-cli
Project-URL: Repository, https://github.com/mhmzdev/whatsapp-agent-cli
Project-URL: Issues, https://github.com/mhmzdev/whatsapp-agent-cli/issues
Author: Muhammad Hamza
License: MIT
License-File: LICENSE
Keywords: agent,cli,messaging,whatsapp
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Communications :: Chat
Requires-Python: >=3.10
Requires-Dist: requests>=2.32
Provides-Extra: dev
Requires-Dist: build; extra == 'dev'
Requires-Dist: twine; extra == 'dev'
Description-Content-Type: text/markdown

# whatsapp-agent-cli

**A client for the WhatsApp Agent Platform — a library first, with a command-line tool on top.**

[![PyPI](https://img.shields.io/pypi/v/wa-agent)](https://pypi.org/project/wa-agent/)
[![Python](https://img.shields.io/pypi/pyversions/wa-agent)](https://pypi.org/project/wa-agent/)
[![Tests](https://github.com/mhmzdev/whatsapp-agent-cli/actions/workflows/tests.yml/badge.svg?branch=develop)](https://github.com/mhmzdev/whatsapp-agent-cli/actions/workflows/tests.yml)
[![License](https://img.shields.io/badge/license-MIT-lightgrey)](https://github.com/mhmzdev/whatsapp-agent-cli/blob/main/LICENSE)

Send and receive WhatsApp messages from a script, a cron job, a git hook, or a coding agent with shell access. It handles the parts that are tedious to get right — the long-poll and its cursor, per-method rate limits, the 4,096-character send cap, the two-hop media fetch, and an error table where one code means "back off" and another means "this token is dead, stop".

```bash
wa-agent send "deploy finished, 3 tests failing"
```

It knows nothing about coding agents, folders, permissions or models. It moves messages.

> **First product built on it: [Hisab](https://mhmzdev.github.io/hisab/)** — a ledger that texts back. Double-entry bookkeeping for small businesses, run entirely from WhatsApp. [More below.](#built-on-it-hisab)

## Contents

- [Install](#install)
- [Get a token](#get-a-token)
- [Use it](#use-it)
- [Use it from Python](#use-it-from-python)
- [Where it keeps things](#where-it-keeps-things)
- [When something fails](#when-something-fails)
- [Built on it: Hisab](#built-on-it-hisab)
- [Coming next: the relay](#coming-next-the-relay)
- [Contributing](#contributing)
- [License](#license)

## Install

```bash
pip install wa-agent
```

The package, the command and the module are all `wa-agent` / `wa_agent`. (This repository is named `whatsapp-agent-cli`; that name and `whatsapp-agent` both belong to unrelated projects on PyPI.)

Transcription needs no extra install — only a key, because it is an ordinary HTTP call. Two providers, and the choice is always yours: `gemini` unless you say `--provider openrouter`. A key for the other one is never used in its place, however many you have exported.

```bash
export GEMINI_API_KEY='…'
wa-agent transcribe voice-note.ogg

export OPENROUTER_API_KEY='…'
wa-agent transcribe voice-note.ogg --provider openrouter    # --model takes an OpenRouter model id
```

## Get a token

In WhatsApp: **Settings → Agents → Create an agent → Chat info → API key.** An agent may only message its own creator — you — which is why there is no recipient management here.

```bash
export WHATSAPP_AGENT_TOKEN='…'        # or: wa-agent --token-file ~/.wa-token …
```

## Use it

```bash
# say something to yourself
wa-agent send "the backup finished"

# read what arrives, one JSON object per line, until you stop it
wa-agent recv --follow --json

# attach a file; the text becomes its caption
wa-agent send "this week's numbers" --file chart.png

# fetch something someone texted you, and open it
open "$(wa-agent media get <media-id>)"
```

The first `recv` records who you are, after which `send` needs no `--to`.

A voice note keeps its shape and gains the words, so code that reads `text.body` finds them and nothing about the message is lost:

```json
{"id": "wamid.A", "type": "audio", "audio": {"id": "media-1", "voice": true},
 "text": {"body": "call me back at six"}, "transcribed": true}
```

`recv --transcribe` takes the same `--provider`. Without a key for it, it warns once and delivers voice notes marked `transcribed: false` rather than stopping.

`recv --download` fetches each photo, document and voice note into the state directory as it arrives and adds a `path` to the message. It is opt-in because it puts a fetch inside the delivery loop; a download that fails is delivered marked with `download_error`, never dropped. With `--transcribe` as well, a voice note is fetched once, kept, and transcribed from that copy.

| Command | Does |
|---|---|
| `send <text>` | Send a message. Splits a long body on paragraph boundaries, numbers the parts `(i/n)`, converts markdown to WhatsApp formatting |
| `send --file <path>` | Upload and attach. `--media <id>` attaches something already uploaded |
| `send --dry-run` | Print exactly what would be sent, send nothing, need no token |
| `recv` | Messages since the last run. `--json` for one object per line, `--follow` to stream, `--typing` to show a typing indicator while you work, `--transcribe` (with `--provider`) to add words to voice notes, `--download` to keep photos and files as they arrive |
| `transcribe <file>` | Audio in, text out. Gemini or OpenRouter, chosen with `--provider`; offline is [#20](https://github.com/mhmzdev/whatsapp-agent-cli/issues/20) |
| `media get <id>` | Download to the state directory, or `--out DIR`. Prints the path and nothing else |
| `media put <path>` | Upload, print the media id |
| `doctor` | Check a setup, a line each: Python, token, each transcription key, state directory, creator. Says what to fix, exits `15` if anything fails. It never polls, so it is safe beside a running `recv`, but it does make real, free metadata requests to the platform and to every provider whose key is set in your environment |
| `errors` | The exit-code table |

Global options — `--token-file`, `--state-dir`, `--profile` — go **before** the subcommand, as in git:

```bash
wa-agent --profile work recv --follow     # yes
wa-agent recv --follow --profile work     # no: unrecognized argument
```

## Use it from Python

```python
from wa_agent import WhatsApp, Store, WhatsAppError

client = WhatsApp(token)
for sent in client.send_iter("user:123", "**done** in 40s"):
    print(sent.id, sent.text)       # one per part, as each leaves

messages, cursor = client.poll(offset=None)
for message in messages:
    print(message["from"], message.get("text", {}).get("body"))
```

`send_iter` yields each part as it is delivered, so a failure halfway never hides what already arrived. `Store` is the message log and the cursor, keyed by the platform's own message ids.

## Where it keeps things

Nothing is written into your working directory. State lives at `$XDG_STATE_HOME/wa-agent/<profile>/` (or `~/.local/state/…`), holding the poll cursor, the message log and downloaded media. `--state-dir` moves it; `--profile` keeps two agents apart.

**One poller per token.** The platform allows a single long-poll per agent and answers `409` when a second one takes the cursor, so `recv` exits rather than silently competing for your messages.

## When something fails

Every failure names its code and exits with a number a script can branch on:

```
error [platform_rejected]: the platform refused this request; retrying will not help
detail: POST /messages: HTTP 400 error.code 131009 …
```

Not sure where a setup stands? `wa-agent doctor` checks it in one go and says what to fix.

`wa-agent errors` lists them all. [`docs/errors.md`](https://github.com/mhmzdev/whatsapp-agent-cli/blob/main/docs/errors.md) says what to do about each and which are worth retrying — the short version is that exit `7` is, and `4` and `6` never are.

## Built on it: Hisab

[![Hisab — a ledger that texts back](https://raw.githubusercontent.com/mhmzdev/hisab-whatsapp/main/showcase/hisab-cover.png)](https://mhmzdev.github.io/hisab/)

[**Hisab**](https://mhmzdev.github.io/hisab/) is a plain-language ledger you keep by texting WhatsApp — *"2500 coffee"* posts an entry, *"how much do I owe Metro?"* gets an answer — in English, Urdu or Roman Urdu, by voice, photo or text, with every entry checked by `hledger` before it is written.

It is where this package came from. The cursor that only advances after a batch, the dedup, the per-method rate limits and the dead-token exit were all learned running Hisab against real messages, then extracted here so nothing else has to learn them again. Hisab is the first product on this transport, and moves onto the published `wa-agent` package next.

The two repositories split the work cleanly:

| | [whatsapp-agent-cli](https://github.com/mhmzdev/whatsapp-agent-cli) (this) | [hisab-whatsapp](https://github.com/mhmzdev/hisab-whatsapp) |
|---|---|---|
| Is | the transport: messages, media, transcription | a product: a ledger with a model and six tools |
| Knows about | tokens, cursors, rate limits | accounts, entries, `hledger` |
| You use it | from a script, a cron job, or your own agent | by texting it |

## Coming next: the relay

The transport moves messages. The relay is what makes it an agent in your pocket.

```bash
wa-agent relay --folder ~/code/my-project     # coming soon
```

Text it from your phone — *"why is the deploy failing?"*, a screenshot of an error, a voice note describing a bug — and it runs a coding agent such as **Claude Code** over that folder and sends back what it says. The design is settled; the code starts once this release is out:

- **Read-only by default.** The agent can read the folder and nothing else. Writable paths are declared, never assumed, and a folder created later is denied until you say otherwise.
- **The relay owns the session.** Starting fresh, switching models and compacting a long conversation happen in the relay, before the agent is called, because none of them survive a non-interactive run otherwise.
- **Everything it hears, it can use.** Voice notes arrive as words, photos and files arrive by path, and a quoted reply arrives with the message it quoted — all from this package, underneath.
- **One command in this package, optional.** `pip install wa-agent` never makes you run it. The transport stays usable on its own, and the relay uses it exactly as your own scripts would.

Claude Code comes first, Codex after. Follow along on the [issues](https://github.com/mhmzdev/whatsapp-agent-cli/issues).

## Contributing

```bash
cp .env.example .env    # fill in your agent token, and a transcription key if you want it
make dev                # a virtualenv with this checkout installed
make check              # the check: no network, no token, a couple of seconds
make up                 # a live inbox: text your agent and watch it land, until Ctrl-C
make live               # a scripted round trip: send, wait for your reply, read it back
```

`make up` and `make live` keep their state in `.live-state/`, never your real one, and `make clean` removes it. `develop` is the trunk and PRs target it; `main` is what is published. Conventions live in [`AGENTS.md`](https://github.com/mhmzdev/whatsapp-agent-cli/blob/main/AGENTS.md).

## License

MIT.
