|
|
1 day ago | |
|---|---|---|
| openwiki | 1 day ago | |
| src | 1 day ago | |
| .gitignore | 1 day ago | |
| AGENTS.md | 2 days ago | |
| Cargo.lock | 2 days ago | |
| Cargo.toml | 2 days ago | |
| README.md | 1 day ago | |
| build.rs | 1 day ago | |
| config.toml.global.template | 1 day ago | |
| soul.md.template | 1 day ago |
Terminal WhatsApp client (ratatui) with per-chat and global AI autoreply via an OpenAI-compatible API.
cargo build --release
wh --version (or -V) prints the build version and exits: package version and short git commit, e.g. wh 0.1.0 (cde0f55) (unknown when built without git). The same version is shown at the bottom right of the main screen.
Built from Windows with cargo-zigbuild. Zig provides the C cross-compiler and linker for the bundled C code (SQLite, aws-lc-sys, ring).
One-time prerequisites:
rustup target add x86_64-unknown-linux-musl
winget install -e --id zig.zig # or: pip install ziglang
cargo install --locked cargo-zigbuild
Build:
cargo zigbuild --release --target x86_64-unknown-linux-musl
Output: target/x86_64-unknown-linux-musl/release/wh
Verify on Linux:
file wh # ELF 64-bit x86-64, statically linked
chmod +x wh && ./wh --version
On Linux everything lives in ~/.config/wh/ of the user running wh; on Windows in %APPDATA%\wh. File names are case-sensitive on Linux.
| File | Content |
|---|---|
config.toml |
settings (optional) |
SOUL.md |
system prompt (required for the AI) |
vision.md |
request used to describe images (optional) |
session.db |
WhatsApp linked-device session |
history.db |
chats, messages, autoreply flags, image descriptions |
ai_requests.log |
AI request log, only with ai_log = true |
Copy session.db from Windows to reuse the linked session instead of scanning the QR again.
Settings live in config.toml in the data directory. Start from config.toml.global.template: copy it there and fill openai_api_key. A missing file means defaults. Unknown keys are ignored.
| Key | Default | Description |
|---|---|---|
openai_api_key |
unset | API key of the OpenAI-compatible endpoint (required for AI) |
openai_base_url |
https://ollama.com/v1 |
endpoint base URL |
openai_model |
deepseek-v4.1-flash |
model for autoreplies |
openai_vision_model |
unset | model that describes images (see Images); unset disables image understanding |
ai_log |
false |
append every AI request and its outcome to ai_requests.log (full chat context in clear text) |
reply_delay_seconds |
30 |
seconds the autoreply waits after the first unanswered message of a chat before answering (0 = at once) |
catch_up_seconds |
7200 |
after a restart, answer the last unanswered private message up to this age (0 = off) |
ai_retries |
2 |
retries of a failed AI request, 2s, 4s, ... backoff (0 = off; HTTP 4xx except 429 never retried) |
web_port |
unset | port of the web control panel (1-65535); unset disables it |
SOUL.mdThe system prompt (the responder's identity and rules) lives in its own file, SOUL.md, next to config.toml, and is sent as written. Start from soul.md.template. Without openai_api_key and a non-blank SOUL.md the app runs as a plain WhatsApp client and the status line says AI non configurata. The file is read only at startup: restart wh after editing it.
The app appends to the prompt only factual context: whether the chat is private (Chat privata con «nome».) or a group (Chat di gruppo «nome».) and who writes the user/assistant messages. Everything about behaviour belongs in SOUL.md. Texts the model can find in the conversation:
| Text | Meaning |
|---|---|
Nome: ... |
in groups, each participant message starts with the sender name |
@[te] |
a mention of the account owner |
[Il contatto ha inviato un'immagine. Descrizione: ...] |
an image, replaced by its stored description |
[Il contatto ha inviato un'immagine che non puoi vedere] |
an image without description |
[Il contatto ha inviato un messaggio vocale che non puoi ascoltare] |
a voice note |
no_reply toolEvery AI request offers the model one tool, no_reply (no parameters): when the model calls it, nothing is sent and the status line shows AI: nessuna risposta necessaria. The app never says when to use it: describe the cases in SOUL.md, e.g. "se il messaggio non richiede risposta, chiama il tool no_reply".
system_prompt in config.toml is no longer read: move the prompt to SOUL.md.cooldown_seconds and group_cooldown_seconds are no longer read: use reply_delay_seconds.Enable it per chat with F3 or for all chats with F4 (private chats and groups; broadcasts and newsletters are never answered).
reply_delay_seconds wait; messages arriving meanwhile join the same reply. Groups behave like private chats.Risposta AI scartata: hai già risposto).catch_up_seconds > 0, the last unanswered recent message of each private chat is answered once, at once.openai_vision_model and the description is stored in history.db; replies never send images to the AI, only their descriptions, in every later context too.Descrizione immagine fallita (N): ....vision.md, next to SOUL.md, replaces the built-in request used to describe images; when missing or blank the built-in request (objective, concise, in Italian) is used.| Key | Action |
|---|---|
Tab |
focus list/input |
Enter |
open chat / send |
↑/↓, PgUp/PgDn, Home/End (list focused) |
move selection by one row, by 10 rows, to first/last row |
F2 |
chats/contacts |
| typing (list focused) | filter names; Backspace edits, Esc clears |
Canc/Delete twice |
delete chat locally (history only, not on WhatsApp) |
F3 |
AI autoreply for the open chat |
F4 |
AI autoreply for all chats (persistent; footer shortcut turns green when on) |
F10 |
quit |
Only function keys are used for commands, so they work inside GNU screen, tmux and over SSH.
web_port in config.toml (e.g. web_port = 8080); open http://<host>:<port>/.0.0.0.0 without authentication: use it only on a trusted network (e.g. a VPN).F4), per-chat AI toggle grid (private chats and groups only), "Chiudi bot" button to quit. The page refreshes every 5 seconds; changes show up in the TUI too.GET /api/state (JSON {global_autoreply, chats:[{jid,name,autoreply}], status}); POST /api/global; POST /api/chat with the jid as plain-text body; POST /api/quit. Every POST requires the header X-Wh-Request: 1 (otherwise 403), which blocks cross-site requests from other pages.Example:
curl -X POST -H "X-Wh-Request: 1" http://host:8080/api/global