@craftsamo/whatsapp-tools 0.0.0-stage → 0.1.0
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/.agents/plugins/marketplace.json +20 -0
- package/.antigravity-plugin/plugin.json +12 -0
- package/.claude-plugin/marketplace.json +17 -0
- package/.claude-plugin/plugin.json +18 -0
- package/.codex-plugin/plugin.json +31 -0
- package/.mcp.json +8 -0
- package/LICENSE +429 -0
- package/README.md +569 -2
- package/__init__.py +36 -0
- package/dist/cli.js +30714 -0
- package/dist/mcp.js +37673 -0
- package/dist/opencode.js +27584 -0
- package/mcp_config.json +8 -0
- package/package.json +70 -4
- package/plugin.yaml +46 -0
- package/skills/.gitkeep +0 -0
- package/skills/whatsapp-account/SKILL.md +55 -0
- package/skills/whatsapp-actions/SKILL.md +109 -0
- package/skills/whatsapp-chats/SKILL.md +125 -0
- package/skills/whatsapp-contacts/SKILL.md +59 -0
- package/skills/whatsapp-count-activity/SKILL.md +63 -0
- package/skills/whatsapp-files/SKILL.md +112 -0
- package/skills/whatsapp-sends/SKILL.md +50 -0
- package/src/hermes/whatsapp_tools_hermes/__init__.py +5 -0
- package/src/hermes/whatsapp_tools_hermes/approval.py +330 -0
- package/src/hermes/whatsapp_tools_hermes/card.py +507 -0
- package/src/hermes/whatsapp_tools_hermes/context.py +34 -0
- package/src/hermes/whatsapp_tools_hermes/listing.py +78 -0
- package/src/hermes/whatsapp_tools_hermes/plugin.py +38 -0
- package/src/hermes/whatsapp_tools_hermes/project.py +47 -0
- package/src/hermes/whatsapp_tools_hermes/runtime.py +204 -0
- package/src/hermes/whatsapp_tools_hermes/settings.py +171 -0
- package/src/hermes/whatsapp_tools_hermes/skills.py +131 -0
- package/src/hermes/whatsapp_tools_hermes/tools.py +97 -0
package/README.md
CHANGED
|
@@ -1,3 +1,570 @@
|
|
|
1
|
-
#
|
|
1
|
+
# whatsapp-tools
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
WhatsApp tools for coding agents: chats, messages, contacts, history, received files, sends and actions on the user's own messages and chats.
|
|
4
|
+
|
|
5
|
+
| Harness | Delivery | Pinned by |
|
|
6
|
+
| -------------- | ---------------------------------------------- | ------------------------------ |
|
|
7
|
+
| OpenCode V2 | native plugin (npm package, with skills) | the version in the package |
|
|
8
|
+
| OpenCode V1 | native plugin (npm package, no skills) | the version in the package |
|
|
9
|
+
| Claude Code | plugin: MCP server and skills | the release tag, as the ref |
|
|
10
|
+
| Codex | plugin: MCP server and skills | the release tag, as the ref |
|
|
11
|
+
| Grok Build | plugin: MCP server and skills | the release tag, as `@ref` |
|
|
12
|
+
| Antigravity | plugin: MCP server and skills | the release tag's tree URL |
|
|
13
|
+
| Hermes Agent | native plugin (Python, calls the command line) | the release commit, as the SHA |
|
|
14
|
+
| other MCP host | MCP server from npm | the version in the package |
|
|
15
|
+
|
|
16
|
+
The tools need [Node.js](https://nodejs.org/) 24 or later.
|
|
17
|
+
[`docs/conventions.md`](docs/conventions.md) describes how the tools are named
|
|
18
|
+
and how they behave.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
Install from a release, never from the default branch: a release is a tag whose
|
|
23
|
+
commit carries the built `dist/`, and the default branch does not. In the
|
|
24
|
+
commands below, `<version>` is a release's version (`1.2.3`) and `<sha>` is the
|
|
25
|
+
commit its tag names:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
gh api repos/craft-agent/whatsapp-tools/commits/v<version> --jq .sha
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### OpenCode
|
|
32
|
+
|
|
33
|
+
OpenCode V2 takes the package and its options in `opencode.json`:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"plugins": [{ "package": "@craftsamo/whatsapp-tools@<version>", "options": { "read_only": false } }],
|
|
38
|
+
"permissions": [{ "action": "whatsapp_*", "resource": "*", "effect": "ask" }]
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
OpenCode V1 takes the package as a string, or as a pair with its options:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"plugin": [["@craftsamo/whatsapp-tools@<version>", { "read_only": false }]],
|
|
47
|
+
"permission": { "whatsapp_*": "ask" }
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
OpenCode allows any action no rule names, so the plugin's request is granted
|
|
52
|
+
silently unless a rule says `ask`. Read tools never ask, so the `whatsapp_*` glob
|
|
53
|
+
is safe.
|
|
54
|
+
|
|
55
|
+
V1 registers no skills. Unpack the package and copy its `skills/*` folders into
|
|
56
|
+
`~/.config/opencode/skills/`:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
npm pack @craftsamo/whatsapp-tools@<version>
|
|
60
|
+
tar -xzf craftsamo-whatsapp-tools-<version>.tgz
|
|
61
|
+
mkdir -p ~/.config/opencode/skills
|
|
62
|
+
cp -r package/skills/* ~/.config/opencode/skills/
|
|
63
|
+
rm -rf package craftsamo-whatsapp-tools-<version>.tgz
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Claude Code
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
claude plugin marketplace add craft-agent/whatsapp-tools#v<version>
|
|
70
|
+
claude plugin install whatsapp-tools@craft-agent
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Codex
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
codex plugin marketplace add craft-agent/whatsapp-tools --ref v<version>
|
|
77
|
+
codex plugin add whatsapp-tools@craft-agent
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Codex turns `-` into `_` in server names, so it shows the tools as
|
|
81
|
+
`mcp__whatsapp_tools__whatsapp_*`. It starts the MCP server in the plugin's
|
|
82
|
+
install directory, which is never the caller's working tree.
|
|
83
|
+
|
|
84
|
+
### Grok Build
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
grok plugin install craft-agent/whatsapp-tools@v<version> --trust
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Grok starts the MCP server in the session's working directory.
|
|
91
|
+
|
|
92
|
+
### Antigravity
|
|
93
|
+
|
|
94
|
+
Install from the tree of the release tag, never from the default branch, which
|
|
95
|
+
has no built `dist/`; Antigravity clones the tag into
|
|
96
|
+
`~/.gemini/config/plugins/whatsapp-tools`.
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
agy plugin install https://github.com/craft-agent/whatsapp-tools/tree/v<version>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Hermes Agent
|
|
103
|
+
|
|
104
|
+
The plugin needs Hermes Agent 0.22 or later. Hermes installs a commit, not a
|
|
105
|
+
tag, and the plugin installs disabled:
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
hermes plugins install craft-agent/whatsapp-tools --ref <sha>
|
|
109
|
+
hermes plugins enable whatsapp-tools
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
If a Hermes gateway is already running, restart it (`hermes gateway restart`) to
|
|
113
|
+
load the plugin.
|
|
114
|
+
|
|
115
|
+
The plugin runs the Node it found when Hermes started. If a version manager
|
|
116
|
+
removes that version while Hermes runs, a call looks for Node again; when it
|
|
117
|
+
finds none, restart Hermes from a shell where `node` runs.
|
|
118
|
+
|
|
119
|
+
### Any other MCP host
|
|
120
|
+
|
|
121
|
+
Add the server to the host's MCP configuration. Flags go in `args`:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"mcpServers": {
|
|
126
|
+
"whatsapp-tools": {
|
|
127
|
+
"command": "npx",
|
|
128
|
+
"args": ["-y", "@craftsamo/whatsapp-tools@<version>", "--read-only"]
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Tools and skills
|
|
135
|
+
|
|
136
|
+
Each toolset comes with a skill that tells an agent how to use its tools. A
|
|
137
|
+
skill names its toolset under `metadata` in its frontmatter
|
|
138
|
+
(`metadata: { toolset: sends }`, written as a block). OpenCode V2 and Hermes
|
|
139
|
+
register it only when the settings register at least one of that toolset's
|
|
140
|
+
tools, so a profile with the toolset off, or with all its tools removed, does
|
|
141
|
+
not see it; a skill without a toolset is always registered. Claude Code,
|
|
142
|
+
Codex, Grok Build and Antigravity install the skill folder whole and pass the
|
|
143
|
+
settings only to the MCP server, so they list every skill whatever the
|
|
144
|
+
settings select. OpenCode V1 registers no skills, and its copied skills are
|
|
145
|
+
all listed too.
|
|
146
|
+
|
|
147
|
+
### Account (`account`)
|
|
148
|
+
|
|
149
|
+
| Tool | What it does | Class |
|
|
150
|
+
| ----------------------- | --------------------------------------------------------------------------------------------------------- | ----- |
|
|
151
|
+
| `whatsapp_account_read` | each label: pairing, sync service state, last sync and activity, wacli support and the person's next step | read |
|
|
152
|
+
|
|
153
|
+
It reads local state only: no network, no running service needed, and no
|
|
154
|
+
message content, number, JID or key in its result. The skill is
|
|
155
|
+
[`whatsapp-account`](skills/whatsapp-account/SKILL.md), shown as
|
|
156
|
+
`whatsapp-tools:whatsapp-account` in Claude Code and `whatsapp-tools:account`
|
|
157
|
+
in Hermes. Unlike other tools, leaving out `account` lists every label.
|
|
158
|
+
|
|
159
|
+
### Chats (`chats`)
|
|
160
|
+
|
|
161
|
+
| Tool | What it does | Class |
|
|
162
|
+
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
|
|
163
|
+
| `whatsapp_chat_read` | by `method`: `list` finds chats, `view` reads one chat's messages, `search` finds messages by words, `around` reads the messages next to a message | read |
|
|
164
|
+
|
|
165
|
+
It reads the account's local mirror only, with no network needed, and what it
|
|
166
|
+
returns of other people's words is data, never instructions. The skills are
|
|
167
|
+
[`whatsapp-chats`](skills/whatsapp-chats/SKILL.md), shown as
|
|
168
|
+
`whatsapp-tools:whatsapp-chats` in Claude Code and `whatsapp-tools:chats` in
|
|
169
|
+
Hermes, and
|
|
170
|
+
[`whatsapp-count-activity`](skills/whatsapp-count-activity/SKILL.md), shown as
|
|
171
|
+
`whatsapp-tools:whatsapp-count-activity` and `whatsapp-tools:count-activity`,
|
|
172
|
+
which says how to count chats and replies over a period.
|
|
173
|
+
|
|
174
|
+
### Contacts (`contacts`)
|
|
175
|
+
|
|
176
|
+
| Tool | What it does | Class |
|
|
177
|
+
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------- | ----- |
|
|
178
|
+
| `whatsapp_contact_read` | by `method`: `search` finds contacts by name, number or JID, `check` asks WhatsApp whether up to 20 phone numbers are on it | read |
|
|
179
|
+
|
|
180
|
+
`search` reads the account's local mirror only, with no network or running
|
|
181
|
+
service needed. `check` asks WhatsApp through the label's running sync service,
|
|
182
|
+
writes nothing and adds no contact. The skill is
|
|
183
|
+
[`whatsapp-contacts`](skills/whatsapp-contacts/SKILL.md), shown as
|
|
184
|
+
`whatsapp-tools:whatsapp-contacts` in Claude Code and `whatsapp-tools:contacts`
|
|
185
|
+
in Hermes.
|
|
186
|
+
|
|
187
|
+
### History (`history`)
|
|
188
|
+
|
|
189
|
+
| Tool | What it does | Class |
|
|
190
|
+
| --------------------------- | ---------------------------------------------------------------------------------- | ----- |
|
|
191
|
+
| `whatsapp_history_backfill` | asks the phone for one chat's older messages, in 1 to 5 batches of 50, best effort | add |
|
|
192
|
+
|
|
193
|
+
It writes only to the label's own mirror and sends nothing to anyone. The sync
|
|
194
|
+
service runs it, pausing its sync for the call, so the service must be
|
|
195
|
+
running; the result says how many messages arrived, the oldest one now held,
|
|
196
|
+
why it stopped, and whether the service is syncing again. The phone must be
|
|
197
|
+
online, and nothing added does not prove that nothing older exists. Its skill
|
|
198
|
+
is [`whatsapp-chats`](skills/whatsapp-chats/SKILL.md).
|
|
199
|
+
|
|
200
|
+
A call can take up to about five minutes. Some harnesses stop a tool call
|
|
201
|
+
sooner by default, and the call then fails, though what arrived stays and the
|
|
202
|
+
service syncs again:
|
|
203
|
+
|
|
204
|
+
- **Codex** stops a call after 60 s by default; the plugin raises the limit to
|
|
205
|
+
330 s. A server added under `[mcp_servers]` instead of the plugin (see
|
|
206
|
+
[Settings](#settings)) needs `tool_timeout_sec = 330`.
|
|
207
|
+
- **Antigravity** stops a call after 180 s. Give the server
|
|
208
|
+
`"timeoutSeconds": 330` in its MCP configuration.
|
|
209
|
+
- **Hermes** gives the plugin's calls 150 s, and 330 s to
|
|
210
|
+
`whatsapp_history_backfill`, `whatsapp_file_save` and `whatsapp_file_send`;
|
|
211
|
+
nothing needs setting.
|
|
212
|
+
|
|
213
|
+
### Files (`files`)
|
|
214
|
+
|
|
215
|
+
| Tool | What it does | Class |
|
|
216
|
+
| -------------------- | -------------------------------------------------------------------------------------------------------- | ----- |
|
|
217
|
+
| `whatsapp_file_save` | saves one message's file into `download_dir`, inspecting archives and unpacking one when asked to | add |
|
|
218
|
+
| `whatsapp_file_send` | sends 1 to 10 files, up to 100 MiB, to a person or a group, one message each, checked before any is sent | add |
|
|
219
|
+
|
|
220
|
+
Off by default: turn it on with the `files` toolset. A message's file goes into
|
|
221
|
+
its own folder under `download_dir` (`~/Downloads/whatsapp-tools` by default),
|
|
222
|
+
private and never executable, and nothing is overwritten. Programs,
|
|
223
|
+
installers, disk images, macro documents and archives that cannot be read are
|
|
224
|
+
refused; zip, tar and gzipped tar are inspected first and unpacked only when
|
|
225
|
+
asked. On macOS each saved file carries the quarantine flag. A file WhatsApp's
|
|
226
|
+
server no longer keeps is asked of the phone through the running sync service.
|
|
227
|
+
The skill is
|
|
228
|
+
[`whatsapp-files`](skills/whatsapp-files/SKILL.md), shown as
|
|
229
|
+
`whatsapp-tools:whatsapp-files` in Claude Code and `whatsapp-tools:files` in
|
|
230
|
+
Hermes.
|
|
231
|
+
|
|
232
|
+
A call that falls back to the phone can take a few minutes; the tool time
|
|
233
|
+
limits given above under History apply to it too.
|
|
234
|
+
|
|
235
|
+
`whatsapp_file_send` sends only from the working directory and the
|
|
236
|
+
directories the `send_dirs` setting names, judged by real path; a file
|
|
237
|
+
anywhere else is refused, and so is the state directory. Keys and secrets
|
|
238
|
+
(`.ssh`, `.env`, `*.pem`, …), the data folders of browsers, password managers
|
|
239
|
+
and messengers, empty files, programs, installers and anything holding a
|
|
240
|
+
private key block are never sent, wherever they are; an archive is sent only
|
|
241
|
+
once every entry passes, and is never unpacked. Each file is copied into the
|
|
242
|
+
label's outbox and checked there, so what is checked is what leaves, and a
|
|
243
|
+
`sha256` the user approved makes a changed file `stale`. Each file is its own
|
|
244
|
+
message, in the order given, with the text as the first file's caption (at most
|
|
245
|
+
1,024 characters, refused on audio). Photos, videos and audio WhatsApp shows
|
|
246
|
+
itself go as such, decided from the name and the first bytes, and everything
|
|
247
|
+
else as a file; `as_document` sends all of them as files. A call that stops
|
|
248
|
+
after some files went out fails and lists in `details` what went out, with
|
|
249
|
+
the message ids, and what did not, so nothing is sent twice; a file that may
|
|
250
|
+
have gone out is `uncertain`. A call takes at most 280 s, and the tool time
|
|
251
|
+
limits given above under History apply to it too.
|
|
252
|
+
|
|
253
|
+
### Sends (`sends`)
|
|
254
|
+
|
|
255
|
+
| Tool | What it does | Class |
|
|
256
|
+
| ----------------------- | --------------------------------------------------------------------------------------------- | ----- |
|
|
257
|
+
| `whatsapp_message_send` | sends one text, up to 4,000 characters, to a person or a group, with an optional quoted reply | add |
|
|
258
|
+
|
|
259
|
+
On by default, and it goes through the label's running sync service, so the
|
|
260
|
+
service must be running. It sends to people and groups only: a channel, a
|
|
261
|
+
broadcast list and the status feed are refused. The chat is a JID from
|
|
262
|
+
`whatsapp_chat_read`; for someone the mirror does not hold yet, it is the
|
|
263
|
+
`chat` that `whatsapp_contact_read` `check` gives, and WhatsApp is asked right
|
|
264
|
+
before sending whether the number has an account. The text goes out as given,
|
|
265
|
+
surrounding blank space trimmed, never split, and with no link preview. A
|
|
266
|
+
result means WhatsApp accepted the message, not that it was delivered or read.
|
|
267
|
+
A failure known to happen before anything left says so; every other failure
|
|
268
|
+
after the request was handed over is `uncertain`: the message may have gone
|
|
269
|
+
out, and the chat is checked and the user asked before it is sent again. Like
|
|
270
|
+
a message written on the phone, a send lowers the chat's unread count in the
|
|
271
|
+
mirror and sends no read receipt. The skill is
|
|
272
|
+
[`whatsapp-sends`](skills/whatsapp-sends/SKILL.md), shown as
|
|
273
|
+
`whatsapp-tools:whatsapp-sends` in Claude Code and `whatsapp-tools:sends` in
|
|
274
|
+
Hermes.
|
|
275
|
+
|
|
276
|
+
### Actions (`actions`)
|
|
277
|
+
|
|
278
|
+
| Tool | What it does | Class |
|
|
279
|
+
| ------------------------- | ------------------------------------------------------------------------------ | -------------- |
|
|
280
|
+
| `whatsapp_chat_archive` | archives a chat, or unarchives it | idempotent |
|
|
281
|
+
| `whatsapp_chat_mark` | marks a chat read or unread, optionally sending read receipts | idempotent |
|
|
282
|
+
| `whatsapp_chat_mute` | mutes a chat for 8 hours, a week or always, or unmutes it | idempotent |
|
|
283
|
+
| `whatsapp_chat_pin` | pins a chat, or unpins it, up to the three WhatsApp allows | idempotent |
|
|
284
|
+
| `whatsapp_message_delete` | deletes a message the user sent for everyone, at most two days after it | administrative |
|
|
285
|
+
| `whatsapp_message_edit` | replaces the text of a text message the user sent, at most 15 minutes after it | overwrite |
|
|
286
|
+
| `whatsapp_message_react` | reacts to a message with one emoji, or removes the user's reaction | overwrite |
|
|
287
|
+
|
|
288
|
+
Off by default: turn it on with the `actions` toolset. Each action goes
|
|
289
|
+
through the label's running sync service and acts only on a chat and a message
|
|
290
|
+
the mirror holds, named by JID and id from `whatsapp_chat_read`. Deleting for
|
|
291
|
+
everyone pauses the sync service for the call, a few seconds, and the result
|
|
292
|
+
says whether it syncs again. It keeps to the limits WhatsApp's own apps hold,
|
|
293
|
+
and refuses past them before anything leaves. A result means WhatsApp
|
|
294
|
+
accepted the action; a failure known to happen before anything left says so,
|
|
295
|
+
and every other failure is `uncertain`: the chat is checked and the user asked
|
|
296
|
+
before the action is tried again. A chat's archive, pin, mute and read state
|
|
297
|
+
are states only the user's own devices show, and setting one again is safe,
|
|
298
|
+
so their `uncertain` says so. Marking a chat read tells no one by default;
|
|
299
|
+
only when the user asks for it does `receipts: true` send read receipts for up
|
|
300
|
+
to 100 unread messages instead, which tells their senders and clears only the
|
|
301
|
+
mirror's unread count, not necessarily the phone's mark. The skill is
|
|
302
|
+
[`whatsapp-actions`](skills/whatsapp-actions/SKILL.md), shown as
|
|
303
|
+
`whatsapp-tools:whatsapp-actions` in Claude Code and `whatsapp-tools:actions`
|
|
304
|
+
in Hermes.
|
|
305
|
+
|
|
306
|
+
## Adding a tool
|
|
307
|
+
|
|
308
|
+
1. Write the spec in `src/core/<domain>/<verb>.ts` with `defineTool`: an ID
|
|
309
|
+
`<prefix>_<domain>_<verb>`, a title and description, the Zod argument and
|
|
310
|
+
output schemas, its toolset and write class, and `execute(args, context)`.
|
|
311
|
+
A write tool also gives `targets(args, context)`: what the call writes to,
|
|
312
|
+
as a permission rule should name it.
|
|
313
|
+
2. Test it beside the spec (`<verb>.test.ts`), calling it through `invoke` as
|
|
314
|
+
the entry points do. A tool that runs a program is tested against a fake of
|
|
315
|
+
that program on `PATH`; no test reaches a real service.
|
|
316
|
+
3. List it in `src/tools.ts`. A new toolset goes into `toolsets` in
|
|
317
|
+
`src/project.ts` (and `defaultToolsets` when it is on by default), a
|
|
318
|
+
setting of the package's own into `options` (and `OPTIONS` in `project.py`
|
|
319
|
+
with `config_schema` in `plugin.yaml`), and a variable the tool reads
|
|
320
|
+
besides the settings into `toolVariables`.
|
|
321
|
+
4. Add or update the toolset's skill, `skills/<prefix>-<toolset>/SKILL.md`,
|
|
322
|
+
whose frontmatter names the toolset under `metadata`.
|
|
323
|
+
5. Run `pnpm run build && pnpm run manifests` to list the tool in
|
|
324
|
+
`plugin.yaml` and its variables in Codex's manifest, then describe it under
|
|
325
|
+
"Tools and skills".
|
|
326
|
+
|
|
327
|
+
[`docs/conventions.md`](docs/conventions.md) has the rules each step follows.
|
|
328
|
+
|
|
329
|
+
## Reads and writes
|
|
330
|
+
|
|
331
|
+
Every read tool is named `<prefix>_<domain>_read`. Reads can run unasked;
|
|
332
|
+
writes are confirmed by the harness.
|
|
333
|
+
|
|
334
|
+
| Harness | Reads without asking |
|
|
335
|
+
| ----------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
336
|
+
| OpenCode | always; the plugin asks only before a write |
|
|
337
|
+
| Claude Code | the rule `mcp__plugin_whatsapp-tools_whatsapp-tools__*_read` in `permissions.allow` of `~/.claude/settings.json` |
|
|
338
|
+
| Codex | always; Codex runs tools that declare `readOnlyHint` unasked, as the read tools do |
|
|
339
|
+
| Grok Build | `grok --allow 'MCPTool(whatsapp-tools__*_read)' --allow 'MCPTool(whatsapp_*_read)'` (both rules; see below) |
|
|
340
|
+
| Antigravity | the rule `mcp(whatsapp-tools_whatsapp-tools/<tool id>)` in `permissions.allow`, one for each read tool |
|
|
341
|
+
| Hermes | always; the plugin asks only before a write |
|
|
342
|
+
|
|
343
|
+
Where a write is asked about, and where it is not:
|
|
344
|
+
|
|
345
|
+
- **OpenCode** grants any action no rule names, so a write runs unasked unless
|
|
346
|
+
a rule asks for it: add the `whatsapp_*` rule shown under [OpenCode](#opencode).
|
|
347
|
+
- **Codex** under `codex exec`, and **Grok Build** under `grok -p`, refuse a
|
|
348
|
+
write they cannot ask about, unless it is allowed beforehand.
|
|
349
|
+
- **Grok Build** calls the plugin's tools through its `use_tool` tool, and the
|
|
350
|
+
agent often names a tool without its server first. Grok checks the rules
|
|
351
|
+
against the name as given, so the server-qualified rule alone does not match
|
|
352
|
+
that first call. The second rule matches the short name; a write still
|
|
353
|
+
matches neither.
|
|
354
|
+
- **Antigravity** asks before any MCP tool no rule allows, reads included, and
|
|
355
|
+
names the plugin's server `whatsapp-tools_whatsapp-tools` (plugin,
|
|
356
|
+
then server). `agy -p` refuses a tool it cannot ask about.
|
|
357
|
+
- **Hermes** `hermes chat -q` refuses a write it cannot ask about; `hermes -z`
|
|
358
|
+
and `--yolo` skip approval, so writes then run unasked.
|
|
359
|
+
|
|
360
|
+
## Settings
|
|
361
|
+
|
|
362
|
+
Every entry point takes the same settings. A flag wins over its environment
|
|
363
|
+
variable, and an option or setting wins over the variable too, except in
|
|
364
|
+
Hermes, noted below.
|
|
365
|
+
|
|
366
|
+
| Setting | Flag | Variable | Meaning |
|
|
367
|
+
| --------------- | ----------------- | ------------------------------ | --------------------------------------------------------------------------------- |
|
|
368
|
+
| `toolsets` | `--toolsets` | `WHATSAPP_TOOLS_TOOLSETS` | the toolsets that are on, or `all`; default those `src/project.ts` names |
|
|
369
|
+
| `read_only` | `--read-only` | `WHATSAPP_TOOLS_READ_ONLY` | registers no tool that writes; wins over every other setting |
|
|
370
|
+
| `tools` | `--tools` | `WHATSAPP_TOOLS_TOOLS` | single tools to add by ID, whatever their toolset |
|
|
371
|
+
| `exclude_tools` | `--exclude-tools` | `WHATSAPP_TOOLS_EXCLUDE_TOOLS` | single tools to remove by ID; wins over `tools` and `toolsets` |
|
|
372
|
+
| `confirm` | `--confirm` | `WHATSAPP_TOOLS_CONFIRM` | MCP server only: `harness` (default) or `elicitation`, as the conventions say |
|
|
373
|
+
| `language` | `--language` | `WHATSAPP_TOOLS_LANGUAGE` | native plugins only: the approval prompt's language, such as `ja` |
|
|
374
|
+
| `emoji` | — | — | Hermes only: the emoji shown beside each toolset, by toolset name (`""` for none) |
|
|
375
|
+
| `state_dir` | `--state-dir` | `WHATSAPP_TOOLS_STATE_DIR` | where every label's state lives; default `$XDG_STATE_HOME/whatsapp-tools` |
|
|
376
|
+
| `download_dir` | `--download-dir` | `WHATSAPP_TOOLS_DOWNLOAD_DIR` | where received files are saved; default `~/Downloads/whatsapp-tools` |
|
|
377
|
+
| `send_dirs` | `--send-dirs` | `WHATSAPP_TOOLS_SEND_DIRS` | the directories files may be sent from, besides the working directory |
|
|
378
|
+
|
|
379
|
+
Lists are comma-separated in a flag or a variable. A package's own settings,
|
|
380
|
+
listed in `options` in `src/project.ts`, take a row each here and are read the
|
|
381
|
+
same way. Where each harness reads them:
|
|
382
|
+
|
|
383
|
+
- **OpenCode** takes the plugin `options` shown above, with lists as arrays,
|
|
384
|
+
`read_only` as a boolean, `state_dir` and `download_dir` as paths and
|
|
385
|
+
`send_dirs` as an array of paths, and falls back to the variables. V2 runs plugins in
|
|
386
|
+
its background service, which keeps the environment it started with: a
|
|
387
|
+
variable set where `opencode run` starts has no effect until the service is
|
|
388
|
+
restarted in that environment (`opencode service restart`). V1 runs plugins
|
|
389
|
+
in its own process.
|
|
390
|
+
- **Hermes** takes `plugins.entries.whatsapp-tools.settings` in
|
|
391
|
+
`config.yaml`:
|
|
392
|
+
|
|
393
|
+
```yaml
|
|
394
|
+
plugins:
|
|
395
|
+
entries:
|
|
396
|
+
whatsapp-tools:
|
|
397
|
+
settings:
|
|
398
|
+
read_only: false
|
|
399
|
+
exclude_tools: []
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
Without a `language` setting, the approval card follows Hermes's own
|
|
403
|
+
`display.language` (or `HERMES_LANGUAGE`), the language of its approval
|
|
404
|
+
buttons, and else stays in English. The plugin passes its own environment
|
|
405
|
+
to the command line it runs, so the variables work too. A setting that is
|
|
406
|
+
off or empty (`read_only: false`, `tools: []`, `exclude_tools: []`,
|
|
407
|
+
`send_dirs: []`) passes nothing, so a variable set in that environment
|
|
408
|
+
still applies: `WHATSAPP_TOOLS_READ_ONLY=true` keeps the plugin read-only
|
|
409
|
+
whatever the setting says. A running gateway keeps the environment it
|
|
410
|
+
started with; restart it (`hermes gateway restart`) after changing one.
|
|
411
|
+
|
|
412
|
+
- **Claude Code, Grok Build and Antigravity** start the plugin's server without
|
|
413
|
+
flags and pass it the client's environment, so set the variables in the
|
|
414
|
+
environment the harness runs in.
|
|
415
|
+
- **Codex** passes a plugin's MCP server only a few basic variables (such as
|
|
416
|
+
`PATH` and `HOME`) and those the plugin's manifest names, which are the
|
|
417
|
+
variables above: set them in the environment Codex runs in. A variable the
|
|
418
|
+
tools read besides the settings belongs in `toolVariables` in
|
|
419
|
+
`src/project.ts`; `pnpm run manifests` adds it to the manifest after the
|
|
420
|
+
settings' variables. To pass flags, or to give the server a variable the
|
|
421
|
+
package does not name, add the server under `[mcp_servers]` in Codex's
|
|
422
|
+
`config.toml` instead of using the plugin: flags go in `args`, `env_vars`
|
|
423
|
+
names the variables to forward from Codex's environment, and `env` sets
|
|
424
|
+
fixed values.
|
|
425
|
+
|
|
426
|
+
```toml
|
|
427
|
+
[mcp_servers.whatsapp-tools]
|
|
428
|
+
command = "npx"
|
|
429
|
+
args = ["-y", "@craftsamo/whatsapp-tools@<version>", "--read-only"]
|
|
430
|
+
env_vars = ["WHATSAPP_TOOLS_TOOLSETS"]
|
|
431
|
+
tool_timeout_sec = 330
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### Accounts and the sync service
|
|
435
|
+
|
|
436
|
+
The tools use a WhatsApp account as a linked device of the user's phone, the
|
|
437
|
+
way WhatsApp Web does, through [wacli](https://github.com/openclaw/wacli),
|
|
438
|
+
which you install yourself (`brew install openclaw/tap/wacli`, or a release
|
|
439
|
+
binary). They never drive WhatsApp Web or read another client's data.
|
|
440
|
+
|
|
441
|
+
- **wacli 0.26 or later.** WhatsApp stops serving old clients until wacli
|
|
442
|
+
catches up with its protocol, so keep it updated; after an update, restart
|
|
443
|
+
the sync service so it runs the new release.
|
|
444
|
+
- **Each linked device has a label**, 1 to 24 lowercase letters, digits and
|
|
445
|
+
`_` (`default` when pairing names none). A tool's `account` argument picks
|
|
446
|
+
the label; leave it out when only one device is paired. The account tool
|
|
447
|
+
instead lists every label when `account` is absent.
|
|
448
|
+
- **Each label keeps its state in one directory**, `<state_dir>/<label>/`
|
|
449
|
+
(mode 700): wacli's store (the session keys, its mirror of your messages,
|
|
450
|
+
downloaded media), the sync service's socket, and the copies of files being
|
|
451
|
+
sent. `state_dir` defaults to `$XDG_STATE_HOME/whatsapp-tools`, or
|
|
452
|
+
`~/.local/state/whatsapp-tools`. wacli's own accounts in `~/.wacli` are not
|
|
453
|
+
used. Keep the directory out of backups you share: the keys read and send as
|
|
454
|
+
you.
|
|
455
|
+
- **A sync service runs for each label**: it keeps the mirror current, and
|
|
456
|
+
every send and every other write goes through it, so the tools that write
|
|
457
|
+
need it running. Reads work with or without it. History starts when the
|
|
458
|
+
device is paired, with what the phone sends then; older messages can be
|
|
459
|
+
asked of the phone, best effort.
|
|
460
|
+
- **WhatsApp unlinks a device unused for 30 days**, and every linked device
|
|
461
|
+
when the phone has been unused for 14; a phone takes four linked devices at
|
|
462
|
+
most. A running service keeps the device in use. An unlinked label is paired
|
|
463
|
+
again from the phone.
|
|
464
|
+
- **wacli is not an official client.** WhatsApp has no API for personal
|
|
465
|
+
accounts, and it may limit or ban an account it finds using an unofficial
|
|
466
|
+
one.
|
|
467
|
+
|
|
468
|
+
### Pairing a device and running the sync service
|
|
469
|
+
|
|
470
|
+
Install [wacli 0.26 or later](https://github.com/openclaw/wacli) first: on
|
|
471
|
+
macOS, `brew install openclaw/tap/wacli`; elsewhere, a release binary. macOS
|
|
472
|
+
and Linux are supported; Windows is
|
|
473
|
+
[#11](https://github.com/craft-agent/whatsapp-tools/issues/11).
|
|
474
|
+
|
|
475
|
+
The commands come with the npm package, apart from any harness plugin: a
|
|
476
|
+
plugin gives the tools, not a command on your `PATH`. Install the package
|
|
477
|
+
globally, at the version of the plugin you use, so that the service it
|
|
478
|
+
installs keeps a stable path:
|
|
479
|
+
|
|
480
|
+
```sh
|
|
481
|
+
npm install -g @craftsamo/whatsapp-tools@<version>
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
```sh
|
|
485
|
+
# On the phone, WhatsApp > Linked devices > Link a device, then scan the QR.
|
|
486
|
+
whatsapp-tools-cli pair --account work
|
|
487
|
+
|
|
488
|
+
# Start syncing at logon, and restart after a failure.
|
|
489
|
+
whatsapp-tools-cli service install --account work
|
|
490
|
+
whatsapp-tools-cli service status
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
`--account` may be left out when only one label exists (`pair` names a new
|
|
494
|
+
one `default`), except by `remove`, which always needs it; every command takes
|
|
495
|
+
`--state-dir` like the tools. With
|
|
496
|
+
several labels, add `--account work` to `service status` too.
|
|
497
|
+
|
|
498
|
+
Every command below follows `whatsapp-tools-cli`:
|
|
499
|
+
|
|
500
|
+
| Command | What it does |
|
|
501
|
+
| ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
502
|
+
| `pair [--account <label>] [--phone <number>] [--full-history [--history-days <n>]]` | pairs this machine from a QR code, or with `--phone` prints a code to enter on the phone, then waits for the first sync |
|
|
503
|
+
| `sync [--account <label>]` | runs the sync service in the foreground, logging to standard error, until stopped |
|
|
504
|
+
| `service install \| uninstall \| status [--account <label>]` | registers/starts the service as a launchd agent (macOS) or systemd user unit (Linux), removes it, or reports its state |
|
|
505
|
+
| `remove --account <label> [--yes]` | uninstalls the service, logs the device out when possible and deletes the label's keys and history after confirmation |
|
|
506
|
+
|
|
507
|
+
- **The service records this package's paths and Node's**, so run
|
|
508
|
+
`service install` again after updating either. Where Node's path holds its
|
|
509
|
+
version, as under a version manager, `service install` records a `node` on
|
|
510
|
+
`PATH` at a path without it that starts the same Node, and warns when there
|
|
511
|
+
is none. That `node` then runs whichever Node the manager makes current.
|
|
512
|
+
`service status` and `whatsapp_account_read` say when the recorded program
|
|
513
|
+
is gone, once the service has been installed with this version. On macOS its
|
|
514
|
+
log is
|
|
515
|
+
`~/Library/Logs/io.github.craft-agent.whatsapp-tools/<label>.log`; on Linux,
|
|
516
|
+
`journalctl --user -u <unit>` as `service status` shows, and
|
|
517
|
+
`loginctl enable-linger` keeps it running while you are logged out.
|
|
518
|
+
- **On macOS, Login Items lists the service as `whatsapp-tools-sync-<label>`**:
|
|
519
|
+
the agent starts through that launcher script, which `service install`
|
|
520
|
+
writes to
|
|
521
|
+
`~/Library/Application Support/io.github.craft-agent.whatsapp-tools/` and
|
|
522
|
+
`service uninstall` deletes. Switching the item off there stops syncing.
|
|
523
|
+
- **A phone takes at most four linked devices**, and each label is one of
|
|
524
|
+
them. WhatsApp unlinks a device unused for 30 days, and every linked device
|
|
525
|
+
when the phone has been unused for 14 days; a running service keeps the
|
|
526
|
+
linked device in use, not the phone.
|
|
527
|
+
- **`--full-history` asks the phone for its full history** while pairing,
|
|
528
|
+
instead of its recent months; `--history-days` limits how far back. It
|
|
529
|
+
takes effect only while pairing, and the phone keeps sending for a while
|
|
530
|
+
after `pair` ends, which reaches the mirror once the service runs. A
|
|
531
|
+
device already paired gets older messages with `whatsapp_history_backfill`,
|
|
532
|
+
one chat at a time, or by being removed and paired again.
|
|
533
|
+
- **To pair a revoked label again**, run `pair` again: the dead session is
|
|
534
|
+
deleted and the mirror is kept. Use a new label for a different account.
|
|
535
|
+
- **Removing a label logs the device out when possible.** If it cannot,
|
|
536
|
+
remove the linked device on the phone under WhatsApp > Linked devices too.
|
|
537
|
+
- `whatsapp_account_read` tells an agent which step a label needs.
|
|
538
|
+
|
|
539
|
+
## Layout
|
|
540
|
+
|
|
541
|
+
- `src/project.ts` — the package's name, tool ID prefix, setting prefix,
|
|
542
|
+
toolsets, tool options and the other variables its tools read.
|
|
543
|
+
- `src/core/` — the tool contract and the tools, free of any harness and of
|
|
544
|
+
the project's names.
|
|
545
|
+
- `src/tools.ts` — the list of every tool, which each entry point registers.
|
|
546
|
+
- `src/words.ts` — the translations of the tools' words, per language, which
|
|
547
|
+
the native plugins use in their approval prompts.
|
|
548
|
+
- `src/mcp/` — the MCP server, built to `dist/mcp.js` and run by
|
|
549
|
+
`whatsapp-tools`.
|
|
550
|
+
- `src/opencode/` — the OpenCode plugin, built to `dist/opencode.js`.
|
|
551
|
+
- `src/cli/` — the command line the Hermes plugin calls, built to `dist/cli.js`
|
|
552
|
+
and run by `whatsapp-tools-cli`.
|
|
553
|
+
- `src/hermes/`, `plugin.yaml` and `__init__.py` — the Hermes plugin.
|
|
554
|
+
- `skills/` — one skill per toolset, `skills/<name>/SKILL.md`, which every
|
|
555
|
+
harness that takes skills loads from the package or plugin root; the native
|
|
556
|
+
plugins load only those of the toolsets that are on.
|
|
557
|
+
- `.claude-plugin/`, `.codex-plugin/`, `.agents/plugins/`, `.mcp.json`,
|
|
558
|
+
`.antigravity-plugin/` and `mcp_config.json` — the plugin manifests of the
|
|
559
|
+
other harnesses. Codex reads `.codex-plugin/plugin.json`; a root
|
|
560
|
+
`plugin.json` would make it load the plugin as an Agent Plugin, whose tool
|
|
561
|
+
budget hides tools past a fixed size. The root `.mcp.json` is the
|
|
562
|
+
configuration Grok Build reads (with `${CLAUDE_PLUGIN_ROOT}`, the variable
|
|
563
|
+
both Grok Build and Claude Code expand); when working on this repository in
|
|
564
|
+
Claude Code, decline the project MCP server it offers from that file.
|
|
565
|
+
|
|
566
|
+
How a release is made is in [`RELEASING.md`](RELEASING.md).
|
|
567
|
+
|
|
568
|
+
## License
|
|
569
|
+
|
|
570
|
+
[AGPL-3.0 with Commons Clause](LICENSE)
|
package/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Loads the plugin package from ``src/hermes/whatsapp_tools_hermes`` for Hermes, which imports this file.
|
|
2
|
+
|
|
3
|
+
The package is loaded as ``<this module's name>._impl``, so Hermes, which evicts
|
|
4
|
+
everything under a plugin's module name when it reloads it or loads it for another
|
|
5
|
+
profile, evicts the package too, and each install path gets a package of its own.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import importlib.util
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
_NAME = f"{__name__}._impl"
|
|
13
|
+
_PACKAGE_DIR = Path(__file__).resolve().parent / "src" / "hermes" / "whatsapp_tools_hermes"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _evict() -> None:
|
|
17
|
+
"""Drop the package and its submodules from ``sys.modules``."""
|
|
18
|
+
for name in list(sys.modules):
|
|
19
|
+
if name == _NAME or name.startswith(f"{_NAME}."):
|
|
20
|
+
sys.modules.pop(name, None)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_spec = importlib.util.spec_from_file_location(
|
|
24
|
+
_NAME, _PACKAGE_DIR / "__init__.py", submodule_search_locations=[str(_PACKAGE_DIR)]
|
|
25
|
+
)
|
|
26
|
+
_module = importlib.util.module_from_spec(_spec)
|
|
27
|
+
_evict()
|
|
28
|
+
sys.modules[_NAME] = _module
|
|
29
|
+
try:
|
|
30
|
+
_spec.loader.exec_module(_module)
|
|
31
|
+
except BaseException:
|
|
32
|
+
_evict()
|
|
33
|
+
raise
|
|
34
|
+
|
|
35
|
+
register = _module.register
|
|
36
|
+
__all__ = ["register"]
|