diff --git a/.claude/skills/setup/SKILL.md b/.claude/skills/setup/SKILL.md
index 7f62053..10ca85b 100644
--- a/.claude/skills/setup/SKILL.md
+++ b/.claude/skills/setup/SKILL.md
@@ -5,182 +5,187 @@ description: Run initial NanoClaw setup. Use when user wants to install dependen
# 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
-
-Run these checks. Install any that are missing:
+## 1. Install Dependencies
```bash
-python3 --version # Need 3.10+
-node --version # Need 18+
-uv --version
+npm install
```
-If missing, install automatically:
-- **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
+## 2. WhatsApp Authentication
**USER ACTION REQUIRED**
-Run the bridge in background and monitor for connection:
+Run the authentication script:
```bash
-cd bridge && node bridge.js > /tmp/bridge_output.log 2>&1 &
-BRIDGE_PID=$!
+npm run auth
```
Tell the user:
-> A QR code will appear below. On your phone:
+> A QR code will appear. On your phone:
> 1. Open WhatsApp
> 2. Tap **Settings → Linked Devices → Link a Device**
> 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
-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
-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)
-**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:
-1. Create a GCP project
-2. Enable Gmail API
-3. Create OAuth 2.0 credentials
-4. Download credentials to `~/.gmail-mcp/gcp-oauth.keys.json`
+If yes, they need Google Cloud Platform OAuth credentials first:
+1. Create a GCP project at https://console.cloud.google.com
+2. Enable the Gmail API
+3. Create OAuth 2.0 credentials (Desktop app)
+4. Download and save to `~/.gmail-mcp/gcp-oauth.keys.json`
Then run:
```bash
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:
-> Do you want to use a **personal chat** (message yourself) or a **WhatsApp group** as your main channel?
+## 5. Configure launchd Service
-For personal chat:
-> 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:
+Get the actual paths:
```bash
-# For personal chat
-sqlite3 bridge/store/messages.db "SELECT DISTINCT chat_jid FROM messages WHERE chat_jid NOT LIKE '%@g.us' ORDER BY rowid DESC LIMIT 5"
-
-# 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"
+which node
+pwd
```
-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`:
-```json
-{
- "THE_JID_HERE": {
- "name": "main",
- "folder": "main",
- "trigger": "@AssistantName",
- "added_at": "CURRENT_TIMESTAMP_ISO"
- }
-}
+```xml
+
+
+
+
+ Label
+ com.nanoclaw
+ ProgramArguments
+
+ NODE_PATH_HERE
+ PROJECT_PATH_HERE/dist/index.js
+
+ WorkingDirectory
+ PROJECT_PATH_HERE
+ RunAtLoad
+
+ KeepAlive
+
+ EnvironmentVariables
+
+ PATH
+ /usr/local/bin:/usr/bin:/bin:HOME_PATH_HERE/.local/bin
+ HOME
+ HOME_PATH_HERE
+
+ StandardOutPath
+ PROJECT_PATH_HERE/logs/nanoclaw.log
+ StandardErrorPath
+ PROJECT_PATH_HERE/logs/nanoclaw.error.log
+
+
```
-## 6. Configure launchd
+Replace the placeholders with actual paths from the commands above.
-First, detect the actual paths:
+Build and start the service:
```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:
-
-**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:
+Verify it's running:
```bash
launchctl list | grep nanoclaw
```
-## 7. Test
+## 6. Test
-Wait a few seconds for services to start, then tell the user:
-> Send `@AssistantName hello` in your registered chat/group.
+Tell the user:
+> Send `@Andy hello` in your registered chat.
-Check `logs/router.log` for activity:
+Check the logs:
```bash
-tail -f logs/router.log
+tail -f logs/nanoclaw.log
```
-If there are issues, also check:
-- `logs/router.error.log`
-- `logs/bridge.log`
-- `logs/bridge.error.log`
+The user should receive a response in WhatsApp.
## 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
-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`).
diff --git a/package-lock.json b/package-lock.json
index 7517f7b..c46f756 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,11 +12,13 @@
"@whiskeysockets/baileys": "^7.0.0-rc.9",
"better-sqlite3": "^11.8.1",
"pino": "^9.6.0",
- "pino-pretty": "^13.0.0"
+ "pino-pretty": "^13.0.0",
+ "qrcode-terminal": "^0.12.0"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.12",
"@types/node": "^22.10.0",
+ "@types/qrcode-terminal": "^0.12.2",
"tsx": "^4.19.0",
"typescript": "^5.7.0"
},
@@ -1175,6 +1177,13 @@
"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": {
"version": "7.0.0-rc.9",
"resolved": "https://registry.npmjs.org/@whiskeysockets/baileys/-/baileys-7.0.0-rc.9.tgz",
@@ -1980,6 +1989,14 @@
"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": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
diff --git a/package.json b/package.json
index c9309a9..255a3e5 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,7 @@
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx src/index.ts",
+ "auth": "tsx src/auth.ts",
"lint": "eslint src/",
"typecheck": "tsc --noEmit"
},
@@ -16,11 +17,13 @@
"@whiskeysockets/baileys": "^7.0.0-rc.9",
"better-sqlite3": "^11.8.1",
"pino": "^9.6.0",
- "pino-pretty": "^13.0.0"
+ "pino-pretty": "^13.0.0",
+ "qrcode-terminal": "^0.12.0"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.12",
"@types/node": "^22.10.0",
+ "@types/qrcode-terminal": "^0.12.2",
"tsx": "^4.19.0",
"typescript": "^5.7.0"
},
diff --git a/src/auth.ts b/src/auth.ts
new file mode 100644
index 0000000..0635d2c
--- /dev/null
+++ b/src/auth.ts
@@ -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 {
+ 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);
+});