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); +});