Separate WhatsApp auth from daemon into standalone script

- Add src/auth.ts for interactive QR code authentication
- Add `npm run auth` script
- Update setup skill for current Node.js architecture
- Daemon (src/index.ts) now only uses stored credentials

Auth is run during setup; daemon assumes credentials exist and
shows macOS notification if re-auth is needed.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-01-31 19:06:25 +02:00
parent e1867f8d27
commit 8a385c7988
4 changed files with 232 additions and 118 deletions

View File

@@ -5,182 +5,187 @@ description: Run initial NanoClaw setup. Use when user wants to install dependen
# NanoClaw Setup # NanoClaw Setup
**IMPORTANT**: Run all commands automatically. Only pause for user action when physical interaction is required (scanning QR codes). Give clear instructions for exactly what the user needs to do. Run all commands automatically. Only pause when user action is required (scanning QR codes).
## 1. Check Prerequisites ## 1. Install Dependencies
Run these checks. Install any that are missing:
```bash ```bash
python3 --version # Need 3.10+ npm install
node --version # Need 18+
uv --version
``` ```
If missing, install automatically: ## 2. WhatsApp Authentication
- **uv**: `curl -LsSf https://astral.sh/uv/install.sh | sh`
- **node**: `brew install node`
- **python**: `brew install python@3.10`
## 2. Install Dependencies
Run all of these automatically:
```bash
# Python dependencies
uv venv && source .venv/bin/activate && uv pip install -r requirements.txt
```
```bash
# WhatsApp bridge dependencies
cd bridge && npm install
```
```bash
# Create logs directory
mkdir -p logs
```
## 3. WhatsApp Authentication
**USER ACTION REQUIRED** **USER ACTION REQUIRED**
Run the bridge in background and monitor for connection: Run the authentication script:
```bash ```bash
cd bridge && node bridge.js > /tmp/bridge_output.log 2>&1 & npm run auth
BRIDGE_PID=$!
``` ```
Tell the user: Tell the user:
> A QR code will appear below. On your phone: > A QR code will appear. On your phone:
> 1. Open WhatsApp > 1. Open WhatsApp
> 2. Tap **Settings → Linked Devices → Link a Device** > 2. Tap **Settings → Linked Devices → Link a Device**
> 3. Scan the QR code > 3. Scan the QR code
Then poll for either QR code or successful connection (check every 2 seconds for up to 3 minutes): Wait for the script to output "Successfully authenticated" then continue.
If it says "Already authenticated", skip to the next step.
## 3. Register Main Channel
Ask the user:
> Do you want to use your **personal chat** (message yourself) or a **WhatsApp group** as your main control channel?
For personal chat:
> Send any message to yourself in WhatsApp (the "Message Yourself" chat). Tell me when done.
For group:
> Send any message in the WhatsApp group you want to use as your main channel. Tell me when done.
After user confirms, start the app briefly to capture the message:
```bash ```bash
cat /tmp/bridge_output.log # Look for QR code or "Connected to WhatsApp!" timeout 10 npm run dev || true
``` ```
When you see "Connected to WhatsApp!" in the output, stop the bridge: Then find the JID from the database:
```bash ```bash
kill $BRIDGE_PID # For personal chat (ends with @s.whatsapp.net)
sqlite3 store/messages.db "SELECT DISTINCT chat_jid FROM messages WHERE chat_jid LIKE '%@s.whatsapp.net' ORDER BY timestamp DESC LIMIT 5"
# For group (ends with @g.us)
sqlite3 store/messages.db "SELECT DISTINCT chat_jid FROM messages WHERE chat_jid LIKE '%@g.us' ORDER BY timestamp DESC LIMIT 5"
``` ```
Session persists until logged out from WhatsApp. Get the assistant name from environment or default:
```bash
echo ${ASSISTANT_NAME:-Andy}
```
Create/update `data/registered_groups.json`:
```json
{
"THE_JID_HERE": {
"name": "main",
"folder": "main",
"trigger": "@Andy",
"added_at": "2026-01-31T12:00:00Z"
}
}
```
Ensure the groups folder exists:
```bash
mkdir -p groups/main/logs
```
## 4. Gmail Authentication (Optional) ## 4. Gmail Authentication (Optional)
**Skip this step** unless user specifically needs Gmail integration. It requires Google Cloud Platform OAuth credentials setup. Ask the user:
> Do you want to enable Gmail integration for reading/sending emails?
If needed, user must first: If yes, they need Google Cloud Platform OAuth credentials first:
1. Create a GCP project 1. Create a GCP project at https://console.cloud.google.com
2. Enable Gmail API 2. Enable the Gmail API
3. Create OAuth 2.0 credentials 3. Create OAuth 2.0 credentials (Desktop app)
4. Download credentials to `~/.gmail-mcp/gcp-oauth.keys.json` 4. Download and save to `~/.gmail-mcp/gcp-oauth.keys.json`
Then run: Then run:
```bash ```bash
npx -y @gongrzhe/server-gmail-autoauth-mcp npx -y @gongrzhe/server-gmail-autoauth-mcp
``` ```
## 5. Register Main Channel This will open a browser for OAuth consent. After authorization, credentials are cached.
Ask the user: ## 5. Configure launchd Service
> Do you want to use a **personal chat** (message yourself) or a **WhatsApp group** as your main channel?
For personal chat: Get the actual paths:
> Send a test message to yourself in WhatsApp. Tell me when done.
For group:
> Send a message in the WhatsApp group you want to use. Tell me when done.
After user confirms, find the JID:
```bash ```bash
# For personal chat which node
sqlite3 bridge/store/messages.db "SELECT DISTINCT chat_jid FROM messages WHERE chat_jid NOT LIKE '%@g.us' ORDER BY rowid DESC LIMIT 5" pwd
# For group
sqlite3 bridge/store/messages.db "SELECT DISTINCT chat_jid FROM messages WHERE chat_jid LIKE '%@g.us' ORDER BY rowid DESC LIMIT 5"
``` ```
Read the assistant name from `src/config.py` (look for `ASSISTANT_NAME = "..."`). Create the plist file at `~/Library/LaunchAgents/com.nanoclaw.plist`:
Then update `data/registered_groups.json`: ```xml
```json <?xml version="1.0" encoding="UTF-8"?>
{ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
"THE_JID_HERE": { <plist version="1.0">
"name": "main", <dict>
"folder": "main", <key>Label</key>
"trigger": "@AssistantName", <string>com.nanoclaw</string>
"added_at": "CURRENT_TIMESTAMP_ISO" <key>ProgramArguments</key>
} <array>
} <string>NODE_PATH_HERE</string>
<string>PROJECT_PATH_HERE/dist/index.js</string>
</array>
<key>WorkingDirectory</key>
<string>PROJECT_PATH_HERE</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/local/bin:/usr/bin:/bin:HOME_PATH_HERE/.local/bin</string>
<key>HOME</key>
<string>HOME_PATH_HERE</string>
</dict>
<key>StandardOutPath</key>
<string>PROJECT_PATH_HERE/logs/nanoclaw.log</string>
<key>StandardErrorPath</key>
<string>PROJECT_PATH_HERE/logs/nanoclaw.error.log</string>
</dict>
</plist>
``` ```
## 6. Configure launchd Replace the placeholders with actual paths from the commands above.
First, detect the actual paths: Build and start the service:
```bash ```bash
which node # Get actual node path (may be nvm, homebrew, etc.) npm run build
mkdir -p logs
launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
``` ```
Create plist files directly in `~/Library/LaunchAgents/` with: Verify it's running:
**com.nanoclaw.bridge.plist:**
- ProgramArguments: `[actual_node_path, /Users/.../nanoclaw/bridge/bridge.js]`
- WorkingDirectory: `/Users/.../nanoclaw/bridge`
- StandardOutPath/StandardErrorPath: `/Users/.../nanoclaw/logs/bridge.log` and `bridge.error.log`
**com.nanoclaw.router.plist:**
- ProgramArguments: `[/Users/.../nanoclaw/.venv/bin/python, -u, /Users/.../nanoclaw/src/router.py]`
- The `-u` flag is required for unbuffered output (so logs appear immediately)
- WorkingDirectory: `/Users/.../nanoclaw`
- EnvironmentVariables:
- `PATH`: `/Users/USERNAME/.local/bin:/usr/local/bin:/usr/bin:/bin` (must include path to `claude` CLI)
- `HOME`: `/Users/USERNAME` (required for Claude CLI to find its config)
- StandardOutPath/StandardErrorPath: `/Users/.../nanoclaw/logs/router.log` and `router.error.log`
**NOTE**: Do NOT set ANTHROPIC_API_KEY - the Claude CLI handles its own authentication.
Then load the services:
```bash
launchctl load ~/Library/LaunchAgents/com.nanoclaw.bridge.plist
launchctl load ~/Library/LaunchAgents/com.nanoclaw.router.plist
```
Verify they're running:
```bash ```bash
launchctl list | grep nanoclaw launchctl list | grep nanoclaw
``` ```
## 7. Test ## 6. Test
Wait a few seconds for services to start, then tell the user: Tell the user:
> Send `@AssistantName hello` in your registered chat/group. > Send `@Andy hello` in your registered chat.
Check `logs/router.log` for activity: Check the logs:
```bash ```bash
tail -f logs/router.log tail -f logs/nanoclaw.log
``` ```
If there are issues, also check: The user should receive a response in WhatsApp.
- `logs/router.error.log`
- `logs/bridge.log`
- `logs/bridge.error.log`
## Troubleshooting ## Troubleshooting
**"Command failed with exit code 1"** - Usually means the Claude CLI isn't in PATH. Verify PATH in the router plist includes the directory containing `claude` (typically `~/.local/bin`). **Service not starting**: Check `logs/nanoclaw.error.log`
**Messages received but no WhatsApp response** - Check that the bridge HTTP server is running: **No response to messages**:
- Verify the trigger pattern matches (`@Andy` at start of message)
- Check that the chat JID is in `data/registered_groups.json`
- Check `logs/nanoclaw.log` for errors
**WhatsApp disconnected**:
- The service will show a macOS notification
- Run `npm run auth` to re-authenticate
- Restart the service: `launchctl kickstart -k gui/$(id -u)/com.nanoclaw`
**Unload service**:
```bash ```bash
curl -s http://127.0.0.1:3141/send -X POST -H "Content-Type: application/json" -d '{"jid":"test","message":"test"}' launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist
``` ```
Should return an error about invalid JID (not connection refused).
**Router not processing messages** - Check the trigger pattern matches. Messages must start with the trigger (e.g., `@Andy hello`).

19
package-lock.json generated
View File

@@ -12,11 +12,13 @@
"@whiskeysockets/baileys": "^7.0.0-rc.9", "@whiskeysockets/baileys": "^7.0.0-rc.9",
"better-sqlite3": "^11.8.1", "better-sqlite3": "^11.8.1",
"pino": "^9.6.0", "pino": "^9.6.0",
"pino-pretty": "^13.0.0" "pino-pretty": "^13.0.0",
"qrcode-terminal": "^0.12.0"
}, },
"devDependencies": { "devDependencies": {
"@types/better-sqlite3": "^7.6.12", "@types/better-sqlite3": "^7.6.12",
"@types/node": "^22.10.0", "@types/node": "^22.10.0",
"@types/qrcode-terminal": "^0.12.2",
"tsx": "^4.19.0", "tsx": "^4.19.0",
"typescript": "^5.7.0" "typescript": "^5.7.0"
}, },
@@ -1175,6 +1177,13 @@
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
}, },
"node_modules/@types/qrcode-terminal": {
"version": "0.12.2",
"resolved": "https://registry.npmjs.org/@types/qrcode-terminal/-/qrcode-terminal-0.12.2.tgz",
"integrity": "sha512-v+RcIEJ+Uhd6ygSQ0u5YYY7ZM+la7GgPbs0V/7l/kFs2uO4S8BcIUEMoP7za4DNIqNnUD5npf0A/7kBhrCKG5Q==",
"dev": true,
"license": "MIT"
},
"node_modules/@whiskeysockets/baileys": { "node_modules/@whiskeysockets/baileys": {
"version": "7.0.0-rc.9", "version": "7.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@whiskeysockets/baileys/-/baileys-7.0.0-rc.9.tgz", "resolved": "https://registry.npmjs.org/@whiskeysockets/baileys/-/baileys-7.0.0-rc.9.tgz",
@@ -1980,6 +1989,14 @@
"node": ">=20" "node": ">=20"
} }
}, },
"node_modules/qrcode-terminal": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz",
"integrity": "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==",
"bin": {
"qrcode-terminal": "bin/qrcode-terminal.js"
}
},
"node_modules/quick-format-unescaped": { "node_modules/quick-format-unescaped": {
"version": "4.0.4", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",

View File

@@ -8,6 +8,7 @@
"build": "tsc", "build": "tsc",
"start": "node dist/index.js", "start": "node dist/index.js",
"dev": "tsx src/index.ts", "dev": "tsx src/index.ts",
"auth": "tsx src/auth.ts",
"lint": "eslint src/", "lint": "eslint src/",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
@@ -16,11 +17,13 @@
"@whiskeysockets/baileys": "^7.0.0-rc.9", "@whiskeysockets/baileys": "^7.0.0-rc.9",
"better-sqlite3": "^11.8.1", "better-sqlite3": "^11.8.1",
"pino": "^9.6.0", "pino": "^9.6.0",
"pino-pretty": "^13.0.0" "pino-pretty": "^13.0.0",
"qrcode-terminal": "^0.12.0"
}, },
"devDependencies": { "devDependencies": {
"@types/better-sqlite3": "^7.6.12", "@types/better-sqlite3": "^7.6.12",
"@types/node": "^22.10.0", "@types/node": "^22.10.0",
"@types/qrcode-terminal": "^0.12.2",
"tsx": "^4.19.0", "tsx": "^4.19.0",
"typescript": "^5.7.0" "typescript": "^5.7.0"
}, },

89
src/auth.ts Normal file
View File

@@ -0,0 +1,89 @@
/**
* WhatsApp Authentication Script
*
* Run this during setup to authenticate with WhatsApp.
* Displays QR code, waits for scan, saves credentials, then exits.
*
* Usage: npx tsx src/auth.ts
*/
import makeWASocket, {
useMultiFileAuthState,
DisconnectReason,
makeCacheableSignalKeyStore,
} from '@whiskeysockets/baileys';
import pino from 'pino';
import qrcode from 'qrcode-terminal';
import fs from 'fs';
import path from 'path';
const AUTH_DIR = './store/auth';
const logger = pino({
level: 'warn', // Quiet logging - only show errors
});
async function authenticate(): Promise<void> {
fs.mkdirSync(AUTH_DIR, { recursive: true });
const { state, saveCreds } = await useMultiFileAuthState(AUTH_DIR);
// Check if already authenticated
if (state.creds.registered) {
console.log('✓ Already authenticated with WhatsApp');
console.log(' To re-authenticate, delete the store/auth folder and run again.');
process.exit(0);
}
console.log('Starting WhatsApp authentication...\n');
const sock = makeWASocket({
auth: {
creds: state.creds,
keys: makeCacheableSignalKeyStore(state.keys, logger),
},
printQRInTerminal: false,
logger,
browser: ['NanoClaw', 'Chrome', '1.0.0'],
});
sock.ev.on('connection.update', (update) => {
const { connection, lastDisconnect, qr } = update;
if (qr) {
console.log('Scan this QR code with WhatsApp:\n');
console.log(' 1. Open WhatsApp on your phone');
console.log(' 2. Tap Settings → Linked Devices → Link a Device');
console.log(' 3. Point your camera at the QR code below\n');
qrcode.generate(qr, { small: true });
}
if (connection === 'close') {
const reason = (lastDisconnect?.error as any)?.output?.statusCode;
if (reason === DisconnectReason.loggedOut) {
console.log('\n✗ Logged out. Delete store/auth and try again.');
process.exit(1);
} else {
console.log('\n✗ Connection failed. Please try again.');
process.exit(1);
}
}
if (connection === 'open') {
console.log('\n✓ Successfully authenticated with WhatsApp!');
console.log(' Credentials saved to store/auth/');
console.log(' You can now start the NanoClaw service.\n');
// Give it a moment to save credentials, then exit
setTimeout(() => process.exit(0), 1000);
}
});
sock.ev.on('creds.update', saveCreds);
}
authenticate().catch((err) => {
console.error('Authentication failed:', err.message);
process.exit(1);
});