pi-harness-whatsapp 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENT_INSTALL.md CHANGED
@@ -1,6 +1,6 @@
1
- # Installing pi-harness-whatsapp: notes for AI agents
1
+ # Installing (and uninstalling) pi-harness-whatsapp: notes for AI agents
2
2
 
3
- This file is for an AI agent (Pi, Claude Code, Codex, …) asked to install this extension for a user. Read it fully before changing anything.
3
+ This file is for an AI agent (Pi, Claude Code, Codex, …) asked to install or remove this extension for a user. Read it fully before changing anything. **Asked to remove it?** Go to [Uninstalling](#uninstalling).
4
4
 
5
5
  ## What you can and can't do
6
6
 
@@ -22,8 +22,16 @@ Do these yourself before asking anything, so your questions can include the resu
22
22
  echo '{"type":"get_commands"}' | pi --mode rpc | grep '"get_commands"'
23
23
  ```
24
24
  If another package already registers `/whatsapp` (each entry shows its `source`), tell the user it has to be removed first, and ask before removing it. Other WhatsApp extensions that use different command names can stay.
25
- - **ffmpeg:** run `ffmpeg -version`. Without it, text still works, but voice notes don't. If it's missing, ask before installing it (it usually needs sudo: `sudo apt install ffmpeg`, `brew install ffmpeg`, …).
26
- - **Voice support on this machine:** voice notes need about **3.5 GB of free RAM** while they're processed (check with `free -m`) and **1.5 GB of disk** for the speech files. They work on Linux (x64 or ARM64), Windows (x64) and macOS on Apple Silicon, but not on Intel Macs or Windows on ARM. If the machine can't do voice, tell the user: text still works, and they should keep Reply type on text.
25
+ - **ffmpeg** (only needed if the user wants voice): run `ffmpeg -version`. Without it, text, images and files still work, but voice notes don't. If it's missing and they want voice, ask before installing it (it usually needs sudo: `sudo apt install ffmpeg`, `brew install ffmpeg`, …), and **record what you install** (see [Record what you install](#record-what-you-install-outside-pi)).
26
+ - **Voice support on this machine:** voice is optional and off by default. It works on Linux (x64 or ARM64), Windows (x64) and macOS on Apple Silicon, not on Intel Macs or Windows on ARM. What each part needs (check with `free -m` and `df -h ~`):
27
+
28
+ | Part | Disk | Free RAM while a voice note is handled |
29
+ |---|---|---|
30
+ | Understand voice notes (speech-to-text) | 1.0 GB | about 2 GB |
31
+ | Reply with voice notes (text-to-speech) | 0.6 GB | about 1.2 GB |
32
+ | Both | 1.4 GB | about 3.4 GB |
33
+
34
+ If the machine can't do voice, say so when you ask about it, and leave voice off.
27
35
 
28
36
  ## 2. Ask the basics
29
37
 
@@ -32,7 +40,8 @@ Ask these together, in one message, and wait for the answers:
32
40
  1. **Which WhatsApp number will Pi use?** Recommend a **second number**. The extension uses an unofficial WhatsApp Web client (Baileys), and WhatsApp may restrict numbers that use one. Using their own number also works: they then talk to Pi in the "Message yourself" chat.
33
41
  2. **Owner:** their own phone number, with country code. The owner is the only one who can change Pi from WhatsApp (`/model`, `/compact`, `/quit`, …). If they don't know it yet, skip it: the first person who messages the bot is offered as owner in the terminal.
34
42
  3. **Languages:** which languages they'll write and speak in. The default is English only. Supported: English, Turkish, German, French, Spanish, Italian, Portuguese, Dutch, Polish, Russian, Ukrainian, Arabic, Hindi, Swedish and Finnish. Hindi voice notes are less accurate than the others (see the README's table). Adding languages downloads nothing extra.
35
- 4. **Background service:** should Pi keep running without a terminal (see "Run in the background" in the README)? Don't set it up unless they say yes. If they do, ask which project folder Pi should work in.
43
+ 4. **Voice notes:** off (the default), understand voice notes, reply with voice notes, or both? Give them the disk and RAM from the table in step 1, and the result of your check. Off means a voice note gets "🎤 Voice notes are off. Please type your message."; they can switch voice on later in `/whatsapp` → Voice notes.
44
+ 5. **Background service:** should Pi keep running without a terminal (see "Run in the background" in the README)? Don't set it up unless they say yes. If they do, ask which project folder Pi should work in.
36
45
 
37
46
  ## 3. Offer the other settings
38
47
 
@@ -40,11 +49,11 @@ Then show the remaining settings in **one** message, with the default for each,
40
49
 
41
50
  | Setting (name in `/whatsapp`) | What it does | Default | Other choices |
42
51
  |---|---|---|---|
43
- | Reply type | How Pi answers | **mirror me**: a voice note gets a voice reply, text gets text | text only · voice note · voice note + text |
52
+ | Reply type (only with "reply with voice notes") | How Pi answers | **mirror me**: a voice note gets a voice reply, text gets text | text only · voice note · voice note + text. With "reply" but not "understand", mirror me means text replies only, so suggest "voice note" if they want to hear Pi. |
44
53
  | Reply language | Which language Pi answers in | **mirror me**: the language they used | always one of their languages |
45
54
  | Fallback language | Used only when Pi can't tell the language (e.g. very short replies) | **the first language** they named | any of their languages |
46
- | Voice per language | Which voice speaks each language | **F1** (female) | F2–F5 (female), M1–M5 (male); the same 10 voices for every language |
47
- | Voice reply speed | How fast voice replies are spoken | **1.0** | 0.5–2.0 (0.9 slower, 1.2 faster) |
55
+ | Voice per language (only with "reply with voice notes") | Which voice speaks each language | **F1** (female) | F2–F5 (female), M1–M5 (male); the same 10 voices for every language |
56
+ | Voice reply speed (only with "reply with voice notes") | How fast voice replies are spoken | **1.0** | 0.5–2.0 (0.9 slower, 1.2 faster) |
48
57
  | Terminal answers | Also sends the owner Pi's answers to prompts typed in the terminal, as text | **off** | on |
49
58
  | Progress messages | Sends what Pi writes while it's still working | **off** | on |
50
59
  | Live tool log | One message, updated live, with the latest tool steps | **off** | on |
@@ -71,11 +80,12 @@ Settings live in `~/.pi/agent/pi-harness-whatsapp/config.json`. Unknown or inval
71
80
 
72
81
  Pi reads this file when a session starts, and saves it when a setting changes. If a Pi with this extension is already running, ask the user to quit it before you write the file, so it doesn't keep the old settings or save over yours.
73
82
 
74
- Example: English and Turkish, a male voice for Turkish, the owner set, stats on, a slightly faster voice:
83
+ Example: English and Turkish, voice notes both ways, a male voice for Turkish, the owner set, stats on, a slightly faster voice:
75
84
 
76
85
  ```json
77
86
  {
78
87
  "version": 1,
88
+ "voice": { "understand": true, "reply": true },
79
89
  "languages": ["en", "tr"],
80
90
  "voices": { "tr": "M1" },
81
91
  "voiceMode": "mirror",
@@ -88,9 +98,10 @@ Example: English and Turkish, a male voice for Turkish, the owner set, stats on,
88
98
 
89
99
  | Field | Setting | Values |
90
100
  |---|---|---|
101
+ | `voice` | Voice notes | `{ "understand": <bool>, "reply": <bool> }`, both `false` by default. Always write this field, even when both are `false`. |
91
102
  | `languages` | My languages, Fallback language | Language codes from the table below. The **first** one is the fallback language. |
92
103
  | `voices` | Voice per language | `{ "<code>": "<voice>" }` with a voice from `F1`–`F5`, `M1`–`M5`, for the languages whose voice should differ from the default (`F1`) |
93
- | `voiceMode` | Reply type | `mirror` (default) · `off` (text only) · `voice` · `voice+text` |
104
+ | `voiceMode` | Reply type | `mirror` (default) · `off` (text only) · `voice` · `voice+text`. Only used while `voice.reply` is `true`. |
94
105
  | `replyLanguage` | Reply language | `mirror` (default) or one of `languages` |
95
106
  | `voiceSpeed` | Voice reply speed | 0.5–2.0 (default 1.0) |
96
107
  | `replies` | Terminal answers, Progress messages, Live tool log, Changed files, Stats | `{ "terminal", "interim", "toolActivity", "touchedFiles", "summary" }`, booleans, all `false` by default |
@@ -106,7 +117,46 @@ Language codes: `en` English · `tr` Turkish · `de` German · `fr` French · `e
106
117
 
107
118
  If the user can't decide on a voice, keep the default and tell them they can hear each one later: `/whatsapp` → Voice per language → a voice → "Send me a sample on WhatsApp".
108
119
 
109
- Speech files download by themselves when first needed. You don't have to fetch them.
120
+ Speech files download by themselves when first needed: with voice switched on in the file, the first voice note (or the first spoken reply) waits for the download, and the chat is told. You don't have to fetch them. Tell the user. If they'd rather download right away, leave `voice` off in the file and have them tick the parts in `/whatsapp` → Voice notes after starting Pi: leaving that menu asks "Download now (recommended)" or "When first needed".
121
+
122
+ ## Record what you install outside Pi
123
+
124
+ Anything you install on the computer for this extension (ffmpeg, and tmux for the background service) must be recorded in `setup.json` in the data folder (`~/.pi/agent/pi-harness-whatsapp/setup.json`, or the folder from `PI_HARNESS_WHATSAPP_DIR`). When the user later uninstalls, `/whatsapp` → Remove everything reads it, asks the user about each entry, and removes **exactly those packages** again. A program that was already installed is not recorded.
125
+
126
+ Record every package the install added, not just the one you asked for: installing ffmpeg with apt pulls in about 100 libraries. List the packages before and after, and record the difference:
127
+
128
+ ```bash
129
+ dpkg-query -W -f='${Package}\n' | sort > /tmp/phw-before.txt
130
+ sudo apt-get install -y ffmpeg
131
+ dpkg-query -W -f='${Package}\n' | sort > /tmp/phw-after.txt
132
+ comm -13 /tmp/phw-before.txt /tmp/phw-after.txt # these are the "packages"
133
+ rm /tmp/phw-before.txt /tmp/phw-after.txt
134
+ ```
135
+
136
+ Other package managers: list with `brew list --formula -1 | sort` (Homebrew), `rpm -qa --qf '%{NAME}\n' | sort` (dnf, zypper), `pacman -Qq` (pacman) or `apk info | sort` (apk), the same way. For winget, record the package id you installed.
137
+
138
+ Then write (or add to) `setup.json`, and keep it private (`chmod 600`):
139
+
140
+ ```json
141
+ {
142
+ "version": 1,
143
+ "installed": [
144
+ {
145
+ "what": "ffmpeg",
146
+ "manager": "apt",
147
+ "packages": ["ffmpeg", "libavcodec61", "libavformat61", "…every name from the comm output…"],
148
+ "at": "2026-10-15",
149
+ "by": "Claude Code"
150
+ }
151
+ ]
152
+ }
153
+ ```
154
+
155
+ - `manager` is one of `apt`, `dnf`, `pacman`, `zypper`, `apk`, `brew`, `winget`. Entries with anything else, or with package names that aren't plain names, are ignored.
156
+ - `what` is the name the user will see ("ffmpeg", "tmux"); `at` and `by` are optional and shown to the user.
157
+ - If the install added nothing (it was already there), don't add an entry.
158
+
159
+ A background service isn't recorded here (Pi can't remove the service it runs in); tell the user about it, and see Uninstalling, step 3.
110
160
 
111
161
  ## 6. Hand over to the user
112
162
 
@@ -120,11 +170,15 @@ Tell the user:
120
170
 
121
171
  Then tell them, briefly:
122
172
 
123
- - Anything that isn't a command goes to Pi as a prompt. Voice notes, images and files work too.
173
+ - Anything that isn't a command goes to Pi as a prompt. Images and files work too: Pi sees images, gets files saved for it to open, and can send images and files back when asked ("send me the screenshot").
174
+ - Voice notes, if they turned them on: understanding and replying can each be switched on or off in `/whatsapp` → Voice notes; switching a part off deletes its files after asking. To stop voice replies without deleting anything, set Reply type to text.
175
+ - To remove it later, the README's **Uninstall** section has the steps (in Pi: `/whatsapp` → Danger → Remove everything, or `/whatsapp remove`, then `pi uninstall`).
124
176
  - If they want Pi in a group: add the bot's number to the group, have someone write there, then allow it in `/whatsapp` → Allowed chats → Pick from recent chats. In the group, people @mention Pi or reply to its messages; Pi also sees the last 20 text messages before that for context. You can't allow a group for them in advance, because the group's id is only known once it appears in recent chats. Remind them that every member of an allowed group can prompt Pi.
125
177
  - From WhatsApp they can switch the model (`/model`), the reasoning level (`/think`), compact (`/compact`), stop a run (`/stop`), start a new session (`/new`) and change the reply type, language, speed and extras (`/voice`, `/lang`, `/speed`, `/replies`). Menus come as polls: tap an option.
126
178
  - Everything else, including on/off, allowed chats and voices, is in `/whatsapp` in the terminal.
127
179
 
180
+ Finally, tell the user **everything you set up outside Pi**: for example "installed ffmpeg with apt (recorded in setup.json, so Remove everything can take it out again)", "created `~/.config/systemd/user/pi-whatsapp.service` and enabled lingering". If you have a memory or notes file for this user, write it there too.
181
+
128
182
  ## Check it worked
129
183
 
130
184
  - The footer of Pi shows `WA: connected as +<number> · <n> chats`.
@@ -138,6 +192,75 @@ Then tell them, briefly:
138
192
  - Don't set `commandAccess` to `allowed` unless the user asks.
139
193
  - Don't install the systemd service, or enable it at boot, without the user's yes.
140
194
  - Don't publish, fork or re-upload this package on the user's behalf.
195
+ - Don't edit or delete Pi's session files (`~/.pi/agent/sessions/`), not even to remove WhatsApp traces; see [Uninstalling](#uninstalling).
196
+ - When uninstalling, don't delete more than the steps say: no whole `PI_HARNESS_WHATSAPP_DIR` folder that holds other files, and no ffmpeg, tmux or npm cache without the user's yes.
197
+
198
+ ## Uninstalling
199
+
200
+ Goal: **nothing left behind** on the user's computer, and Pi unlinked from their phone. Follow these steps in order. Don't improvise extra deletions.
201
+
202
+ ### What the extension leaves on a computer
203
+
204
+ | What | Where | Removed by |
205
+ |---|---|---|
206
+ | The code | Pi's package folder, plus an entry in `~/.pi/agent/settings.json` | `pi uninstall` |
207
+ | All its data: WhatsApp login (`auth/`), speech files (`models/`, ~1.5 GB), settings (`config.json`), recent chats, received files (`inbox/`), log (`log/`), lock, `setup.json` | `~/.pi/agent/pi-harness-whatsapp/` (or `$PI_HARNESS_WHATSAPP_DIR` if set; or `$PI_CODING_AGENT_DIR/pi-harness-whatsapp/` if that is set) | "Remove everything", or deleting the folder |
208
+ | The linked device on the phone | WhatsApp → Settings → Linked devices | "Remove everything", or the user on their phone |
209
+ | A background service, only if one was set up | a systemd unit (usually `~/.config/systemd/user/*.service` or `/etc/systemd/system/*.service`), maybe lingering and a tmux session | you, step 3 |
210
+ | Copies of it and its npm packages | npm's download cache (`~/.npm/_cacache`) | "Remove everything" (only these entries) |
211
+ | Programs installed for it, only if they were | `ffmpeg`, `tmux`; listed in `setup.json` when recorded | "Remove everything" asks about each; or you, step 6, with the user's yes |
212
+
213
+ The extension writes nowhere else. It never edits shell profiles or creates files outside its data folder.
214
+
215
+ ### Steps
216
+
217
+ 1. **Look first, change nothing yet.**
218
+ - `pi list`: note the exact source (`npm:pi-harness-whatsapp`, or a `git:` source).
219
+ - The data folder: check `PI_HARNESS_WHATSAPP_DIR` and `PI_CODING_AGENT_DIR` in the environment; otherwise it's `~/.pi/agent/pi-harness-whatsapp/`.
220
+ - A service: `grep -rlE "pi-harness-whatsapp|PI_HARNESS_WHATSAPP|--whatsapp" ~/.config/systemd/user /etc/systemd/system 2>/dev/null`.
221
+ - Programs installed just for this: read `setup.json` in the data folder. If there's none, check your notes or ask the user.
222
+ 2. **Tell the user what will be removed and get a yes.** The WhatsApp login is deleted, so using it again means a new QR scan; the speech files would have to download again.
223
+ 3. **Stop the service, if there is one** (so it can't start Pi again): `systemctl --user disable --now <unit>` (system unit: `sudo systemctl disable --now <unit>`). Delete its unit file, then run `systemctl --user daemon-reload` (or `sudo systemctl daemon-reload`). Kill its tmux session if it's still there (`tmux ls`, then `tmux kill-session -t <name>`). If lingering was turned on only for this service (your notes, or ask), run `loginctl disable-linger`.
224
+ 4. **Clean up the data from inside Pi.** First ask the user to close every Pi window. Then either the user does it, or you do it through Pi's RPC mode.
225
+
226
+ **You, over RPC** (from the user's usual project folder, with no other Pi running): start `pi --mode rpc`, and send this line on its stdin:
227
+ ```json
228
+ {"id": "1", "type": "prompt", "message": "/whatsapp remove"}
229
+ ```
230
+ Pi then sends `extension_ui_request` records on stdout. For each one with `"method": "confirm"`, show the user its `title` and `message`, and answer with their decision, using the request's `id`:
231
+ ```json
232
+ {"type": "extension_ui_response", "id": "<the request's id>", "confirmed": true}
233
+ ```
234
+ The first question is "Remove everything?"; then one "Also remove …?" per program in `setup.json`. Records with `"method": "notify"` are progress and the result; the last one starts with `WhatsApp removed.` and lists what was done and any command left for you to run. Show it to the user, then close stdin so Pi quits. Read stdout as a stream and split records on `\n` only (see Pi's `docs/rpc.md`).
235
+
236
+ **Or the user**, in Pi:
237
+ 1. run `/whatsapp` → **Danger** → **Remove everything (before uninstalling)…** (or type `/whatsapp remove`) and confirm. It unlinks the phone, deletes the data folder, removes this extension's entries from npm's cache, and asks about each program in `setup.json` (removing exactly the recorded packages, after an apt dry run that keeps everything if anything else would go too),
238
+ 2. read you its summary (the last notice), then quit Pi.
239
+
240
+ **Already uninstalled?** The cleanest way is to install it again (`pi install npm:pi-harness-whatsapp`) and do the step above (over RPC, or the user); it then also cleans what the first uninstall left behind.
241
+
242
+ **By hand** (only if the user doesn't want that): read `setup.json` first (step 6 needs it). Make sure no Pi is running (the user quits them all; `connection.lock` in the data folder shows the pid of a Pi that holds WhatsApp), then delete the data folder: `rm -rf ~/.pi/agent/pi-harness-whatsapp`. **If `PI_HARNESS_WHATSAPP_DIR` points at a folder that also holds other things, delete only these entries in it:** `config.json*`, `recents.json*`, `auth/`, `models/`, `inbox/`, `log/`, `connection.lock`, `announce.json`, `setup.json`. This way, npm's cache keeps its copies: only "Remove everything" knows which entries are this extension's, and `npm cache clean --force` (only if the user asks) empties the whole cache. Then tell the user to unlink Pi on their phone: WhatsApp → Settings → Linked devices → tap the device that belongs to Pi → Log out.
243
+ 5. **Remove the code:** `pi uninstall <source from step 1>`, with Pi not running. npm leaves some empty folders behind (e.g. `@types`); remove only empty ones:
244
+ `find ~/.pi/agent/npm/node_modules ~/.pi/agent/git -mindepth 1 -type d -empty -delete 2>/dev/null`. Don't touch anything else in those folders; other Pi packages live there.
245
+ 6. **Programs:** "Remove everything" already asked about each `setup.json` entry; its summary says what was removed, and gives the command for anything it couldn't do (for example when `sudo` needs a password). Run those commands only if the user says yes. If you went the manual way, for each entry, with the user's yes: for apt, first run `apt-get -s remove --purge <packages>` and continue only if every line starting with `Remv` or `Purg` names a package from the entry; then `sudo apt-get remove --purge -y <packages>` and delete those packages' downloaded files, `/var/cache/apt/archives/<name>_*.deb`. Never remove a program that wasn't recorded or confirmed by the user; other programs may need it.
246
+ 7. **Verify, and show the user the output.** All of these must come back empty (or "No such file or directory"):
247
+ ```bash
248
+ pi list | grep -i whatsapp
249
+ ls -a ~/.pi/agent/pi-harness-whatsapp
250
+ find ~/.pi ~/.config/systemd /etc/systemd -iname "*pi-harness-whatsapp*" 2>/dev/null
251
+ find ~/.pi/agent/npm/node_modules ~/.pi/agent/git -mindepth 1 -type d -empty 2>/dev/null
252
+ grep -rlE "pi-harness-whatsapp|PI_HARNESS_WHATSAPP" ~/.config/systemd /etc/systemd ~/.bashrc ~/.profile ~/.zshrc 2>/dev/null
253
+ npm cache ls 2>/dev/null | grep -i "pi-harness-whatsapp"
254
+ ```
255
+ For each removed `setup.json` entry, `dpkg-query -W <packages>` (or your package manager's equivalent) must no longer show them as installed.
256
+ 8. **Report** what you removed, and what stays on purpose (below).
257
+
258
+ ### What stays, on purpose; don't delete it
259
+
260
+ - **Pi's sessions** (`~/.pi/agent/sessions/`). Conversations with Pi from WhatsApp are normal Pi sessions and contain small `pi-harness-whatsapp/session` entries (WhatsApp on/off). Pi ignores them without the extension. **Never edit session files**: entries are linked to each other, and cutting some out breaks the session. If the user wants those conversations gone, they delete whole sessions in Pi.
261
+ - **Other packages in npm's cache.** Only this extension's entries are removed. Emptying the whole cache (`npm cache clean --force`) is only for when the user asks for it.
262
+ - **npm's own logs** (`~/.npm/_logs`, its last few runs) may mention the install; npm replaces them as it runs again.
263
+ - **The package manager's history** (`/var/log/apt/history.log`, `/var/log/dpkg.log`, …) records installs and removals of every program. Never edit system logs.
141
264
 
142
265
  ## Troubleshooting
143
266
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.2 — 2026-10-16
4
+
5
+ - **Fixed file permissions:** the data folder and the WhatsApp login in it were readable by other users on the same machine. Every folder the extension creates is now private (only your user can open it), and existing installs are fixed the next time Pi starts.
6
+
7
+ ## 0.1.1 — 2026-10-15
8
+
9
+ - **Voice is optional, and off by default.** `/whatsapp` → Voice → **Voice notes** has two ticks: understand voice notes (speech-to-text, 1.0 GB) and reply with voice notes (text-to-speech, 0.6 GB), plus "Delete all speech files". Leaving the menu asks before deleting the files of a part you unticked ("You set speech-to-text for removal. 828 MB will be freed."; No puts the tick back) and offers to download a part you ticked (now, or when first needed). Nothing is downloaded while voice is off.
10
+ - While understanding is off, a voice note gets "🎤 Voice notes are off. Please type your message." (in groups, only one that replies to Pi) and never reaches the model.
11
+ - Reply type, the voices and the voice speed are greyed out until voice replies are on, and say which tick turns them on. `/voice` and `/speed` on WhatsApp say so instead of changing anything; `/help` and `/status` show both switches.
12
+ - The panel has a VOICE section and a DANGER section (Remove everything, and where the log file is); the Speech files row is gone.
13
+ - Existing setups keep the voice parts whose files are already on disk. Leftovers from older versions (Piper voices, older Whisper models, unfinished downloads nothing needs) are deleted on start.
14
+ - README and AGENT_INSTALL: what voice costs in disk and RAM per part, the new settings, and an install question about voice.
15
+
16
+ - **Uninstall without leftovers:** `/whatsapp` → Connection → **Remove everything (before uninstalling)**, or `/whatsapp remove`, unlinks Pi from the phone (connecting first if needed), stops running downloads, deletes every file the extension made, removes its copies (and its dependencies') from npm's download cache, and keeps Pi from writing any more until it quits. It refuses while another Pi holds WhatsApp. AI agents can run it over Pi's RPC mode.
17
+ - **`setup.json`:** programs installed just for the extension (ffmpeg, tmux) are recorded with the exact packages that came with them; Remove everything asks about each and removes exactly those (apt: after a dry run that keeps everything if anything else would go too; Homebrew: `brew uninstall`; otherwise it shows the command). AGENT_INSTALL tells agents how to record them.
18
+ - The README and AGENT_INSTALL describe the full uninstall, the background service, what stays on purpose and how to check that nothing is left.
19
+ - A Pi that never turned WhatsApp on no longer creates a log or recent-chats file when it quits.
20
+ - README: a section on sending and receiving images and files.
21
+ - The examples in Pi's instructions for spoken replies are in English.
22
+
3
23
  ## 0.1.0 — 2026-10-15
4
24
 
5
25
  First release.
package/README.md CHANGED
@@ -6,23 +6,36 @@
6
6
 
7
7
  Talk to the [Pi coding agent](https://pi.dev) from WhatsApp: text, voice notes, images and files. Pi answers automatically, and you can switch models, compact the context or stop a run from your phone.
8
8
 
9
- > 🤖 **AI agent installing this for someone?** Read [AGENT_INSTALL.md](AGENT_INSTALL.md) first: what to ask the user, how to configure, and what never to touch.
9
+ > 🤖 **AI agent installing or removing this for someone?** Read [AGENT_INSTALL.md](AGENT_INSTALL.md) first: what to ask the user, how to configure, how to uninstall without leaving anything behind, and what never to touch.
10
10
 
11
11
  - **Scan a QR code once and you're in.** Pi runs as a linked device of a WhatsApp number. The login is saved, and Pi reconnects on its own.
12
12
  - **Replies are automatic.** Pi's final answer is always sent back. The model doesn't have to call a tool.
13
13
  - **You choose what else you get:** a summary line (`✅ 4/6 tool calls ok · ⏱ 2m 14s`), in-between messages, the list of touched files, and a live tool-activity message.
14
- - **Voice notes both ways, on your own CPU.** [Whisper](https://github.com/openai/whisper) turbo understands them and [Supertonic 3](https://huggingface.co/Supertone/supertonic-3) speaks the replies, both through Intel's [OpenVINO](https://github.com/openvinotoolkit/openvino). No GPU or cloud service needed, in 15 languages.
14
+ - **Optional voice notes, both ways, on your own CPU.** [Whisper](https://github.com/openai/whisper) turbo understands them and [Supertonic 3](https://huggingface.co/Supertone/supertonic-3) speaks the replies, both through Intel's [OpenVINO](https://github.com/openvinotoolkit/openvino). No GPU or cloud service needed, in 15 languages. Off by default; see below for what it costs.
15
+ - **Images and files both ways.** Send Pi a photo or screenshot and it sees it; send a file (PDF, log, zip, …) and it's saved for Pi to open. Ask Pi for an image or a file and it sends it to your chat, and screenshots its tools take can come automatically.
15
16
  - **Control Pi from WhatsApp** with commands and poll menus: `/model`, `/think`, `/compact`, `/stop`, `/new`, `/status`, …
16
17
  - **A real off switch.** Off means disconnected: nothing is read or sent, and the WhatsApp tools disappear from the model.
17
18
 
19
+ > **Voice is optional and off by default.** Turn it on in `/whatsapp` → **Voice notes**, one part or both:
20
+ >
21
+ > | Part | Disk | RAM while a voice note is handled |
22
+ > |---|---|---|
23
+ > | Understand voice notes (speech-to-text) | 1.0 GB | about 2 GB |
24
+ > | Reply with voice notes (text-to-speech) | 0.6 GB | about 1.2 GB |
25
+ > | Both | 1.4 GB (they share a 202 MB engine) | about 3.4 GB |
26
+ >
27
+ > The files download only once you switch a part on, and switching it off deletes them again. The models are unloaded after 10 minutes without voice work, so idle RAM use is zero. Everything runs on the CPU; nothing goes to the cloud.
28
+
18
29
  > **Use a second number if you can.** This extension uses [Baileys](https://github.com/WhiskeySockets/Baileys), an unofficial WhatsApp Web client. WhatsApp can restrict numbers that use unofficial clients, so a spare number is the safe choice.
19
30
 
20
31
  ## Requirements
21
32
 
22
33
  - Pi 1.2 or newer
23
- - `ffmpeg` on the PATH (for voice notes): `sudo apt install ffmpeg`
24
- - For voice notes: Linux (x64 or ARM64), Windows (x64) or macOS on Apple Silicon. OpenVINO has no build for Intel Macs or Windows on ARM; text works there, voice notes don't.
25
- - For voice notes: about 3.5 GB of free RAM while they're being processed, and 1.5 GB of disk
34
+
35
+ Only if you turn voice on:
36
+ - `ffmpeg` on the PATH: `sudo apt install ffmpeg`
37
+ - Linux (x64 or ARM64), Windows (x64) or macOS on Apple Silicon. OpenVINO has no build for Intel Macs or Windows on ARM; text, images and files work there, voice notes don't.
38
+ - The disk and free RAM from the table above.
26
39
 
27
40
  ## Install
28
41
 
@@ -78,21 +91,23 @@ Every row opens a small menu with the choices and a short explanation. Nothing c
78
91
  | | Who can use commands | **Only me** (the owner) · all allowed chats. |
79
92
  | | Ask on/off at session start | On · **Off**. Asks whether WhatsApp should be on when a session opens. |
80
93
  | Replies | Final answer | Always on. |
81
- | | Reply type | **Mirror me** (voice note in → voice reply) · text · voice · voice + text. |
82
94
  | | Terminal answers | On · **Off**. Also sends the owner Pi's answers to prompts typed in the terminal, as text. |
83
95
  | | Progress messages | On · **Off**. What Pi writes while it's still working. |
84
96
  | | Live tool log | On · **Off**. One message, updated live, with the latest tool steps. |
85
97
  | | Changed files | On · **Off**. The files Pi wrote or edited. |
86
98
  | | Stats (tools · time) | On · **Off**. A line like `✅ 4/6 tool calls ok · ⏱ 2m 14s`. |
87
99
  | | Auto-send tool images | On · **Off**. Every image a tool makes while answering you (e.g. browser screenshots). You can always ask Pi to send a specific image instead. |
100
+ | Voice | Voice notes | Two ticks, both **off**: **Understand voice notes** (1.0 GB) and **Reply with voice notes** (0.6 GB), plus **Delete all speech files**. See [Voice](#voice). |
101
+ | | Reply type | **Mirror me** (voice note in → voice reply, text in → text reply) · text · voice · voice + text. Greyed out until "Reply with voice notes" is on. Without "Understand voice notes", mirror me answers everything with text (Pi gets no voice notes to mirror), so pick voice if you want spoken replies. |
102
+ | | Voice per language | Which voice speaks each language: one of 10 voices, **F1**–F5 (female) and M1–M5 (male), the same list for every language. "Send me a sample" sends a short voice note to your WhatsApp first. Greyed out until "Reply with voice notes" is on. |
103
+ | | Voice reply speed | 0.9 · **1.0** · 1.2 · custom (0.5–2.0). Greyed out until "Reply with voice notes" is on. |
88
104
  | Languages | Reply language | **Mirror me** (the language you used) or one of your languages. |
89
105
  | | My languages | Tick the languages you use (**English**). See [Voice](#voice) for the 15 languages. |
90
106
  | | Fallback language | Used only when Pi can't tell which language to use (**the first of your languages**). |
91
- | | Voice per language | Which voice speaks each language: one of 10 voices, **F1**–F5 (female) and M1–M5 (male), the same list for every language. "Send me a sample" sends a short voice note to your WhatsApp first. |
92
- | Technical | Voice reply speed | 0.9 · **1.0** · 1.2 · custom (0.5–2.0). |
93
- | | Speech files | Download what your settings need, or delete unused files (shown as a ticked list; untick what you want to keep). |
107
+ | Danger | Remove everything (before uninstalling) | Unlinks Pi from your phone and deletes everything it made. Asks first. See [Uninstall](#uninstall). |
108
+ | | Log file | Where the log is (shown, not a setting). |
94
109
 
95
- Speech files are never downloaded just because you look at a choice. When you close the panel and your new settings need files, Pi asks once: download now, or later when first needed.
110
+ Greyed-out rows can still be selected: they say which tick in **Voice notes** turns them on.
96
111
 
97
112
  Settings are stored in `~/.pi/agent/pi-harness-whatsapp/config.json`.
98
113
 
@@ -103,13 +118,13 @@ Commands are handled by the extension and never reach the model. Send `/help` to
103
118
  | Command | What it does |
104
119
  |---|---|
105
120
  | `/help` | Commands and current settings |
106
- | `/status` | Connection, model, thinking level, context use, voice models |
121
+ | `/status` | Connection, model, thinking level, context use, voice |
107
122
  | `/stop` | Stop what Pi is doing |
108
123
  | `/compact [focus]` | Compact the context, then report e.g. `✅ Compacted in 18s · 142k → 23k tokens (−84%) · context 89% free` |
109
124
  | `/autocompact [on\|off]` | Automatic compaction for this session (Pi still compacts if the context is completely full) |
110
125
  | `/model [name]` | Switch the model for this session; lists only models with a login or API key |
111
126
  | `/think [level]` | Reasoning level, limited to what the model supports |
112
- | `/voice [text\|voice\|both\|mirror]` · `/lang` · `/speed [x]` | Reply type, reply language, voice reply speed |
127
+ | `/voice [text\|voice\|both\|mirror]` · `/lang` · `/speed [x]` | Reply type, reply language, voice reply speed. `/voice` and `/speed` work once voice replies are on; voice itself is switched on only in the terminal, since it downloads files. |
113
128
  | `/replies` | Switch the reply extras on and off (multi-choice poll) |
114
129
  | `/languages` | Show your languages (change them in the terminal) |
115
130
  | `/new` | Start a fresh Pi session (the old one stays saved) |
@@ -121,6 +136,18 @@ Commands are handled by the extension and never reach the model. Send `/help` to
121
136
 
122
137
  ## Voice
123
138
 
139
+ Voice is optional and **off by default**. In `/whatsapp` → **Voice notes**, tick what you want:
140
+
141
+ - **Understand voice notes** (speech-to-text, 1.0 GB): Pi transcribes the voice notes you send. While it's off, a voice note gets "🎤 Voice notes are off. Please type your message." and never reaches the model; in a group, only a voice note that replies to Pi gets that answer.
142
+ - **Reply with voice notes** (text-to-speech, 0.6 GB): Pi can answer with voice notes; **Reply type**, the voices and the speed then become available. While it's off, Pi always answers with text.
143
+
144
+ Downloads and deletions happen when you leave that menu:
145
+ - **A part you ticked** whose files aren't there yet: Pi asks "Download now (recommended)" or "When first needed". "When first needed" downloads with the first voice note (the chat is told while it waits).
146
+ - **A part you unticked:** Pi asks, e.g. "You set speech-to-text for removal. 828 MB will be freed." Yes deletes the files; No puts the tick back. The shared engine stays as long as the other part needs it.
147
+ - **"Delete all speech files"** unticks both, and asks the same way.
148
+
149
+ You don't have to untick anything to stop voice replies for a while: set **Reply type** to text.
150
+
124
151
  Voice notes are transcribed on your machine and given to Pi with a label, e.g. `[WhatsApp · Name · voice note · your reply is spoken to Name] …`. When the reply will be spoken, the model is told so, and writes plain speech. Long answers are read in full as several voice notes (up to ~10 minutes; anything longer also comes as text). Answers that are mostly code come as text too, since code can't be read aloud. With **Mirror me**, Pi answers in the language you used, and the voice reply uses that language's voice.
125
152
 
126
153
  Supported languages: **English, Turkish, German, French, Spanish, Italian, Portuguese, Dutch, Polish, Russian, Ukrainian, Arabic, Hindi, Swedish and Finnish**. Pi understands and speaks all of them with the same two models, so adding a language downloads nothing.
@@ -131,7 +158,7 @@ Supported languages: **English, Turkish, German, French, Spanish, Italian, Portu
131
158
  | Speaking replies | [Supertonic 3](https://huggingface.co/Supertone/supertonic-3) by Supertone (10 voices) | 401 MB |
132
159
  | Running both | Intel's OpenVINO for Node | 202 MB |
133
160
 
134
- Everything downloads the first time it's needed, and the chat is told while it waits. If you change languages, voices or the reply type in `/whatsapp`, Pi asks when you close the panel whether to download the new files now. Models are unloaded after 10 minutes without voice work, so idle RAM use is zero. While voice notes are being handled, Pi used about 3.4 GB of RAM in total in the test below.
161
+ Models are unloaded after 10 minutes without voice work, so idle RAM use is zero. While a voice note is handled, Pi needed about 2 GB of RAM to understand it, about 1.2 GB to speak a reply, and about 3.4 GB with both, measured on the machine below.
135
162
 
136
163
  Measured on an Intel i5-13500, CPU only, on real recordings from Google's [FLEURS](https://huggingface.co/datasets/google/fleurs) dataset: 10 sentences per language, read by real people. Wrong words are counted against the dataset's transcript, ignoring punctuation and capitals; a number written as digits ("18") where the transcript spells it out counts as wrong. For the voice, Supertonic read a short sentence in each language and Whisper listened to it again.
137
164
 
@@ -153,18 +180,23 @@ Measured on an Intel i5-13500, CPU only, on real recordings from Google's [FLEUR
153
180
  | Swedish | 12 % | 2.9 s | ✓ |
154
181
  | Hindi | 31 % | 3.2 s | 1 word off |
155
182
 
156
- Supertonic makes speech about 10× faster than real time, so a 20-second reply is ready in about 2 seconds. Symbols and units are hard for any voice to read ("13°C" is not "13 derece"), so when a reply will be spoken, Pi is told to write everything out as it should be said.
183
+ Supertonic makes speech about 10× faster than real time, so a 20-second reply is ready in about 2 seconds. Symbols and units are hard for any voice to read ("13°C" is not "13 degrees"), so when a reply will be spoken, Pi is told to write everything out as it should be said.
157
184
 
158
- Received files are saved in `~/.pi/agent/pi-harness-whatsapp/inbox/` (kept for 7 days), and Pi is told where they are.
185
+ ## Images and files
159
186
 
160
- ## Tools for the model
187
+ **To Pi:**
188
+ - **Images** (photos, screenshots) go to the model with their caption, so you can ask "what's wrong in this screenshot?". In a group, an image counts when its caption @mentions Pi, or when you reply to an image and @mention Pi.
189
+ - **Files** (PDFs, documents, logs, archives, videos; up to 50 MB) are saved in `~/.pi/agent/pi-harness-whatsapp/inbox/`, and Pi is told the name, type, size and where it is, so it can open it with its own tools. Received files are deleted after 7 days.
190
+ - **Voice notes** are transcribed when **Understand voice notes** is on; see [Voice](#voice).
161
191
 
162
- These are optional; normal replies never need them. They are visible to the model only while WhatsApp is on, and they only send to allowed contacts.
192
+ **From Pi:** ask for it ("send me the screenshot", "send me report.pdf") and Pi sends images and files from this computer to your chat, up to 60 MB. With **Auto-send tool images** on, every image a tool makes while answering you (e.g. a browser screenshot) is sent without asking.
163
193
 
164
- - `whatsapp_send_image(path, caption?, to?)`
165
- - `whatsapp_send_file(path, caption?, to?)`
194
+ Pi does this with two tools that the model sees only while WhatsApp is on, and that send only to allowed chats:
166
195
 
167
- There's no voice tool on purpose: when your reply is spoken, Pi's answer itself becomes the voice note, so the model can't send the same thing twice.
196
+ - `whatsapp_send_image(path, caption?, to?)`: a PNG, JPEG, WebP or GIF, shown as a photo
197
+ - `whatsapp_send_file(path, caption?, to?)`: any file, sent as a document
198
+
199
+ `to` is an allowed contact's name or number; without it, Pi sends to the chat it's answering (or the owner). Normal replies never need these tools. There's no voice tool on purpose: when your reply is spoken, Pi's answer itself becomes the voice note, so the model can't send the same thing twice.
168
200
 
169
201
  ## Run in the background
170
202
 
@@ -178,11 +210,68 @@ Everything lives in `~/.pi/agent/pi-harness-whatsapp/`:
178
210
  |---|---|
179
211
  | `config.json` | Settings |
180
212
  | `auth/` | The WhatsApp login. Keep it private; deleting it means scanning a new QR code. |
181
- | `models/` | Speech models and the OpenVINO engine that runs them |
213
+ | `models/` | Speech models and the OpenVINO engine that runs them (only while voice is on) |
182
214
  | `inbox/` | Files people sent (kept 7 days) |
183
215
  | `recents.json` | Recent chat ids and names, for picking contacts (no message text) |
184
216
  | `connection.lock` | Which Pi process holds the connection right now |
185
- | `log/pi-harness-whatsapp.log` | Log: connection events and errors, no message text (unless debug logging is on) |
217
+ | `announce.json` | For a moment around `/new` and `/restart`: which chat to tell that Pi is back |
218
+ | `setup.json` | Only if the installer recorded it: programs installed just for the extension (see [Uninstall](#what-was-installed-for-it)) |
219
+ | `log/pi-harness-whatsapp.log` | Log: connection events and errors, no message text (unless debug logging is on). Rotated at 5 MB into `.log.1`. |
220
+
221
+ The folder is private: only your user can open it. If you set `PI_HARNESS_WHATSAPP_DIR`, everything is in that folder instead. The extension writes nothing anywhere else, apart from Pi's own files (your sessions and its package list).
222
+
223
+ ## Uninstall
224
+
225
+ Do it in this order, so nothing is left behind.
226
+
227
+ 1. **Clean up from inside Pi.** Close every other Pi window first. Then run `/whatsapp` → **Danger** → **Remove everything (before uninstalling)…** (or type `/whatsapp remove`) and confirm. Pi:
228
+ - unlinks itself from your phone (WhatsApp → Settings → Linked devices),
229
+ - stops any speech-file download that's still running,
230
+ - deletes `~/.pi/agent/pi-harness-whatsapp/` with everything in it: the WhatsApp login, speech files, settings, allowed chats, recent chats, log and received files,
231
+ - deletes this extension's copies (and those of the packages it brought along) from npm's download cache, and nothing else there,
232
+ - asks about each program that was installed just for it, if the installer recorded one (see [What was installed for it](#what-was-installed-for-it)), and removes exactly the packages you say yes to,
233
+ - and writes nothing more until you quit, even if you keep using Pi.
234
+ 2. **If Pi runs as a background service, stop and remove it now** (see [Removing the background service](#removing-the-background-service)); otherwise systemd starts Pi again.
235
+ 3. **Quit Pi and remove the code:**
236
+ ```bash
237
+ pi uninstall npm:pi-harness-whatsapp
238
+ ```
239
+ If you installed it from GitHub, use the source that `pi list` shows (e.g. `pi uninstall git:github.com/mkemalemdar/pi-harness-whatsapp`).
240
+ 4. **Tidy up after npm.** npm removes every file, but leaves a few empty folders (like `@types`) in Pi's package folder. This deletes only folders that are empty:
241
+ ```bash
242
+ find ~/.pi/agent/npm/node_modules ~/.pi/agent/git -mindepth 1 -type d -empty -delete 2>/dev/null
243
+ ```
244
+
245
+ **Check that it's gone:** `pi list` no longer shows it, and `ls ~/.pi/agent/pi-harness-whatsapp` says "No such file or directory".
246
+
247
+ ### Already uninstalled without step 1?
248
+
249
+ The easiest clean way: install it again (`pi install npm:pi-harness-whatsapp`), start Pi, and do the steps above. "Remove everything" then also cleans up what the first uninstall left.
250
+
251
+ By hand instead: make sure no Pi is running, and look at `~/.pi/agent/pi-harness-whatsapp/setup.json` first, if it's there, for programs installed for the extension. Then delete the folder: `rm -rf ~/.pi/agent/pi-harness-whatsapp` (if you set `PI_HARNESS_WHATSAPP_DIR` to a folder that holds other things too, delete only the entries listed under [Files](#files)). On your phone, remove Pi under WhatsApp → Settings → Linked devices.
252
+
253
+ ### Removing the background service
254
+
255
+ Use your unit's name. For a system service, run the same with `sudo` and without `--user`; its file is in `/etc/systemd/system/`.
256
+ ```bash
257
+ systemctl --user disable --now pi-whatsapp.service
258
+ rm ~/.config/systemd/user/pi-whatsapp.service
259
+ systemctl --user daemon-reload
260
+ ```
261
+ If you turned on lingering only for this service, turn it off again with `loginctl disable-linger`. Remove the tmux session it used with `tmux kill-session -t <name>` if it's still running.
262
+
263
+ ### What was installed for it
264
+
265
+ The extension itself never installs programs; you or an AI agent install `ffmpeg` (and `tmux` for the background service) when they're missing. Whoever does that can record it in `setup.json` in the data folder, and "Remove everything" then offers to remove it again. [AGENT_INSTALL.md](AGENT_INSTALL.md) has the format, and AI agents installing the extension write it for you.
266
+
267
+ Removal is careful: with apt, it first does a dry run and keeps everything if removing those packages would also remove anything not on the list (like a program you installed later that needs them). It runs as root or with `sudo` that needs no password; otherwise it shows you the command. With Homebrew it runs `brew uninstall`; with other package managers it shows the command.
268
+
269
+ ### What isn't removed, on purpose
270
+
271
+ - **Your Pi sessions** (`~/.pi/agent/sessions/`). Chats you had with Pi from WhatsApp are ordinary Pi sessions, and they contain small `pi-harness-whatsapp/session` entries (whether WhatsApp was on). Pi ignores those once the extension is gone; cutting them out would break the sessions. Delete whole sessions in Pi if you don't want them.
272
+ - **Programs nobody recorded** in `setup.json`: if you installed `ffmpeg` or `tmux` yourself just for this, remove them with your package manager (e.g. `sudo apt remove --purge ffmpeg tmux`, then `sudo apt autoremove` for what came with them, after checking its list).
273
+ - **npm's own logs** (`~/.npm/_logs`): npm keeps a log of its last few runs, which can mention the install. They're replaced as npm runs again.
274
+ - **Your package manager's history** (e.g. `/var/log/apt/history.log`, `/var/log/dpkg.log`) notes every install and removal, like it does for every program.
186
275
 
187
276
  ## Troubleshooting
188
277
 
@@ -190,7 +279,8 @@ Everything lives in `~/.pi/agent/pi-harness-whatsapp/`:
190
279
  - **"needs a new QR scan":** WhatsApp logged the device out (for example, it was removed from Linked devices). Connection → Show a new QR code.
191
280
  - **"in use by Pi #…":** another Pi holds the connection; turn WhatsApp off there.
192
281
  - **Messages are ignored:** the sender isn't in Allowed chats. Pi shows a notice in the terminal the first time.
193
- - **Voice notes fail:** check that `ffmpeg -version` works. The first voice note waits for the speech files to download (about 1.4 GB); a download that stalls for a minute is given up and tried again with the next voice note. On Intel Macs and Windows on ARM, voice notes aren't available.
282
+ - **"Voice notes are off":** tick **Understand voice notes** in `/whatsapp` → Voice notes.
283
+ - **Voice notes fail:** check that `ffmpeg -version` works. With "When first needed", the first voice note waits for the download (up to 1.4 GB); a download that drops continues where it stopped, and one that stalls for a minute is given up and tried again with the next voice note. On Intel Macs and Windows on ARM, voice notes aren't available.
194
284
  - **More detail:** set `PI_HARNESS_WHATSAPP_DEBUG=1` before starting Pi, and check the log. Debug logging also writes WhatsApp's own internal messages, which can contain message content, so turn it off again afterwards.
195
285
 
196
286
  ## Development
package/index.ts CHANGED
@@ -22,7 +22,7 @@ export default function piHarnessWhatsApp(pi: ExtensionAPI) {
22
22
  controller.onToolsChanged = (on) => setToolsActive(pi, on);
23
23
 
24
24
  pi.registerCommand("whatsapp", {
25
- description: "WhatsApp settings: on/off, replies, voice, languages, contacts, connection",
25
+ description: "WhatsApp settings: on/off, replies, voice, languages, contacts, connection. /whatsapp remove: remove everything before uninstalling",
26
26
  handler: async (args, ctx) => {
27
27
  // Internal: "/new" from WhatsApp dispatches this so Pi gives us a real command context.
28
28
  if (args.trim() === NEW_SESSION_ARG) {
@@ -30,6 +30,11 @@ export default function piHarnessWhatsApp(pi: ExtensionAPI) {
30
30
  await ctx.newSession();
31
31
  return;
32
32
  }
33
+ // Same as Connection → Remove everything; works without the panel (e.g. for an AI agent over RPC).
34
+ if (args.trim() === "remove") {
35
+ await controller.removeEverything(ctx);
36
+ return;
37
+ }
33
38
  await controller.openSettings(ctx);
34
39
  },
35
40
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-harness-whatsapp",
3
- "version": "0.1.0",
4
- "description": "Talk to the Pi coding agent from WhatsApp: QR pairing, automatic replies, voice notes in and out (local Whisper + Supertonic on OpenVINO), group chats, poll menus to control Pi.",
3
+ "version": "0.1.2",
4
+ "description": "Talk to Pi from WhatsApp: images, files and optional voice notes in and out, automatic replies, group chats, poll menus to control Pi.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "mkemalemdar",
package/src/bridge.ts CHANGED
@@ -12,7 +12,7 @@ import { contactLabel } from "./contacts-flow.ts";
12
12
  import { languageLabel } from "./languages.ts";
13
13
  import { createLogger } from "./log.ts";
14
14
  import { formatActivity, formatTouchedFiles, RunReport } from "./run-report.ts";
15
- import { type Contact, MIRROR, type SettingsStore } from "./settings.ts";
15
+ import { type Contact, MIRROR, replyMode, type SettingsStore } from "./settings.ts";
16
16
  import type { WhatsAppConnection } from "./wa/connection.ts";
17
17
  import { type Access, addressedToSelf, checkAccess, type InboundMessage, parseMessage, SeenIds, senderIdentity } from "./wa/inbound.ts";
18
18
  import { contactJid, isLidJid, jidUser, phoneJid } from "./wa/jid.ts";
@@ -33,6 +33,8 @@ const BUSY_POLL_MS = 500;
33
33
  /** Group messages not meant for Pi that are kept as context for when it is addressed. */
34
34
  const GROUP_CONTEXT_MESSAGES = 20;
35
35
  const CUT_OFF_NOTICE = "⚠️ The model hit its output limit before writing an answer. Try again, or raise the model's output limit.";
36
+ /** The answer to a voice note while "Understand voice notes" is off. */
37
+ export const VOICE_OFF_NOTICE = "🎤 Voice notes are off. Please type your message.";
36
38
 
37
39
  type PromptContent = string | Array<{ type: "text"; text: string } | { type: "image"; data: string; mimeType: string }>;
38
40
 
@@ -214,9 +216,18 @@ export class Bridge {
214
216
  return;
215
217
  }
216
218
 
219
+ // Voice notes while "Understand voice notes" is off: nothing reaches the model and nothing downloads.
220
+ const voiceOff = message.kind === "audio" && !this.deps.settings.get().voice.understand;
217
221
  if (message.isGroup) {
218
222
  // WhatsApp writes a mention as "@<number>"; the model reads "@Pi" better.
219
223
  message.text = this.namePi(message.text);
224
+ if (voiceOff) {
225
+ // In a group, only a voice note that replies to Pi gets the answer; others are just context.
226
+ const { repliedTo } = addressedToSelf(message, this.deps.connection.selfIds(), (id) => this.deps.connection.isOwnMessage(id));
227
+ if (repliedTo) await this.deps.queue.sendText(message.chatJid, VOICE_OFF_NOTICE);
228
+ else this.rememberGroupMessage(message);
229
+ return;
230
+ }
220
231
  if (!this.isForPi(message)) {
221
232
  // Not meant for Pi: keep it as context for the next time Pi is addressed.
222
233
  this.rememberGroupMessage(message);
@@ -230,6 +241,13 @@ export class Bridge {
230
241
  await this.forwardToPi(message, access);
231
242
  }
232
243
 
244
+ /** Voice notes while "Understand voice notes" is off get a short answer instead of reaching Pi. */
245
+ private async answeredVoiceOff(message: InboundMessage): Promise<boolean> {
246
+ if (message.kind !== "audio" || this.deps.settings.get().voice.understand) return false;
247
+ await this.deps.queue.sendText(message.chatJid, VOICE_OFF_NOTICE);
248
+ return true;
249
+ }
250
+
233
251
  /** In a group: does this message ask Pi something (per "In groups, answer")? Commands and menu answers always do. */
234
252
  private isForPi(message: InboundMessage): boolean {
235
253
  const mode = this.deps.settings.get().groupReplies;
@@ -391,7 +409,7 @@ export class Bridge {
391
409
  const from = message.isGroup ? `WhatsApp group "${access.contact?.name ?? "group"}" · ${sender}` : `WhatsApp · ${sender}`;
392
410
  const to = message.isGroup ? "the group" : sender;
393
411
  return (voiceTag) => {
394
- const mode = this.deps.settings.get().voiceMode;
412
+ const mode = replyMode(this.deps.settings.get());
395
413
  const spoken = mode === "voice" || mode === "voice+text" || (mode === "mirror" && !!voiceTag);
396
414
  return `[${[from, voiceTag, spoken ? `your reply is spoken to ${to}` : `your reply goes to ${to}`].filter(Boolean).join(" · ")}]`;
397
415
  };
@@ -399,6 +417,7 @@ export class Bridge {
399
417
 
400
418
  private async forwardToPi(message: InboundMessage, access: Access): Promise<void> {
401
419
  if (!this.deps.getCtx()) return;
420
+ if (await this.answeredVoiceOff(message)) return;
402
421
  void this.deps.connection.presence(message.chatJid, "composing");
403
422
  const header = this.header(message, access);
404
423
  const context = message.isGroup ? this.takeGroupContext(message, access) : undefined;
@@ -482,8 +501,14 @@ export class Bridge {
482
501
  "- Name is talking to you directly from their phone. Answer Name directly, as \"you\" — don't describe their message to someone else or ask what to reply to them.",
483
502
  "- Your final answer is sent to them automatically. Don't use a tool just to reply.",
484
503
  "- Keep answers short and chat-friendly. Summarize long code, and write files to disk instead.",
485
- "- Voice notes arrive already transcribed, e.g. `[WhatsApp · Name · voice note · your reply is spoken to Name]`. Transcripts can contain misheard words.",
486
- "- When the label says \"your reply is spoken\", your answer becomes voice notes automatically (long answers are split into parts). Write it exactly as it should be said: no symbols, units or abbreviations (\"13 derece\", not \"13°C\"; \"yüzde 40\", not \"%40\"; \"25 kilometers\", not \"25 km\"), and no code, tables, links or emoji.",
504
+ ...(settings.voice.understand
505
+ ? ["- Voice notes arrive already transcribed, e.g. `[WhatsApp · Name · voice note · your reply goes to Name]`. Transcripts can contain misheard words."]
506
+ : []),
507
+ ...(settings.voice.reply
508
+ ? [
509
+ "- When the label says \"your reply is spoken\", your answer becomes voice notes automatically (long answers are split into parts). Write it exactly as it should be said: no symbols, units or abbreviations (\"13 degrees\", not \"13°C\"; \"40 percent\", not \"40%\"; \"25 kilometers\", not \"25 km\"), and no code, tables, links or emoji.",
510
+ ]
511
+ : []),
487
512
  "- Group labels look like `[WhatsApp group \"Team\" · Name · your reply goes to the group]`: answer the group. You only get group messages that mention or reply to you; the ones before it may come first as `earlier messages, for context`. Use them to understand the question, but answer only the last message.",
488
513
  `- ${language}`,
489
514
  ].join("\n");