From e3a2d8693e942d90d2c304e8efe51b912b7ecc77 Mon Sep 17 00:00:00 2001 From: Vicente Reig Rincon de Arellano Date: Sat, 8 Aug 2026 17:57:15 +0200 Subject: [PATCH 1/2] docs: tighten prose with economical writing + Simple English, add AGENTS.md and CONTRIBUTING.md Rewrite README, QUICKSTART, SUMMARY, docs/RELEASE, docs/architecture, and CHANGELOG prose for concision and clarity (McCloskey's Economical Writing principles + ASD-STE100 Simplified Technical English). Code blocks, flags, tables, and commands are unchanged. Add AGENTS.md: build/test commands, code layout, and testing conventions, verified against the actual source and CI config. Add CONTRIBUTING.md, informed by patterns across issues #2, #3, #4, #5, #6, #12, #13, #15, #17, #19, #20, #25: recurring known issues (405 auth failures, single-writer store locking, auth-then-sync-immediately), the issue-first-then-PR process, and what the project will and won't build for free. Co-Authored-By: Claude Sonnet 5 --- AGENTS.md | 112 +++++++++++++++++++++++++++++++++++ CHANGELOG.md | 2 +- CONTRIBUTING.md | 65 ++++++++++++++++++++ QUICKSTART.md | 12 ++-- README.md | 138 +++++++++++++++++++++---------------------- SUMMARY.md | 26 ++++---- docs/RELEASE.md | 46 +++++++-------- docs/architecture.md | 68 ++++++++++----------- 8 files changed, 323 insertions(+), 146 deletions(-) create mode 100644 AGENTS.md create mode 100644 CONTRIBUTING.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..05923d8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,112 @@ +# AGENTS.md + +WhatsApp CLI is a single Go binary. It reads, searches, and sends WhatsApp +messages from the command line. It talks to WhatsApp through the `whatsmeow` +library, not through an official WhatsApp API. + +## Build, run, test + +```bash +go mod download # install dependencies +go build -o whatsapp-cli . # build the binary +go test ./... # run all tests (the command CI runs) +go test -race ./... # run tests with the race detector +go test -cover ./... # run tests with coverage +go test ./internal/store # run one package +``` + +If the sandbox cannot write to the global Go module cache, point +`GOMODCACHE` at a folder inside the repo: + +```bash +GOMODCACHE=$(pwd)/.gomodcache go build ./... +``` + +To embed a version string at build time, use `-ldflags`: + +```bash +go build -ldflags "-X main.version=X.Y.Z" -o whatsapp-cli . +``` + +Run the binary like this: + +```bash +./whatsapp-cli auth +./whatsapp-cli sync +./whatsapp-cli --store /custom/path chats list +``` + +CI (`.github/workflows/ci.yml`) runs only `go test ./...`, on every push and +pull request. There is no linter config and no Makefile in this repo. + +## Code style + +- Package layout: `internal/client` wraps `whatsmeow`. `internal/store` + wraps SQLite. `internal/commands` holds the `App` type, with one method + per CLI command. `internal/output` formats the JSON output. + `internal/types` holds types shared between `client` and `commands`, to + avoid an import cycle. +- Define interfaces at the consumer, not at the implementation. + `internal/commands/interfaces.go` defines the `WAClient` and + `MessageStore` interfaces. The concrete types live in `internal/client` + and `internal/store`. +- Give each component two constructors. `NewApp()` builds it with real + dependencies. `NewAppWithDeps()` takes injected mocks, for tests. +- Wrap errors with context: `fmt.Errorf("failed to X: %v", err)`. +- Command methods on `App` (`Auth`, `Sync`, `ListMessages`, `SendMessage`, + and so on) return a JSON string, not a Go `error`. Each method calls + `output.Success(data)` or `output.Error(err)` at its boundary. +- Every CLI command must print this JSON shape to stdout: + ```json + {"success": true, "data": ..., "error": null} + ``` +- Go's `flag` package stops parsing at the first non-flag argument. For a + subcommand like `messages list --chat X`, parse flags from `args[2:]`, + skipping both the command and the subcommand name. See `main.go` and the + regression tests in `main_test.go` (GitHub issue #5). + +## Architecture + +See `docs/architecture.md` for the full design. In short: `main.go` parses +flags and calls into `internal/commands.App`. `App` holds a `WAClient` +(talks to WhatsApp through `whatsmeow`) and a `MessageStore` (reads and +writes SQLite). Session data lives in `store/whatsapp.db`. Message history +lives in `store/messages.db`. Downloaded media lives under `store/media/`. + +## Testing + +- Tests use `testify/require`. There is no VCR setup and no mocking + framework. Mocks are hand-written structs in `*_test.go` files, for + example `internal/commands/mocks_test.go`, that implement the `WAClient` + and `MessageStore` interfaces. +- Tests run against these mocks. They never open a real WhatsApp + connection. +- Name regression tests after the bug they fix. `main_test.go` and + `internal/commands/behavioral_test.go` name the GitHub issue or pull + request in the test name and comments. Follow this pattern for new + regression tests. +- `testdata/` exists but is empty. It is gitignored, so this repo does not + currently ship fixture files. + +## Constraints + +- `whatsmeow` uses the unofficial WhatsApp Web protocol, not a + WhatsApp-published API. A protocol change on WhatsApp's side can break + the client without warning. Commit `bf602c5` fixed one such break (a + "Client outdated (405)" auth error). Check `CHANGELOG.md` for the + history. +- The session lives in `store/whatsapp.db`, created by scanning a QR code. + It normally lasts about 20 days. +- `store/` holds the session and the full message history. Never commit + it. `.gitignore` already excludes `/store/`. +- The `go-sqlite3` driver needs CGO. Cross-compiling for another platform + needs `CGO_ENABLED=1` and, for Linux ARM64, a cross-compiler. See + `.github/workflows/release.yml` for the exact build matrix. + +## More docs + +- `README.md` — full command reference, FAQ, troubleshooting +- `docs/architecture.md` — design and data flow +- `docs/RELEASE.md` — how to cut and publish a release +- `QUICKSTART.md` — five-minute setup +- `CHANGELOG.md` — release history diff --git a/CHANGELOG.md b/CHANGELOG.md index d1ba21c..0865883 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ All notable changes to this project will be documented in this file. ### Improved -- Added dependency injection and sturdier CLI command wiring to reduce runtime brittleness. +- Added dependency injection and more reliable CLI command wiring to reduce runtime failures. - Added image sending support to `send`. - Added standard CI on pushes and pull requests so test coverage runs outside release tags. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..aa0aa58 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,65 @@ +# Contributing + +Thanks for your interest in whatsapp-cli. This project is maintained by one person in spare time. PRs are welcome and get reviewed. Feature requests without a PR wait until a contributor picks them up. + +## Before you file an issue + +Check the "Known issues" section below first. Your problem might already have a fix. + +If it doesn't, include: + +- The CLI version (`whatsapp-cli --version`). +- Your OS and architecture (e.g. macOS ARM64, Linux amd64). +- The exact command you ran. +- The full error output, including any log lines above the error. + +Issue [#15](https://github.com/vicentereig/whatsapp-cli/issues/15) is a good model: it states the bug, gives exact repro steps, shows database evidence, and proposes a fix. Reports at that level get fixed fast. + +## Known issues + +### "Client outdated (405)" auth failures + +This is the most common report ([#2](https://github.com/vicentereig/whatsapp-cli/issues/2), [#13](https://github.com/vicentereig/whatsapp-cli/issues/13), [#25](https://github.com/vicentereig/whatsapp-cli/issues/25)). WhatsApp periodically rotates the web client version it accepts. When that happens, the server sends `ConnectFailureClientOutdated (405)` and closes the connection. + +The fix: bump the `whatsmeow` dependency and cut a new release. This recurs every few months — it is not a bug in this CLI. See the README's Connection Issues section for the exact steps. A PR that bumps `whatsmeow` and updates `go.sum` is always welcome when this happens; you don't need to ask first. + +### "database is locked" during sync + +The local store allows one writer at a time ([#19](https://github.com/vicentereig/whatsapp-cli/issues/19)). Don't run `sync` in two terminals at once, and don't run another store-writing command (like a history backfill) while `sync` is active. Doing so can corrupt the session, not just error out ([#20](https://github.com/vicentereig/whatsapp-cli/issues/20)). + +### Auth succeeds but sync then fails or stalls + +Run `sync` right after `auth` reports success. Waiting between the two steps has caused connection failures for some users ([#12](https://github.com/vicentereig/whatsapp-cli/issues/12)). + +### `chats list` / `messages list` show only 20 results + +That's the default `--limit`, not a bug ([#6](https://github.com/vicentereig/whatsapp-cli/issues/6), [#20](https://github.com/vicentereig/whatsapp-cli/issues/20)). Pass a higher `--limit` for scripts that expect the full store. + +## Proposing a feature + +1. Open an issue first. State the problem, then propose a design. Note any prior art (other projects that solved the same problem) if you know of any. +2. Wait for a green light on the design before you write the PR. +3. Send the PR. Mention any other open issue or PR your change might overlap with; the maintainer will coordinate a merge order or close the duplicate. + +Issue [#20](https://github.com/vicentereig/whatsapp-cli/issues/20) is a model feature proposal: it states the problem, proposes a concrete CLI shape, cites prior art from other WhatsApp clients, links a working proof-of-concept branch, and lists caveats up front. Proposals at that level get accepted fast. + +## Using AI coding agents + +Agent-assisted PRs (Claude, Codex, or similar) are welcome. Point your agent at [AGENTS.md](./AGENTS.md) for build, test, and style conventions before it starts. + +## What this project will and won't do for you + +- PRs get reviewed and merged. +- Feature requests without a PR sit in the backlog until someone contributes one. +- Paid consulting is available if you want a feature built for you rather than contributing it yourself. Contact hey@vicente.services. + +## Development setup + +```bash +git clone https://github.com/vicentereig/whatsapp-cli.git +cd whatsapp-cli +go build +go test ./... +``` + +CI runs `go test ./...` on every push and pull request. Make sure it passes locally before you open a PR. diff --git a/QUICKSTART.md b/QUICKSTART.md index 43e1805..1f1ac35 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -70,16 +70,16 @@ if (result.success) { ## Storage -Session and messages stored in `./store/`: -- `whatsapp.db` - WhatsApp session (don't share!) +The CLI stores the session and messages in `./store/`: +- `whatsapp.db` - WhatsApp session (keep this file private) - `messages.db` - Message history ## Tips -- **Authentication lasts ~20 days** - no need to scan QR every time -- **Phone numbers**: Can use just the number (e.g., `1234567890`) for individuals -- **Groups**: Must use full JID (e.g., `123456789@g.us`) - get from chats list -- **JSON parsing**: All commands return JSON - use `jq` or parse programmatically +- **Authentication lasts about 20 days.** You do not need to scan the QR code each time. +- **Phone numbers**: For an individual contact, you can use the number alone, for example `1234567890`. +- **Groups**: You must use the full JID, for example `123456789@g.us`. Get the JID from the chat list. +- **JSON parsing**: Every command returns JSON. Use `jq`, or parse the output in your own code. ## Next Steps diff --git a/README.md b/README.md index 48036d9..19eab5b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # WhatsApp CLI - Complete Reference -> **For Humans & LLMs**: This document contains comprehensive information about the WhatsApp CLI tool, including installation, usage, API reference, examples, architecture, and troubleshooting. It is designed to be parsed by both humans and large language models. +> **For Humans & LLMs**: This document covers the WhatsApp CLI tool: installation, usage, the API reference, examples, architecture, and troubleshooting. Humans and large language models can both read it. **Version**: 1.3.2 **Repository**: https://github.com/vicentereig/whatsapp-cli @@ -34,7 +34,7 @@ ### What is WhatsApp CLI? -A standalone command-line interface for WhatsApp built on the WhatsApp Web multidevice protocol. Every command returns structured JSON output, making it ideal for: +WhatsApp CLI is a standalone command-line tool for WhatsApp. It uses the WhatsApp Web multidevice protocol. Every command returns structured JSON output. Use it for: - **Automation**: Shell scripts, cron jobs, CI/CD pipelines - **AI Integration**: Codex, Claude Code, GPT-based tools @@ -128,12 +128,12 @@ go install github.com/vicentereig/whatsapp-cli@latest ### Release & Distribution Notes -- Version tags use semantic versioning (`vMAJOR.MINOR.PATCH`). Use a specific tag (e.g., `v1.0.0`) with `go install github.com/vicentereig/whatsapp-cli@v1.0.0` for reproducible builds. -- Pre-built artifacts for Linux/macOS/Windows are published on the [GitHub Releases](https://github.com/vicentereig/whatsapp-cli/releases) page. Each archive is paired with SHA-256 entries inside `checksums.txt`; run `shasum -a 256 -c checksums.txt --ignore-missing` to verify before installing. -- Binaries are named `whatsapp-cli--` (Windows adds `.exe`). After extraction, mark them executable (`chmod +x`) and place them somewhere in your `PATH` such as `/usr/local/bin/whatsapp-cli`. -- Each uploaded file (including `checksums.txt`) also has Sigstore cosign signatures (`.sig`) and certificates (`.pem`). To verify, run `cosign verify-blob --certificate .pem --signature .sig `; GitHub Actions uses OIDC identities, so you can enforce provenance on verification. -- To build from source, follow Method 3. That path is ideal when you want to audit the code, tweak compilation flags, or test changes before tagging a release. -- Maintainers can follow `docs/RELEASE.md` for the step-by-step process that drives `go install`, source builds, and GitHub Release automation. +- Version tags use semantic versioning (`vMAJOR.MINOR.PATCH`). For reproducible builds, use a specific tag, for example `v1.0.0`, with `go install github.com/vicentereig/whatsapp-cli@v1.0.0`. +- The [GitHub Releases](https://github.com/vicentereig/whatsapp-cli/releases) page has pre-built artifacts for Linux, macOS, and Windows. Each archive has a matching SHA-256 entry in `checksums.txt`. Before you install, run `shasum -a 256 -c checksums.txt --ignore-missing` to verify the archive. +- Binaries use the name `whatsapp-cli--` (Windows adds `.exe`). After you extract the file, mark it executable with `chmod +x` and place it in your `PATH`, for example `/usr/local/bin/whatsapp-cli`. +- Each uploaded file, including `checksums.txt`, also has a Sigstore cosign signature (`.sig`) and certificate (`.pem`). To verify a file, run `cosign verify-blob --certificate .pem --signature .sig `. GitHub Actions uses OIDC identities, so you can enforce provenance during verification. +- To build from source, follow Method 3. Use this method to audit the code, change compilation flags, or test changes before you tag a release. +- Maintainers can follow the steps in `docs/RELEASE.md`. It covers `go install`, source builds, and GitHub Release automation. --- @@ -167,7 +167,7 @@ whatsapp-cli auth ### Step 2: Sync Messages -Before you can list or search messages, you need to sync them from WhatsApp: +Before you list or search messages, sync them from WhatsApp: ```bash # Start syncing messages (run this in the background or a separate terminal) @@ -182,7 +182,7 @@ whatsapp-cli sync 4. Stores everything in `./store/messages.db` 5. Runs until you press Ctrl+C -**Tip**: Run sync in a tmux/screen session or as a background service to continuously receive messages. +**Tip**: Run sync in a tmux or screen session, or as a background service, so it keeps receiving messages. ### Step 3: Basic Operations @@ -277,12 +277,12 @@ whatsapp-cli auth ### Command: `sync` -**⚠️ IMPORTANT**: This command must be run to populate the message database. Without running sync, `messages list` and `messages search` will return empty results. +**⚠️ IMPORTANT**: Run this command to fill the message database. If you do not run sync, `messages list` and `messages search` return empty results. -Continuously sync messages from WhatsApp to local database. This command: +Sync messages from WhatsApp to the local database continuously. This command: - Downloads message history from WhatsApp servers -- Receives new incoming messages in real-time -- Stores all messages in SQLite database +- Receives new messages in real time +- Stores all messages in the SQLite database - Runs until you press Ctrl+C **Syntax:** @@ -310,9 +310,9 @@ whatsapp-cli sync - Processes `*events.Message` for real-time messages - Processes `*events.HistorySync` for message history batches - Stores all messages in `store/messages.db` -- Updates progress to stderr (doesn't interfere with JSON output) -- Runs indefinitely until interrupted (Ctrl+C) -- Gracefully disconnects on exit +- Sends progress to stderr (does not affect JSON output) +- Runs until interrupted (Ctrl+C) +- Disconnects on exit **Progress Output (stderr):** ``` @@ -350,15 +350,15 @@ kill -INT **Use Cases:** 1. **Initial Setup**: Run once to download all message history 2. **Continuous Sync**: Run as background service to receive messages -3. **Periodic Sync**: Run via cron to update messages periodically +3. **Periodic Sync**: Run via cron to update messages on a schedule 4. **Development**: Run in terminal while testing queries **Notes:** -- Message history sync may take time depending on message count -- Duplicate messages are handled by SQLite PRIMARY KEY constraints -- Media files are NOT downloaded, only metadata (type, filename, URL) -- Connection stays alive indefinitely until interrupted -- Safe to restart - won't duplicate messages +- Message history sync can take time. The duration depends on the number of messages. +- SQLite PRIMARY KEY constraints prevent duplicate messages. +- The CLI does not download media files. It stores only metadata: type, filename, and URL. +- The connection stays open until you interrupt it. +- You can restart the sync safely. It does not create duplicate messages. --- @@ -631,7 +631,7 @@ whatsapp-cli send --to 1234567890 --message "Server status: $(uptime)" - Supports Unicode (emojis, international characters) **Limitations:** -- Send command currently supports text only (download attachments via `media download`) +- The `send` command supports text messages only. To download attachments, use `media download`. - No delivery/read receipt information returned - Maximum message length: WhatsApp's standard limit (~65,536 characters) @@ -639,7 +639,7 @@ whatsapp-cli send --to 1234567890 --message "Server status: $(uptime)" ### Command: `media download` -Download media attachments (images, videos, audio, documents) that were synced into the local database. +Download media attachments (images, videos, audio, documents) that sync stored in the local database. **Syntax:** ```bash @@ -655,9 +655,9 @@ whatsapp-cli media download --message-id ID [--chat JID] [--output PATH] | `--output` | string | No | Destination file or directory (defaults to auto-structured path) | **Default storage:** -- Media is stored next to the SQLite databases under `STORE/media/{chat}/{message}/{media_type}/filename` -- Paths are sanitized automatically -- If `--output` points to a directory, the original filename (or message ID-based fallback) is used +- The CLI stores media next to the SQLite databases, under `STORE/media/{chat}/{message}/{media_type}/filename` +- The CLI sanitizes paths automatically +- If `--output` points to a directory, the CLI uses the original filename (or a name based on the message ID) **Return value:** ```json @@ -689,16 +689,16 @@ whatsapp-cli media download --message-id XYZ987 --chat 1234567890@s.whatsapp.net ``` **Notes:** -- Requires that `whatsapp-cli sync` has captured the message metadata -- The sync loop downloads media concurrently in the background without blocking new messages -- Re-running the command overwrites the existing file with a fresh download -- Errors include metadata issues (expired link, missing direct path) or filesystem permissions +- Requires that `whatsapp-cli sync` captured the message metadata first +- The sync loop downloads media in the background, without blocking new messages +- If you run the command again, it overwrites the existing file with a fresh download +- Errors include metadata issues (an expired link or a missing direct path) or file permission problems --- ## JSON Response Format -All commands return JSON in this standardized format: +All commands return JSON in this format: ### Success Response @@ -1020,7 +1020,7 @@ interface Chat { ### JID (Jabber ID) Format -WhatsApp uses JIDs to uniquely identify chats: +WhatsApp uses JIDs to identify chats: | Type | Format | Example | |------|--------|---------| @@ -1065,7 +1065,7 @@ whatsapp-cli --store /var/lib/whatsapp chats list - Contact information - Session tokens - **Persistence**: ~20 days before re-authentication required -- **Security**: Contains sensitive data, protect with file permissions +- **Security**: Contains sensitive data. Protect it with file permissions. **Recommended Permissions:** ```bash @@ -1267,7 +1267,7 @@ go build -ldflags="-s -w" . # ~15MB upx --best --lzma whatsapp-cli # ~5MB ``` -For detailed technical architecture, see [docs/architecture.md](docs/architecture.md). +For more about the architecture, see [docs/architecture.md](docs/architecture.md). --- @@ -1306,7 +1306,7 @@ For detailed technical architecture, see [docs/architecture.md](docs/architectur "error": "invalid JID format" } ``` -**Solution:** Ensure JID format is correct (`phone@s.whatsapp.net` or `id@g.us`) +**Solution:** Use the correct JID format (`phone@s.whatsapp.net` or `id@g.us`) #### Database Locked ```json @@ -1331,7 +1331,7 @@ For detailed technical architecture, see [docs/architecture.md](docs/architectur ### Authentication Issues -**Problem**: QR code doesn't appear +**Problem**: QR code does not appear ```bash # Check terminal supports UTF-8 @@ -1346,7 +1346,7 @@ QRSIZE=small whatsapp-cli auth - WhatsApp sessions expire after ~20 days - Re-run `whatsapp-cli auth` -- Sessions also expire if you unlink device from phone +- Sessions also expire if you unlink the device from the phone ### Connection Issues @@ -1365,13 +1365,13 @@ whatsapp-cli --store /tmp/wa-test auth **Problem**: "WebSocket connection failed" -- Corporate firewalls may block WSS -- VPN may interfere with connection -- Try from different network +- Corporate firewalls can block WSS +- A VPN can interfere with the connection +- Try a different network ### WhatsApp Web Version Bumps (405 / close 1006) -WhatsApp occasionally requires a newer desktop/web build. When that happens, the bundled `whatsmeow` dependency logs `Client outdated (405)` and immediately closes the websocket with code `1006`, so `whatsapp-cli` dies right after scanning the QR. +WhatsApp sometimes requires a newer desktop or web build. When that happens, the bundled `whatsmeow` dependency logs `Client outdated (405)`. It then closes the websocket with code `1006`. As a result, `whatsapp-cli` stops right after you scan the QR code. To fix it: @@ -1384,9 +1384,9 @@ To fix it: ```bash go build -o whatsapp-cli . ``` -3. **Verify:** Run `./whatsapp-cli version` followed by `./whatsapp-cli auth` or `./whatsapp-cli sync`. You should see `✓ Connected to WhatsApp` without any `Client outdated (405)` messages. +3. **Verify:** Run `./whatsapp-cli version`, then `./whatsapp-cli auth` or `./whatsapp-cli sync`. You will see `✓ Connected to WhatsApp` with no `Client outdated (405)` messages. -If the upstream library has not yet released a new version, monitor `tulir/whatsmeow` issues for `Client outdated (405)` reports; once a fix lands, repeat the steps above to update your local binary. +If the upstream library does not yet have a new version, watch the `tulir/whatsmeow` issues for `Client outdated (405)` reports. Once a fix lands, repeat the steps above to update your local binary. ### Database Issues @@ -1421,14 +1421,14 @@ sqlite3 store/messages.db "DELETE FROM messages WHERE timestamp < datetime('now' **Problem**: Messages not appearing in searches -- CLI only searches messages stored locally -- Messages received before CLI was running are not stored -- Re-sync by listing messages from specific chats +- The CLI searches only messages stored locally +- Messages received before the CLI was running are not stored +- To re-sync, list messages from specific chats -**Problem**: Can't send to group +**Problem**: Cannot send to a group -- Must use group JID (ending in `@g.us`), not phone number -- Get group JID from `chats list` command +- Use the group JID (it ends in `@g.us`), not a phone number +- Get the group JID from the `chats list` command ### Performance Issues @@ -1480,13 +1480,13 @@ go build -o whatsapp-cli-dev . ### Building with a Local Module Cache -If your environment can't write to the global Go module cache (common in sandboxes or CI), point `GOMODCACHE` to a writable directory within the repo before building: +Some environments, for example sandboxes or CI systems, cannot write to the global Go module cache. If your environment has this limit, point `GOMODCACHE` to a writable directory in the repository before you build: ```bash GOMODCACHE=$(pwd)/.gomodcache go build ./... ``` -This command compiles the full project and stores downloaded modules under `.gomodcache`, keeping the workspace self-contained. +This command compiles the full project and stores downloaded modules under `.gomodcache`. The workspace then stays self-contained. ### Running Tests @@ -1557,9 +1557,9 @@ func TestNewCommand(t *testing.T) { Follow TDD (Test-Driven Development): 1. **Write test first** -2. **Run test (should fail)** +2. **Run test (it fails)** 3. **Implement feature** -4. **Run test (should pass)** +4. **Run test (it passes)** 5. **Refactor** **Example:** @@ -1584,52 +1584,52 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. ### General **Q: Is this an official WhatsApp tool?** -A: No, this is a third-party tool using the unofficial WhatsApp Web protocol via whatsmeow. +A: No. This is a third-party tool. It uses the unofficial WhatsApp Web protocol, through the whatsmeow library. **Q: Can I use this with WhatsApp Business?** -A: Yes, works with both personal and business accounts. +A: Yes. It works with personal and business accounts. **Q: Does this work on multiple devices simultaneously?** -A: Yes, WhatsApp supports up to 5 linked devices. +A: Yes. WhatsApp supports up to 5 linked devices. ### Features **Q: Can I send images/videos?** -A: Not yet. Currently text-only. Media support planned for future release. +A: Not yet. The CLI currently sends text only. Media support is planned for a future release. **Q: Can I create/manage groups?** -A: Not yet. Read-only access to groups currently. +A: Not yet. The CLI currently gives read-only access to groups. **Q: Can I see delivery/read receipts?** -A: Not in current version. Feature planned for future release. +A: Not in the current version. This feature is planned for a future release. **Q: Can I receive real-time messages?** -A: Yes! Run `whatsapp-cli sync` to continuously receive and store messages. Run it in the background or tmux session. +A: Yes. Run `whatsapp-cli sync` to receive and store messages continuously. Run it in the background or in a tmux session. ### Technical **Q: Why Go instead of Python/Node.js?** -A: Go provides single binary distribution, better performance, and whatsmeow is written in Go. +A: Go gives single-binary distribution and strong performance. Also, whatsmeow is written in Go. **Q: Can I run this in Docker?** -A: Yes, but QR code authentication requires terminal access. Use `-it` flags for interactive mode. +A: Yes, but QR code authentication needs terminal access. Use the `-it` flags for interactive mode. **Q: Does it support proxy/VPN?** -A: Respects system proxy settings. Set `HTTP_PROXY` and `HTTPS_PROXY` environment variables. +A: The CLI respects system proxy settings. Set the `HTTP_PROXY` and `HTTPS_PROXY` environment variables. **Q: Can I access messages from before installing CLI?** -A: Yes! When you run `whatsapp-cli sync`, it downloads message history from WhatsApp servers. The amount of history depends on WhatsApp's server-side retention. +A: Yes. When you run `whatsapp-cli sync`, it downloads message history from WhatsApp servers. The amount of history depends on how long WhatsApp retains it on its servers. ### Privacy & Security **Q: Is my data safe?** -A: All data stored locally. No cloud sync. Protect your `store/` directory. +A: The CLI stores all data locally, with no cloud sync. Protect your `store/` directory. **Q: Can WhatsApp detect/ban this?** -A: Uses official WhatsApp Web protocol. Risk is minimal but use at own discretion. +A: The CLI uses the official WhatsApp Web protocol. The risk is low, but use it at your own discretion. **Q: Is end-to-end encryption maintained?** -A: Yes, all messages are E2E encrypted using WhatsApp's protocol. +A: Yes. WhatsApp's protocol encrypts all messages end-to-end. --- diff --git a/SUMMARY.md b/SUMMARY.md index f677ea7..e87781c 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -2,7 +2,7 @@ ## ✅ Completed -Built a complete standalone WhatsApp CLI tool in one session using TDD principles. +This project built a standalone WhatsApp CLI tool in one session, using test-driven development (TDD). ### Project Stats - **Language**: Go 1.24 @@ -65,7 +65,7 @@ Built a complete standalone WhatsApp CLI tool in one session using TDD principle ### Documentation -- [x] README.md with full usage guide +- [x] README.md with usage guide - [x] QUICKSTART.md with examples - [x] docs/architecture.md (technical design) - [x] Inline code comments @@ -74,12 +74,12 @@ Built a complete standalone WhatsApp CLI tool in one session using TDD principle ### Repository - [x] Initialized git -- [x] Created meaningful commits +- [x] Created commits - [x] Pushed to github.com:vicentereig/whatsapp-cli ## Testing -All tests passing: +All tests pass: ``` internal/output: 3/3 tests passed internal/store: 6/6 tests passed @@ -109,21 +109,21 @@ go build -o whatsapp-cli . **Single Binary**: - No runtime dependencies -- Just compile and run +- Compile and run **JSON Output**: -- Perfect for scripts and AI tools -- Easy to parse with `jq` or programmatically +- Works for scripts and AI tools +- Parse it with `jq`, or parse it in your own code **Local Storage**: - SQLite for sessions and messages - No cloud dependencies -- Full privacy +- Data stays on your device **TDD Approach**: - Tests written first - Minimal code to pass tests -- Clean, focused implementation +- Implementation limited to what the tests require ## Next Steps (Future Enhancements) @@ -139,7 +139,7 @@ Potential additions: ## Time to Build -Completed in single session with TDD approach: +Completed in a single session, using TDD: - Setup & dependencies: ~5 min - Output layer (TDD): ~5 min - Store layer (TDD): ~10 min @@ -148,10 +148,10 @@ Completed in single session with TDD approach: - Documentation: ~10 min - Git & push: ~5 min -**Total: ~55 minutes** for a fully functional WhatsApp CLI tool! +**Total: ~55 minutes** to build a working WhatsApp CLI tool. ## Credits - Built with [whatsmeow](https://github.com/tulir/whatsmeow) -- Developed using Claude Code (Sonnet 4.5) -- TDD methodology +- Built with Claude Code (Sonnet 4.5) +- Built with TDD diff --git a/docs/RELEASE.md b/docs/RELEASE.md index 388ad28..5a817a1 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -1,19 +1,19 @@ # Release Guide (v1.0.0 and onward) -This guide describes how to cut a tagged release that supports `go install`, pre-built binaries, and source builds for WhatsApp CLI. +This guide explains how to cut a tagged release. A release supports `go install`, pre-built binaries, and source builds for WhatsApp CLI. ## Semantic Versioning -- Use tags that follow `vMAJOR.MINOR.PATCH` (e.g., `v1.0.0`, `v1.0.1`). -- Stick to semver rules: increment PATCH for bug fixes, MINOR for backward-compatible features, MAJOR for breaking changes. -- `go install github.com/vicentereig/whatsapp-cli@v1.0.0` resolves to that tag automatically, so never retag an existing version. +- Use tags that follow `vMAJOR.MINOR.PATCH`, for example `v1.0.0` or `v1.0.1`. +- Follow semver rules: increment PATCH for bug fixes, MINOR for backward-compatible features, MAJOR for breaking changes. +- The command `go install github.com/vicentereig/whatsapp-cli@v1.0.0` resolves to that tag automatically. Never retag an existing version. ## Pre-Release Checklist -1. Ensure the repo is clean and tests pass: `go test ./...`. +1. Make sure that the repo is clean and tests pass: `go test ./...`. 2. Run `go mod tidy` so dependency metadata matches the source. -3. Update documentation (README, QUICKSTART, etc.) with the new version and noteworthy changes. -4. Review the changelog/summary for the release notes. +3. Update the documentation (README, QUICKSTART, and other files) with the new version and notable changes. +4. Review the changelog for the release notes. 5. Commit all changes before tagging. ## Tagging and Pushing @@ -23,35 +23,35 @@ git tag -a v1.0.0 -m "v1.0.0" git push origin v1.0.0 ``` -Replace `v1.0.0` with the appropriate version number. Pushing the tag triggers the release workflow. +Replace `v1.0.0` with the correct version number. When you push the tag, the release workflow starts. ## GitHub Release Automation The workflow in `.github/workflows/release.yml` runs automatically for tags that match `v*`: -1. Executes `go test ./...` on Ubuntu to ensure the tag is healthy. +1. Runs `go test ./...` on Ubuntu to check that the tag is healthy. 2. Builds CGO-enabled binaries for: - Linux: `amd64`, `arm64` - macOS: `amd64`, `arm64` - Windows: `amd64` -3. Embeds the tag (e.g., `v1.0.0`) into the binary via Go ldflags and smoke-tests each artifact by running `whatsapp-cli version` (QEMU is used where native execution is not possible). +3. Embeds the tag, for example `v1.0.0`, into the binary using Go ldflags. Then it smoke-tests each artifact by running `whatsapp-cli version`. It uses QEMU where native execution is not possible. 4. Packages binaries as: - Linux/macOS: `whatsapp-cli--.tar.gz` - Windows: `whatsapp-cli-windows-amd64.zip` -5. Generates SHA-256 checksum files for each archive and merges them into `checksums.txt`. -6. Signs every uploaded file (tarballs/zips/checksums) with Sigstore cosign using GitHub’s OIDC identity, producing `.sig` and `.pem` companions. -7. Publishes the artifacts, checksums, and cosign metadata to the GitHub Release associated with the tag. +5. Generates a SHA-256 checksum file for each archive and merges the files into `checksums.txt`. +6. Signs every uploaded file — tarballs, zip files, and checksums — with Sigstore cosign, using GitHub's OIDC identity. This produces `.sig` and `.pem` companion files. +7. Publishes the artifacts, checksums, and cosign metadata to the GitHub Release for the tag. ## Pre-Built Binary Distribution After the workflow finishes: 1. Visit the GitHub Release page for the tag. -2. Verify that the tarballs/zip and `checksums.txt` are attached. -3. Add human-friendly release notes (highlights, breaking changes, upgrade instructions). -4. In documentation, reference the release download URLs. +2. Make sure that the tar files, the zip file, and `checksums.txt` are attached. +3. Add clear release notes (highlights, breaking changes, upgrade instructions). +4. In the documentation, link to the release download URLs. -Users should: +To install the release, users run these commands: ```bash # Example for Linux amd64 @@ -63,7 +63,7 @@ chmod +x whatsapp-cli-linux-amd64 sudo mv whatsapp-cli-linux-amd64 /usr/local/bin/whatsapp-cli ``` -To validate provenance with Sigstore: +To check provenance with Sigstore, run: ```bash cosign verify-blob \ @@ -80,11 +80,11 @@ Expected identity: ## Supporting `go install` - Once the tag exists, `go install github.com/vicentereig/whatsapp-cli@v1.0.0` installs the release. -- If a patch is required, cut a new tag like `v1.0.1`. Never retag in place. +- If you need a patch, cut a new tag, for example `v1.0.1`. Never retag in place. ## Source Builds -The README already outlines the source-build flow: +The README already describes the source-build flow: ```bash git clone https://github.com/vicentereig/whatsapp-cli.git @@ -93,10 +93,10 @@ go mod download go build -o whatsapp-cli . ``` -Keep that section updated when dependencies or build flags change. +When dependencies or build flags change, keep that section updated. ## Post-Release 1. Monitor issues or discussions for regressions. -2. Plan the next version number based on the nature of upcoming changes. -3. Keep `docs/RELEASE.md` updated if the process evolves (e.g., new targets or signing requirements). +2. Plan the next version number to match the upcoming changes. +3. If the process changes, for example with new targets or signing requirements, keep `docs/RELEASE.md` updated. diff --git a/docs/architecture.md b/docs/architecture.md index 6be1a5b..aae74db 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -2,7 +2,7 @@ ## Overview -Standalone Go CLI tool built directly on top of `whatsmeow` for WhatsApp access via command line. Designed to be used by Codex/Claude Code and other automation tools. +WhatsApp CLI is a standalone Go program built on `whatsmeow` for WhatsApp access from the command line. Codex, Claude Code, and other automation tools can use it. ## Architecture @@ -18,11 +18,11 @@ Standalone Go CLI tool built directly on top of `whatsmeow` for WhatsApp access ## Key Advantages -- **Single binary**: No Python/Node dependencies, just compile and run -- **Direct access**: No HTTP middleware, queries directly against WhatsApp -- **Reuse existing code**: Most logic from existing `whatsapp-mcp` bridge can be adapted -- **JSON output**: Easy for Codex/Claude Code to parse -- **Same authentication**: QR code on first run, persistent session after +- **Single binary**: no Python or Node.js dependency. Compile and run it. +- **Direct access**: no HTTP middleware. The CLI queries WhatsApp directly. +- **Reuse code**: you can adapt most of the logic from the `whatsapp-mcp` bridge. +- **JSON output**: Codex and Claude Code can parse it directly. +- **Same authentication**: a QR code on first run, then a persistent session. ## Project Structure @@ -79,7 +79,7 @@ whatsapp-cli media download --message-id ID --chat-jid JID ## Output Format -All commands output JSON for easy parsing: +All commands print output as JSON: ```json { @@ -135,12 +135,12 @@ if client.Store.ID == nil { ``` **What happens:** -1. CLI detects no session in `store/whatsapp.db` -2. Generates QR code and displays it in terminal -3. User scans with WhatsApp mobile app (Settings → Linked Devices → Link a Device) -4. WhatsApp sends session credentials via the QR code flow -5. `whatsmeow` saves session data to SQLite automatically -6. Done! Session persists for ~20 days +1. The CLI detects that no session exists in `store/whatsapp.db`. +2. It generates a QR code and shows it in the terminal. +3. The user scans it with the WhatsApp mobile app (Settings → Linked Devices → Link a Device). +4. WhatsApp sends session credentials through the QR code flow. +5. `whatsmeow` saves the session data to SQLite automatically. +6. Done. The session lasts about 20 days. ### Subsequent Logins (Session Exists) @@ -156,17 +156,17 @@ else { ``` **What happens:** -1. CLI reads session from `store/whatsapp.db` -2. Automatically reconnects to WhatsApp servers -3. No QR code needed -4. Works until session expires (~20 days) +1. The CLI reads the session from `store/whatsapp.db`. +2. It reconnects to WhatsApp servers automatically. +3. It does not need a QR code. +4. It works until the session expires, about 20 days later. ### Authentication Strategy **Hybrid approach (recommended):** -- CLI commands auto-connect if session exists -- If no session, show QR code inline and wait -- Optional `--non-interactive` flag for scripts (fails if not authenticated) +- If a session exists, CLI commands connect automatically. +- If no session exists, the CLI shows the QR code inline and waits. +- An optional `--non-interactive` flag supports scripts. It fails if the user is not authenticated. **Example flow:** ```bash @@ -184,13 +184,13 @@ $ whatsapp-cli contacts search "John" ## Session Storage -**Session is stored in SQLite** (`store/whatsapp.db`): +**The session lives in SQLite** (`store/whatsapp.db`) and holds: - Device ID - Encryption keys - Registration info - Contact data -**Message history** stored in `store/messages.db`: +**Message history** is stored in `store/messages.db`: - Chats table (JID, name, last message time) - Messages table (ID, content, sender, timestamp, media metadata) - Media files stored on-demand in `store/{chat_jid}/` @@ -222,7 +222,7 @@ go get github.com/mattn/go-sqlite3 - Add context-aware message retrieval - Support all operations: read, send, search, download -### 4. Build as single binary +### 4. Build as a single binary ```bash go build -o whatsapp-cli ``` @@ -237,19 +237,19 @@ GOOS=windows GOARCH=amd64 go build -o whatsapp-cli.exe ## Data Access Patterns ### Read Operations (Direct SQLite) -- List messages: Query `messages` table with filters -- Search contacts: Query `chats` table -- Get chat metadata: Join `chats` and `messages` +- List messages: Query the `messages` table with filters. +- Search contacts: Query the `chats` table. +- Get chat metadata: Join the `chats` and `messages` tables. ### Write Operations (Via whatsmeow) -- Send message: Use `client.SendMessage()` -- Upload media: Use `client.Upload()` then send -- Download media: Use `client.Download()` +- Send a message: Use `client.SendMessage()`. +- Upload media: Use `client.Upload()`, then send it. +- Download media: Use `client.Download()`. ### Event Handling -- Real-time message sync: Event handlers for incoming messages -- History sync: Handle `HistorySync` events for backfill -- Connection status: Monitor connected/disconnected events +- Real-time message sync: Event handlers process incoming messages. +- History sync: Handle `HistorySync` events for backfill. +- Connection status: Monitor connect and disconnect events. ## Comparison with Existing MCP Server @@ -267,8 +267,8 @@ GOOS=windows GOARCH=amd64 go build -o whatsapp-cli.exe - **Daemon mode**: Optional persistent connection for faster commands - **Webhook support**: HTTP callbacks for incoming messages -- **Export functionality**: Export chats to JSON/CSV/HTML +- **Export**: Export chats to JSON, CSV, or HTML - **Group management**: Create groups, manage participants - **Status updates**: Post and view WhatsApp status - **Typing indicators**: Send typing status -- **Read receipts**: Mark messages as read/unread +- **Read receipts**: Mark messages as read or unread From d4537e7510be0ca818440a0e645495a1110aacfd Mon Sep 17 00:00:00 2001 From: Vicente Reig Rincon de Arellano Date: Sat, 8 Aug 2026 18:17:06 +0200 Subject: [PATCH 2/2] feat(website): add landing page with live terminal demo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Static HTML/CSS/JS page under website/ — no build step. Hallmark editorial/Workbench build: hero + install command, a two-tab live terminal demo (whatsapp-cli's real sync progress output, plus an honestly-labeled illustrative chat_backup_agent.py sample script over the real messages table schema), a command reference table sourced from README.md, and a credits nod to openclaw/wacli (verified via GitHub's API: "Heavily inspired by whatsapp-cli by Vicente Reig", created by @steipete). Design tokens in tokens.css, component/layout styles in style.css, tab switching + typewriter animation + copy-to-clipboard in demo.js. Passed the Hallmark slop-test (58 gates) after fixing a tag-left/ header-right section head and an inline OKLCH value found in review. website/README.md documents local preview and flags that the publish step into oss.vicente.services/whatsapp-cli isn't wired up yet. Co-Authored-By: Claude Sonnet 5 --- .hallmark/log.json | 12 ++ .hallmark/preflight.json | 11 + website/README.md | 61 ++++++ website/demo.js | 180 ++++++++++++++++ website/index.html | 114 ++++++++++ website/style.css | 437 +++++++++++++++++++++++++++++++++++++++ website/tokens.css | 90 ++++++++ 7 files changed, 905 insertions(+) create mode 100644 .hallmark/log.json create mode 100644 .hallmark/preflight.json create mode 100644 website/README.md create mode 100644 website/demo.js create mode 100644 website/index.html create mode 100644 website/style.css create mode 100644 website/tokens.css diff --git a/.hallmark/log.json b/.hallmark/log.json new file mode 100644 index 0000000..7adf552 --- /dev/null +++ b/.hallmark/log.json @@ -0,0 +1,12 @@ +[ + { + "date": "2026-08-08", + "macrostructure": "Workbench", + "theme": "Studio", + "theme_axes": "light / high-contrast-serif / chromatic-green", + "enrichment": "none", + "nav": "N9 Edge-aligned minimal", + "footer": "Ft4 Dense typographic", + "brief": "whatsapp-cli — landing page with live terminal demo (sync + sample query script) and openclaw/wacli credits nod" + } +] diff --git a/.hallmark/preflight.json b/.hallmark/preflight.json new file mode 100644 index 0000000..0edf5ee --- /dev/null +++ b/.hallmark/preflight.json @@ -0,0 +1,11 @@ +{ + "scanned": "2026-08-08", + "findings": { + "font_stack": null, + "palette": null, + "motion_library": null, + "spacing_scale": null, + "framework": "none (pure Go CLI repo; no prior web assets)" + }, + "note": "First Hallmark run for this project. Nothing to preserve — full Hallmark stack introduced under website/." +} diff --git a/website/README.md b/website/README.md new file mode 100644 index 0000000..6ec8bb9 --- /dev/null +++ b/website/README.md @@ -0,0 +1,61 @@ +# website/ + +The static marketing page for whatsapp-cli. Plain HTML, CSS, and vanilla JS — +no build step, no dependencies. + +``` +website/ +├── index.html # the page +├── tokens.css # design tokens (colour, type, spacing) +├── style.css # component + layout styles +├── demo.js # tab switching, typing animation, copy-to-clipboard +└── README.md # this file +``` + +## Preview locally + +Any static file server works. From this directory: + +```bash +python3 -m http.server 8000 +# then open http://localhost:8000 +``` + +Or just open `index.html` directly in a browser — nothing here depends on a +server (fonts load from Google Fonts over HTTPS; everything else is local). + +## Editing + +- **Copy and command reference** — keep it in sync with `../README.md`. + If a flag or command changes there, update the table in `index.html`. +- **Terminal transcripts** (`demo.js`) — both are scripted replays, not a + live WhatsApp connection: `SYNC_LINES` is whatsapp-cli's real documented + `sync` progress output; `AGENT_LINES` is an illustrative sample session for + a companion script, not a shipped `whatsapp-cli` subcommand. Keep both + clearly labelled as illustrative if you change them — don't present sample + data as a real backup. +- **Design tokens** — all colour and font values live in `tokens.css`. Don't + add inline colours or fonts in `style.css` or `index.html`. + +## Deploying to oss.vicente.services/whatsapp-cli + +This site is meant to be published at `oss.vicente.services/whatsapp-cli`, +alongside the other project pages hosted from the `vicente.services/website` +repo (see `sites/oss/` there — that's how `dspy.rb`'s docs are published +under `oss.vicente.services/dspy.rb`). + +**The exact publish step from this repo into that one is not wired up yet.** +The known-working pattern, based on how `dspy.rb` publishes today: + +1. Build or copy this directory's output into the `website` repo, under a + path that maps to `oss.vicente.services/whatsapp-cli` (mirroring the + `dist/oss.vicente.services/dspy.rb/` layout observed there). +2. Deploy the `website` repo as usual (it runs on Fly.io — see that repo's + `fly.toml` and `README.md`). + +Until this is confirmed and scripted, treat step 1 as **manual**: copy this +directory's contents into place in the `website` repo, verify the page +renders under `/whatsapp-cli`, then deploy that repo. If you formalize this +(a script, a CI job, a git submodule), update this section — the workflow +described here is what worked at the time of writing, not a guarantee of +how the pipeline works long-term. diff --git a/website/demo.js b/website/demo.js new file mode 100644 index 0000000..fe755a3 --- /dev/null +++ b/website/demo.js @@ -0,0 +1,180 @@ +/* whatsapp-cli · live demo terminal + * Scripted replay — not a live WhatsApp connection. See the caption under the + * terminal for what each tab actually represents. + */ + +(() => { + "use strict"; + + const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + + /* ---------- Transcript data ---------- */ + /* whatsapp-cli's own documented `sync` progress output (README § Command: sync). */ + const SYNC_LINES = [ + "$ whatsapp-cli sync", + "\u{1F680} Starting WhatsApp sync...", + "✓ Connected to WhatsApp", + "\u{1F504} Listening for messages... (Press Ctrl+C to stop)", + "\u{1F4DC} Processing history sync (42 conversations)...", + "\u{1F4AC} Synced 1234 messages...", + "^C", + "✓ Sync completed. Total messages synced: 1234", + ]; + + /* Illustrative companion script over the local `messages` table — sample + * data, not a real chat backup, and not a whatsapp-cli subcommand. */ + const AGENT_LINES = [ + "$ python3 chat_backup_agent.py", + "» which group chat grew the most this year?", + " SELECT chat_jid, COUNT(*) FROM messages", + " WHERE strftime('%Y', timestamp) = '2026'", + " GROUP BY chat_jid ORDER BY 2 DESC LIMIT 1;", + " Book Club — 1,842 messages (+340% vs. 2025)", + "", + "» when did I last hear from Mom?", + " SELECT MAX(timestamp) FROM messages", + " WHERE chat_jid = ? AND is_from_me = 0;", + ' 3 days ago, 21:14 — "sounds good, see you sunday"', + "", + "» any media I haven't backed up locally?", + " SELECT COUNT(*) FROM messages", + " WHERE media_type != '' AND local_path IS NULL;", + " 212 messages reference media not yet downloaded.", + " Run `whatsapp-cli media download` to fetch them.", + ]; + + const TRANSCRIPTS = { sync: SYNC_LINES, agent: AGENT_LINES }; + const typed = { sync: false, agent: false }; + + /* ---------- Typewriter ---------- */ + + function typeInto(codeEl, lines, done) { + const caret = document.createElement("span"); + caret.className = "terminal__caret"; + caret.setAttribute("aria-hidden", "true"); + codeEl.textContent = ""; + codeEl.appendChild(caret); + + if (reducedMotion) { + codeEl.textContent = lines.join("\n"); + codeEl.appendChild(caret); + if (done) done(); + return; + } + + let line = 0; + let char = 0; + + function step() { + if (line >= lines.length) { + if (done) done(); + return; + } + const current = lines[line]; + if (char <= current.length) { + const soFar = lines.slice(0, line).concat(current.slice(0, char)).join("\n"); + codeEl.textContent = soFar; + codeEl.appendChild(caret); + char++; + setTimeout(step, current.length === 0 ? 40 : 14); + } else { + line++; + char = 0; + setTimeout(step, 90); + } + } + step(); + } + + function ensureTyped(name) { + if (typed[name]) return; + typed[name] = true; + const panel = document.querySelector(`[data-panel="${name}"]`); + const codeEl = panel.querySelector("code"); + typeInto(codeEl, TRANSCRIPTS[name]); + } + + /* ---------- Tabs (ARIA tabs pattern, crossfade content) ---------- */ + + const tabs = Array.from(document.querySelectorAll(".terminal__tab")); + const panels = Array.from(document.querySelectorAll(".terminal__panel")); + + function activate(name) { + tabs.forEach((tab) => { + const isActive = tab.dataset.tab === name; + tab.classList.toggle("is-active", isActive); + tab.setAttribute("aria-selected", String(isActive)); + tab.tabIndex = isActive ? 0 : -1; + }); + panels.forEach((panel) => { + const isActive = panel.dataset.panel === name; + panel.classList.toggle("is-active", isActive); + panel.hidden = !isActive; + }); + ensureTyped(name); + } + + tabs.forEach((tab, i) => { + tab.addEventListener("click", () => activate(tab.dataset.tab)); + tab.addEventListener("keydown", (e) => { + if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return; + e.preventDefault(); + const next = e.key === "ArrowRight" + ? tabs[(i + 1) % tabs.length] + : tabs[(i - 1 + tabs.length) % tabs.length]; + next.focus(); + activate(next.dataset.tab); + }); + }); + + /* Start the first tab's transcript once the terminal scrolls into view. */ + const terminal = document.querySelector("[data-terminal]"); + if (terminal) { + const io = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + ensureTyped("sync"); + io.disconnect(); + } + }); + }, { threshold: 0.3 }); + io.observe(terminal); + } + + /* ---------- Copy to clipboard ---------- */ + + document.querySelectorAll(".copy-btn").forEach((btn) => { + btn.addEventListener("click", async () => { + const targetId = btn.dataset.copyTarget; + const target = document.getElementById(targetId); + if (!target) return; + try { + await navigator.clipboard.writeText(target.textContent.trim()); + } catch { + /* Clipboard API unavailable (e.g. insecure context) — fail silently, label doesn't change. */ + return; + } + const label = btn.querySelector(".copy-btn__label"); + const original = label.textContent; + btn.dataset.state = "copied"; + label.textContent = "Copied"; + setTimeout(() => { + delete btn.dataset.state; + label.textContent = original; + }, 2500); + }); + }); + + /* ---------- Sticky CTA reveal ---------- */ + + const stickyCta = document.querySelector("[data-sticky-cta]"); + const revealTrigger = document.querySelector("#commands"); + if (stickyCta && revealTrigger) { + const io = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + stickyCta.classList.toggle("is-visible", entry.boundingClientRect.top < 0 || entry.isIntersecting); + }); + }, { threshold: 0 }); + io.observe(revealTrigger); + } +})(); diff --git a/website/index.html b/website/index.html new file mode 100644 index 0000000..86bfb09 --- /dev/null +++ b/website/index.html @@ -0,0 +1,114 @@ + + + + + + whatsapp-cli — WhatsApp, on the command line + + + + + + + + + + + + + + + + +
+ +
+
+

WhatsApp, on the command line.

+

Every command returns JSON. Sync once to a local SQLite store, then search, send, and script against it.

+ +
+

Install

+
+ brew install vicentereig/tap/whatsapp-cli + +
+ Other install methods → +
+
+
+ +
+
+

See it work

+

Two terminals. One keeps the local store in sync. The other queries it.

+
+ +
+
+ + +
+
+
+
+
+

Illustrative session over the local messages table. chat_backup_agent.py is a 40-line example script, not a whatsapp-cli subcommand — the store is plain SQLite, so it's yours to query.

+
+ +
+
+

What's in the box

+
+ + + + + + + + + + + + + + + +
CommandWhat it doesFlags
authPair with your phone via QR code.—
syncMirror new messages into the local store. Runs until Ctrl+C.—
messages listList stored messages, newest first.--chat --limit --page
messages searchFull-text search across stored messages.--query --limit --page
contacts searchSearch contacts by name or number.--query
chats listList synced chats by recent activity.--query --limit --page
sendSend a text message.--to --message
media downloadDownload a message's media attachment.--message-id --chat --output
+
+ +
+

Noticed elsewhere

+ +
+ + + +
+ +
+

whatsapp-cli. MIT licensed. Built with Go and whatsmeow. + e3a2d86, 2026-08-08. + Source, issues, and the contributing guide are on + GitHub. + Not affiliated with WhatsApp or Meta.

+
+ + + + diff --git a/website/style.css b/website/style.css new file mode 100644 index 0000000..6067c14 --- /dev/null +++ b/website/style.css @@ -0,0 +1,437 @@ +/* Hallmark · pre-emit critique: P4 H4 E4 S5 R4 V5 + * Hallmark · genre: editorial · macrostructure: Workbench · theme: Studio · enrichment: none (typography + live terminal panels) · nav: N9 · footer: Ft4 + * contrast: pass (40–41) · nav: N9 · footer: Ft4 · slop: pass (42–45) + * honest: pass (46, caption labels the demo as illustrative) · chrome: pass (47, no re-drawn window chrome) · tokens: pass (48) · responsive: pass (49) · icons: pass (30, N/A — no icon library, emoji are verbatim CLI output text) + * mobile: pass (34, 49, 50–57) + */ + +@import url("tokens.css"); + +/* ---------- Reset ---------- */ + +*, *::before, *::after { box-sizing: border-box; } +html, body { margin: 0; padding: 0; } +html { overflow-x: clip; } +body { overflow-x: clip; } + +html { + background: var(--color-paper); + color: var(--color-ink); +} + +body { + font-family: var(--font-body); + font-size: var(--text-base); + line-height: 1.6; + background: var(--color-paper); + color: var(--color-ink); +} + +img, svg { max-width: 100%; display: block; } + +a { color: inherit; } + +code, pre { font-family: var(--font-mono); } + +h1, h2, h3 { + font-family: var(--font-display); + font-weight: 600; + font-style: normal; + line-height: 1.1; + letter-spacing: -0.02em; + margin: 0; + overflow-wrap: anywhere; + min-width: 0; +} + +p { margin: 0; } + +.muted { color: var(--color-neutral); } +.tnum { font-variant-numeric: tabular-nums; } + +.skip-link { + position: absolute; + left: -9999px; + top: var(--space-sm); + background: var(--color-ink); + color: var(--color-paper); + padding: var(--space-xs) var(--space-md); + z-index: var(--z-sticky); +} +.skip-link:focus { + left: var(--space-sm); +} + +:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 3px; + border-radius: 2px; +} + +/* ---------- Layout shell ---------- */ + +.container { + max-width: 64rem; + margin-inline: auto; + padding-inline: var(--page-gutter); +} + +section { + padding-block: var(--space-2xl) var(--space-xl); +} + +/* ---------- Nav — N9 edge-aligned minimal ---------- */ + +.nav-edge { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--space-lg) var(--page-gutter); + gap: var(--space-md); +} + +.wordmark { + font-family: var(--font-mono); + font-size: var(--text-md); + font-weight: 500; + text-decoration: none; + white-space: nowrap; + min-width: 0; + overflow-wrap: normal; +} + +.cta-outline { + border: 1px solid var(--color-rule-2); + padding: var(--space-xs) var(--space-md); + min-height: 44px; + display: inline-flex; + align-items: center; + font-size: var(--text-sm); + text-decoration: none; + white-space: nowrap; + transition: background-color var(--dur-short) var(--ease-out); +} +.cta-outline:hover { background: var(--color-paper-2); } +.cta-outline:active { transform: translateY(1px); } + +/* ---------- Hero (Workbench: small, functional heading) ---------- */ + +.hero { + padding-block: var(--space-2xl) var(--space-3xl); +} + +.hero__inner { + max-width: 42rem; + margin-inline: var(--page-gutter); +} + +.hero h1 { + font-size: var(--text-display); + max-width: 16ch; +} + +.lede { + margin-top: var(--space-lg); + max-width: 58ch; + font-size: var(--text-md); + color: var(--color-ink-2); +} + +.install { + margin-top: var(--space-2xl); +} + +.install__label { + font-family: var(--font-mono); + font-size: var(--text-xs); + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--color-neutral); + margin-bottom: var(--space-xs); +} + +.install__row { + display: flex; + align-items: stretch; + border: 1px solid var(--color-rule-2); + max-width: fit-content; +} + +.install__cmd { + padding: var(--space-sm) var(--space-md); + font-size: var(--text-sm); + white-space: nowrap; + overflow-x: auto; +} + +.copy-btn { + border: none; + border-left: 1px solid var(--color-rule-2); + background: var(--color-paper-2); + font-family: var(--font-mono); + font-size: var(--text-xs); + padding-inline: var(--space-md); + min-width: 44px; + min-height: 44px; + cursor: pointer; + transition: background-color var(--dur-short) var(--ease-out); +} +.copy-btn:hover { background: var(--color-paper-3); } +.copy-btn:active { transform: translateY(1px); } +.copy-btn:disabled { opacity: 0.5; cursor: not-allowed; } +.copy-btn[data-state="copied"] { color: var(--color-accent); } + +.link { + display: inline-block; + margin-top: var(--space-md); + font-size: var(--text-sm); + text-decoration: underline solid var(--color-rule-2) 1px; + text-underline-offset: 3px; + transition: text-decoration-color var(--dur-short) var(--ease-out); +} +.link:hover { text-decoration-color: var(--color-accent); } + +/* ---------- Section head S2 — hanging ---------- */ + +.head-hang { + padding-block: var(--space-3xl) var(--space-xl); + max-width: 42rem; + margin-inline: var(--page-gutter); +} +.head-hang h2 { font-size: var(--text-2xl); } +.section-intro { + margin-top: var(--space-sm); + color: var(--color-ink-2); + max-width: 58ch; +} + +/* ---------- Demo section: terminal, two tabs ---------- */ + +.demo-section { padding-block: var(--space-xl) var(--space-2xl); } + +.terminal { + margin-inline: var(--page-gutter); + border: 1px solid var(--color-rule-2); + background: var(--color-terminal-bg); +} + +.terminal__tabs { + display: flex; + border-bottom: 1px solid var(--color-rule-2); + overflow-x: auto; +} + +.terminal__tab { + font-family: var(--font-mono); + font-size: var(--text-xs); + color: var(--color-terminal-muted); + background: transparent; + border: none; + border-right: 1px solid var(--color-rule-2); + padding: var(--space-sm) var(--space-md); + min-height: 44px; + cursor: pointer; + white-space: nowrap; + flex-shrink: 0; + transition: color var(--dur-short) var(--ease-out), + background-color var(--dur-short) var(--ease-out); +} +.terminal__tab:hover { color: var(--color-terminal-ink); } +.terminal__tab:active { transform: translateY(1px); } +.terminal__tab.is-active { + color: var(--color-terminal-accent); + background: var(--color-terminal-bg-2); +} + +.terminal__body { position: relative; } + +.terminal__panel { + margin: 0; + padding: var(--space-lg); + color: var(--color-terminal-ink); + font-size: var(--text-sm); + line-height: 1.7; + min-height: 16rem; + max-height: 22rem; + overflow-y: auto; + white-space: pre-wrap; + word-break: break-word; +} +.terminal__panel[hidden] { display: none; } + +.terminal__caret { + display: inline-block; + width: 0.55em; + background: var(--color-terminal-accent); + animation: blink 1s steps(2) infinite; +} +@media (prefers-reduced-motion: reduce) { + .terminal__caret { animation: none; opacity: 1; } +} +@keyframes blink { 50% { opacity: 0; } } + +.terminal__caption { + margin-top: var(--space-sm); + margin-inline: var(--page-gutter); + font-size: var(--text-xs); + color: var(--color-neutral); + max-width: 58ch; +} +.terminal__caption code { + font-size: 0.95em; + background: var(--color-paper-2); + padding: 0.05em 0.35em; +} + +/* ---------- Section head — rule-topped, single column ---------- + * (Not S1 left-margin-numbered: an eyebrow beside a heading in a + * second column is gate-54-banned regardless of archetype name. An + * eyebrow atop the heading, single column, is the compliant form.) */ + +.head-rule { + border-top: 1px solid var(--color-rule); + padding-block: var(--space-lg) var(--space-lg); + margin-inline: var(--page-gutter); + margin-top: var(--space-2xl); +} +.head-rule h2 { font-size: var(--text-2xl); } + +/* ---------- Commands — F3 tabular spec sheet ---------- */ + +.commands-section { padding-block: 0 var(--space-2xl); } + +.spec-sheet { + width: calc(100% - 2 * var(--page-gutter)); + margin-inline: var(--page-gutter); + border-collapse: collapse; + font-size: var(--text-sm); +} +.spec-sheet th { + text-align: left; + font-family: var(--font-mono); + font-size: var(--text-xs); + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--color-neutral); + font-weight: 400; + padding-block: var(--space-sm); + border-bottom: 1px solid var(--color-rule-2); +} +.spec-sheet td { + padding-block: var(--space-sm); + border-bottom: 1px solid var(--color-rule); + vertical-align: top; +} +.spec-sheet td:first-child { white-space: nowrap; padding-right: var(--space-md); } +.spec-sheet code { + font-size: 0.9em; + background: var(--color-paper-2); + padding: 0.05em 0.35em; +} + +/* ---------- Credits — S4 inline + T1 pull-quote with marginalia ---------- */ + +.credits-section { + margin-inline: var(--page-gutter); + max-width: 42rem; +} + +.inline-head { + font-family: var(--font-mono); + font-size: var(--text-xs); + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--color-neutral); + margin-bottom: var(--space-lg); +} + +.proof-margin { + display: grid; + grid-template-columns: 1fr 14rem; + gap: var(--space-xl); + align-items: start; +} +.quote { + margin: 0; + font-family: var(--font-display); + font-size: var(--text-xl); + font-weight: 400; + font-style: normal; + line-height: 1.3; +} +.attribution { margin-top: var(--space-md); font-size: var(--text-sm); } +.attribution a { text-decoration: underline; } +.aside-note { + font-size: var(--text-sm); + color: var(--color-neutral); + padding-top: var(--space-xs); + border-top: 1px solid var(--color-rule); +} + +/* ---------- Sticky CTA (C4) ---------- */ + +.sticky-cta { + position: sticky; + bottom: var(--space-md); + display: flex; + justify-content: flex-end; + margin-inline: var(--page-gutter); + pointer-events: none; + opacity: 0; + transform: translateY(8px); + transition: opacity var(--dur-short) var(--ease-out), + transform var(--dur-short) var(--ease-out); +} +.sticky-cta.is-visible { opacity: 1; transform: none; pointer-events: auto; } +.sticky-cta .link { + margin: 0; + background: var(--color-paper); + border: 1px solid var(--color-rule-2); + padding: var(--space-sm) var(--space-lg); + min-height: 44px; + display: inline-flex; + align-items: center; + white-space: nowrap; + box-shadow: var(--shadow-whisper); +} + +/* ---------- Footer Ft4 — dense typographic colophon ---------- */ + +.foot-dense { + padding: var(--space-2xl) var(--page-gutter) var(--space-2xl); + border-top: 1px solid var(--color-rule); + font-size: var(--text-xs); + color: var(--color-neutral); + line-height: 1.7; +} +.foot-dense a { text-decoration: underline; } +.foot-dense p { max-width: 68ch; } + +/* ---------- Page-load reveal ---------- */ + +.reveal { + opacity: 0; + transform: translateY(8px); + animation: reveal var(--dur-long) var(--ease-out) forwards; + animation-delay: calc(var(--i, 0) * 60ms); +} +@keyframes reveal { to { opacity: 1; transform: none; } } + +@media (prefers-reduced-motion: reduce) { + .reveal { animation: reveal-reduced 150ms linear forwards; } + @keyframes reveal-reduced { to { opacity: 1; transform: none; } } + .sticky-cta { transition: opacity 150ms linear; transform: none; } +} + +/* ---------- Responsive ---------- */ + +@media (max-width: 40rem) { + .proof-margin { + grid-template-columns: 1fr; + } + .terminal__panel { max-height: 18rem; } +} + +@media (min-width: 60rem) { + .hero__inner { margin-inline: max(var(--page-gutter), calc((100% - 64rem) / 2 + var(--page-gutter))); } +} diff --git a/website/tokens.css b/website/tokens.css new file mode 100644 index 0000000..117678a --- /dev/null +++ b/website/tokens.css @@ -0,0 +1,90 @@ +/* whatsapp-cli · design tokens + * Theme: Studio (editorial genre · light paper · chromatic-green accent · high-contrast serif) + * Anchor hue: 150 (forest green) — deliberately not WhatsApp's brand green. + */ + +:root { + /* Type */ + --font-display: "Fraunces", ui-serif, Georgia, serif; + --font-body: "IBM Plex Sans", ui-sans-serif, system-ui, sans-serif; + --font-mono: "JetBrains Mono", ui-monospace, "SFMono-Regular", monospace; + + --text-xs: 0.7rem; + --text-sm: 0.8125rem; + --text-base: 1rem; + --text-md: 1.1875rem; + --text-lg: 1.4375rem; + --text-xl: 1.75rem; + --text-2xl: 2.25rem; + --text-display-s: clamp(2.1rem, 3vw + 1rem, 2.9rem); + --text-display: clamp(2.5rem, 4vw + 1rem, 3.6rem); + + /* Space — 4pt scale */ + --space-3xs: 0.125rem; + --space-2xs: 0.25rem; + --space-xs: 0.5rem; + --space-sm: 0.75rem; + --space-md: 1rem; + --space-lg: 1.5rem; + --space-xl: 2.5rem; + --space-2xl: 4rem; + --space-3xl: 6rem; + --space-4xl: 9rem; + + /* Colour — light (default) */ + --color-paper: oklch(97% 0.012 150); + --color-paper-2: oklch(94% 0.014 150); + --color-paper-3: oklch(91% 0.016 150); + --color-rule: oklch(82% 0.012 150); + --color-rule-2: oklch(72% 0.014 150); + --color-neutral: oklch(50% 0.010 150); + --color-muted: oklch(42% 0.010 150); + --color-ink: oklch(19% 0.012 150); + --color-ink-2: oklch(26% 0.012 150); + --color-accent: oklch(45% 0.130 152); + --color-accent-ink: oklch(98% 0.010 150); + --color-focus: oklch(50% 0.150 152); + --color-error: oklch(56% 0.180 25); + + /* Terminal panels stay dark in both modes — real terminals are dark, this is content not chrome */ + --color-terminal-bg: oklch(15% 0.014 150); + --color-terminal-bg-2: oklch(19% 0.016 150); + --color-terminal-ink: oklch(92% 0.010 150); + --color-terminal-muted: oklch(66% 0.012 150); + --color-terminal-accent: oklch(72% 0.135 152); + + /* Motion */ + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); + --ease-in: cubic-bezier(0.7, 0, 0.84, 0); + --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); + --dur-micro: 120ms; + --dur-short: 220ms; + --dur-long: 420ms; + + /* Shadows — named, per layout-and-space.md § Depth */ + --shadow-whisper: 0 1px 2px oklch(20% 0.01 150 / 0.08); + + /* Z-scale */ + --z-base: 1; + --z-raised: 10; + --z-sticky: 200; + + --page-gutter: clamp(1rem, 4vw, 2.5rem); +} + +@media (prefers-color-scheme: dark) { + :root { + --color-paper: oklch(14% 0.012 150); + --color-paper-2: oklch(18% 0.014 150); + --color-paper-3: oklch(22% 0.016 150); + --color-rule: oklch(30% 0.012 150); + --color-rule-2: oklch(38% 0.014 150); + --color-neutral: oklch(60% 0.009 150); + --color-muted: oklch(70% 0.008 150); + --color-ink: oklch(94% 0.008 150); + --color-ink-2: oklch(88% 0.008 150); + --color-accent: oklch(72% 0.120 152); + --color-accent-ink: oklch(14% 0.020 150); + --color-focus: oklch(76% 0.130 152); + } +}