Skip to content

Commit ab9abbb

Browse files
jonazriclaude
andauthored
feat(skill): add WhatsApp reactions skill (emoji reactions + status tracker) (#509)
* feat(skill): add reactions skill (emoji reactions + status tracker) * refactor(reactions): minimize overlays per upstream review Address gavrielc's review on #509: - SKILL.md: remove all inline code, follow add-telegram/add-whatsapp pattern (465→79 lines) - Rebuild overlays as minimal deltas against upstream/main base - ipc-mcp-stdio.ts: upstream base + only react_to_message tool (8% delta) - ipc.ts: upstream base + only reactions delta (14% delta) - group-queue.test.ts: upstream base + isActive tests only (5% delta) - Remove group-queue.ts overlay (isActive provided by container-hardening) - Remove group-queue.ts from manifest modifies list Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5b2bafd commit ab9abbb

16 files changed

Lines changed: 6985 additions & 0 deletions

File tree

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
---
2+
name: add-reactions
3+
description: Add WhatsApp emoji reaction support — receive, send, store, and search reactions.
4+
---
5+
6+
# Add Reactions
7+
8+
This skill adds emoji reaction support to NanoClaw's WhatsApp channel: receive and store reactions, send reactions from the container agent via MCP tool, and query reaction history from SQLite.
9+
10+
## Phase 1: Pre-flight
11+
12+
### Check if already applied
13+
14+
Read `.nanoclaw/state.yaml`. If `reactions` is in `applied_skills`, skip to Phase 3 (Verify). The code changes are already in place.
15+
16+
## Phase 2: Apply Code Changes
17+
18+
Run the skills engine to apply this skill's code package. The package files are in this directory alongside this SKILL.md.
19+
20+
### Apply the skill
21+
22+
```bash
23+
npx tsx scripts/apply-skill.ts .claude/skills/add-reactions
24+
```
25+
26+
This deterministically:
27+
- Adds `scripts/migrate-reactions.ts` (database migration for `reactions` table with composite PK and indexes)
28+
- Adds `src/status-tracker.ts` (forward-only emoji state machine for message lifecycle signaling, with persistence and retry)
29+
- Adds `src/status-tracker.test.ts` (unit tests for StatusTracker)
30+
- Adds `container/skills/reactions/SKILL.md` (agent-facing documentation for the `react_to_message` MCP tool)
31+
- Modifies `src/db.ts` — adds `Reaction` interface, `reactions` table schema, `storeReaction`, `getReactionsForMessage`, `getMessagesByReaction`, `getReactionsByUser`, `getReactionStats`, `getLatestMessage`, `getMessageFromMe`
32+
- Modifies `src/channels/whatsapp.ts` — adds `messages.reaction` event handler, `sendReaction()`, `reactToLatestMessage()` methods
33+
- Modifies `src/types.ts` — adds optional `sendReaction` and `reactToLatestMessage` to `Channel` interface
34+
- Modifies `src/ipc.ts` — adds `type: 'reaction'` IPC handler with group-scoped authorization
35+
- Modifies `src/index.ts` — wires `sendReaction` dependency into IPC watcher
36+
- Modifies `src/group-queue.ts``GroupQueue` class for per-group container concurrency with retry
37+
- Modifies `container/agent-runner/src/ipc-mcp-stdio.ts` — adds `react_to_message` MCP tool exposed to container agents
38+
- Records the application in `.nanoclaw/state.yaml`
39+
40+
### Run database migration
41+
42+
```bash
43+
npx tsx scripts/migrate-reactions.ts
44+
```
45+
46+
### Validate code changes
47+
48+
```bash
49+
npm test
50+
npm run build
51+
```
52+
53+
All tests must pass and build must be clean before proceeding.
54+
55+
## Phase 3: Verify
56+
57+
### Build and restart
58+
59+
```bash
60+
npm run build
61+
```
62+
63+
Linux:
64+
```bash
65+
systemctl --user restart nanoclaw
66+
```
67+
68+
macOS:
69+
```bash
70+
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
71+
```
72+
73+
### Test receiving reactions
74+
75+
1. Send a message from your phone
76+
2. React to it with an emoji on WhatsApp
77+
3. Check the database:
78+
79+
```bash
80+
sqlite3 store/messages.db "SELECT * FROM reactions ORDER BY timestamp DESC LIMIT 5;"
81+
```
82+
83+
### Test sending reactions
84+
85+
Ask the agent to react to a message via the `react_to_message` MCP tool. Check your phone — the reaction should appear on the message.
86+
87+
## Troubleshooting
88+
89+
### Reactions not appearing in database
90+
91+
- Check NanoClaw logs for `Failed to process reaction` errors
92+
- Verify the chat is registered
93+
- Confirm the service is running
94+
95+
### Migration fails
96+
97+
- Ensure `store/messages.db` exists and is accessible
98+
- If "table reactions already exists", the migration already ran — skip it
99+
100+
### Agent can't send reactions
101+
102+
- Check IPC logs for `Unauthorized IPC reaction attempt blocked` — the agent can only react in its own group's chat
103+
- Verify WhatsApp is connected: check logs for connection status
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
---
2+
name: reactions
3+
description: React to WhatsApp messages with emoji. Use when the user asks you to react, when acknowledging a message with a reaction makes sense, or when you want to express a quick response without sending a full message.
4+
---
5+
6+
# Reactions
7+
8+
React to messages with emoji using the `mcp__nanoclaw__react_to_message` tool.
9+
10+
## When to use
11+
12+
- User explicitly asks you to react ("react with a thumbs up", "heart that message")
13+
- Quick acknowledgment is more appropriate than a full text reply
14+
- Expressing agreement, approval, or emotion about a specific message
15+
16+
## How to use
17+
18+
### React to the latest message
19+
20+
```
21+
mcp__nanoclaw__react_to_message(emoji: "👍")
22+
```
23+
24+
Omitting `message_id` reacts to the most recent message in the chat.
25+
26+
### React to a specific message
27+
28+
```
29+
mcp__nanoclaw__react_to_message(emoji: "❤️", message_id: "3EB0F4C9E7...")
30+
```
31+
32+
Pass a `message_id` to react to a specific message. You can find message IDs by querying the messages database:
33+
34+
```bash
35+
sqlite3 /workspace/project/store/messages.db "
36+
SELECT id, sender_name, substr(content, 1, 80), timestamp
37+
FROM messages
38+
WHERE chat_jid = '<chat_jid>'
39+
ORDER BY timestamp DESC
40+
LIMIT 5;
41+
"
42+
```
43+
44+
### Remove a reaction
45+
46+
Send an empty string to remove your reaction:
47+
48+
```
49+
mcp__nanoclaw__react_to_message(emoji: "")
50+
```
51+
52+
## Common emoji
53+
54+
| Emoji | When to use |
55+
|-------|-------------|
56+
| 👍 | Acknowledgment, approval |
57+
| ❤️ | Appreciation, love |
58+
| 😂 | Something funny |
59+
| 🔥 | Impressive, exciting |
60+
| 🎉 | Celebration, congrats |
61+
| 🙏 | Thanks, prayer |
62+
|| Task done, confirmed |
63+
|| Needs clarification |
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Database migration script for reactions table
2+
// Run: npx tsx scripts/migrate-reactions.ts
3+
4+
import Database from 'better-sqlite3';
5+
import path from 'path';
6+
import { fileURLToPath } from 'url';
7+
8+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
9+
const STORE_DIR = process.env.STORE_DIR || path.join(process.cwd(), 'store');
10+
const dbPath = path.join(STORE_DIR, 'messages.db');
11+
12+
console.log(`Migrating database at: ${dbPath}`);
13+
14+
const db = new Database(dbPath);
15+
16+
try {
17+
db.transaction(() => {
18+
db.exec(`
19+
CREATE TABLE IF NOT EXISTS reactions (
20+
message_id TEXT NOT NULL,
21+
message_chat_jid TEXT NOT NULL,
22+
reactor_jid TEXT NOT NULL,
23+
reactor_name TEXT,
24+
emoji TEXT NOT NULL,
25+
timestamp TEXT NOT NULL,
26+
PRIMARY KEY (message_id, message_chat_jid, reactor_jid)
27+
);
28+
`);
29+
30+
console.log('Created reactions table');
31+
32+
db.exec(`
33+
CREATE INDEX IF NOT EXISTS idx_reactions_message ON reactions(message_id, message_chat_jid);
34+
CREATE INDEX IF NOT EXISTS idx_reactions_reactor ON reactions(reactor_jid);
35+
CREATE INDEX IF NOT EXISTS idx_reactions_emoji ON reactions(emoji);
36+
CREATE INDEX IF NOT EXISTS idx_reactions_timestamp ON reactions(timestamp);
37+
`);
38+
39+
console.log('Created indexes');
40+
})();
41+
42+
const tableInfo = db.prepare(`PRAGMA table_info(reactions)`).all();
43+
console.log('\nReactions table schema:');
44+
console.table(tableInfo);
45+
46+
const count = db.prepare(`SELECT COUNT(*) as count FROM reactions`).get() as {
47+
count: number;
48+
};
49+
console.log(`\nCurrent reaction count: ${count.count}`);
50+
51+
console.log('\nMigration complete!');
52+
} catch (err) {
53+
console.error('Migration failed:', err);
54+
process.exit(1);
55+
} finally {
56+
db.close();
57+
}

0 commit comments

Comments
 (0)