Migrate setup from bash scripts to cross-platform Node.js modules (#382)
* refactor: migrate setup from bash scripts to cross-platform Node.js modules Replace 9 bash scripts + qr-auth.html with a two-phase setup system: a bash bootstrap (setup.sh) for Node.js/npm verification, and TypeScript modules (src/setup/) for everything else. Resolves cross-platform issues: sed -i replaced with fs operations, sqlite3 CLI replaced with better-sqlite3, browser opening made cross-platform, service management supports launchd/ systemd/WSL nohup fallback, SQL injection prevented with parameterized queries. Add Linux systemctl equivalents alongside macOS launchctl commands in 8 skill files and CLAUDE.md. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: setup migration issues — pairing code, systemd fallback, nohup escaping - Emit WhatsApp pairing code immediately when received, before polling for auth completion. Previously the code was only shown in the final status block after auth succeeded — a catch-22 since the user needs the code to authenticate. (whatsapp-auth.ts) - Add systemd user session pre-check before attempting to write the user-level service unit. Falls back to nohup wrapper when user-level systemd is unavailable (e.g. su session without login/D-Bus). (service.ts) - Rewrite nohup wrapper template using array join instead of template literal to fix shell variable escaping (\\$ → $). (service.ts) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: detect stale docker group and kill orphaned processes on Linux systemd * fix: remove redundant shell option from execSync to fix TS2769 execSync already runs in a shell by default; the explicit `shell: true` caused a type error with @types/node which expects string, not boolean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: hide QR browser auth option on headless Linux Emit IS_HEADLESS from environment step and condition SKILL.md to only show pairing code + QR terminal when no display server is available (headless Linux without WSL). WSL is excluded from the headless gate because browser opening works via Windows interop. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -246,13 +246,15 @@ cd .. && npm run build
|
||||
Wait for TypeScript compilation, then restart the service:
|
||||
|
||||
```bash
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# Linux: systemctl --user restart nanoclaw
|
||||
```
|
||||
|
||||
Check that it started:
|
||||
|
||||
```bash
|
||||
sleep 2 && launchctl list | grep nanoclaw
|
||||
sleep 2 && launchctl list | grep nanoclaw # macOS
|
||||
# Linux: systemctl --user status nanoclaw
|
||||
```
|
||||
|
||||
### Step 5: Test Gmail Integration
|
||||
@@ -651,7 +653,8 @@ cd .. && npm run build
|
||||
Restart the service:
|
||||
|
||||
```bash
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# Linux: systemctl --user restart nanoclaw
|
||||
```
|
||||
|
||||
Verify it started and check for email channel startup message:
|
||||
@@ -726,5 +729,6 @@ To remove Gmail entirely:
|
||||
```bash
|
||||
cd container && ./build.sh && cd ..
|
||||
npm run build
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# Linux: systemctl --user restart nanoclaw
|
||||
```
|
||||
|
||||
@@ -235,13 +235,15 @@ Rebuild the main app and restart:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# Linux: systemctl --user restart nanoclaw
|
||||
```
|
||||
|
||||
Wait 3 seconds for service to start, then verify:
|
||||
```bash
|
||||
sleep 3
|
||||
launchctl list | grep nanoclaw
|
||||
launchctl list | grep nanoclaw # macOS
|
||||
# Linux: systemctl --user status nanoclaw
|
||||
```
|
||||
|
||||
### 8. Test Integration
|
||||
@@ -287,4 +289,4 @@ To remove Parallel AI integration:
|
||||
2. Revert changes to container-runner.ts and agent-runner/src/index.ts
|
||||
3. Remove Web Research Tools section from groups/main/CLAUDE.md
|
||||
4. Rebuild: `./container/build.sh && npm run build`
|
||||
5. Restart: `launchctl kickstart -k gui/$(id -u)/com.nanoclaw`
|
||||
5. Restart: `launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `systemctl --user restart nanoclaw` (Linux)
|
||||
|
||||
@@ -321,11 +321,14 @@ Also add `TELEGRAM_BOT_POOL` to the launchd plist (`~/Library/LaunchAgents/com.n
|
||||
```bash
|
||||
npm run build
|
||||
./container/build.sh # Required — MCP tool changed
|
||||
# macOS:
|
||||
launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist
|
||||
launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
|
||||
# Linux:
|
||||
# systemctl --user restart nanoclaw
|
||||
```
|
||||
|
||||
Must use `unload/load` (not just `kickstart`) because the plist env vars changed.
|
||||
Must use `unload/load` (macOS) or `restart` (Linux) because the service env vars changed.
|
||||
|
||||
### Step 8: Test
|
||||
|
||||
@@ -377,5 +380,5 @@ To remove Agent Swarm support while keeping basic Telegram:
|
||||
4. Remove `initBotPool` call from `main()`
|
||||
5. Remove `sender` param from MCP tool in `container/agent-runner/src/ipc-mcp-stdio.ts`
|
||||
6. Remove Agent Teams section from group CLAUDE.md files
|
||||
7. Remove `TELEGRAM_BOT_POOL` from `.env`, `data/env/env`, and launchd plist
|
||||
8. Rebuild: `npm run build && ./container/build.sh && launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist && launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist`
|
||||
7. Remove `TELEGRAM_BOT_POOL` from `.env`, `data/env/env`, and launchd plist/systemd unit
|
||||
8. Rebuild: `npm run build && ./container/build.sh && launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist && launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist` (macOS) or `npm run build && ./container/build.sh && systemctl --user restart nanoclaw` (Linux)
|
||||
|
||||
@@ -118,7 +118,8 @@ Tell the user:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# Linux: systemctl --user restart nanoclaw
|
||||
```
|
||||
|
||||
## Phase 4: Registration
|
||||
@@ -183,10 +184,11 @@ tail -f logs/nanoclaw.log
|
||||
|
||||
### Bot not responding
|
||||
|
||||
1. Check `TELEGRAM_BOT_TOKEN` is set in `.env` AND synced to `data/env/env`
|
||||
2. Check chat is registered: `sqlite3 store/messages.db "SELECT * FROM registered_groups WHERE jid LIKE 'tg:%'"`
|
||||
3. For non-main chats: message must include trigger pattern
|
||||
4. Service is running: `launchctl list | grep nanoclaw`
|
||||
Check:
|
||||
1. `TELEGRAM_BOT_TOKEN` is set in `.env` AND synced to `data/env/env`
|
||||
2. Chat is registered in SQLite (check with: `sqlite3 store/messages.db "SELECT * FROM registered_groups WHERE jid LIKE 'tg:%'"`)
|
||||
3. For non-main chats: message includes trigger pattern
|
||||
4. Service is running: `launchctl list | grep nanoclaw` (macOS) or `systemctl --user status nanoclaw` (Linux)
|
||||
|
||||
### Bot only responds to @mentions in groups
|
||||
|
||||
@@ -202,6 +204,36 @@ If `/chatid` doesn't work:
|
||||
|
||||
## After Setup
|
||||
|
||||
Ask the user:
|
||||
If running `npm run dev` while the service is active:
|
||||
```bash
|
||||
# macOS:
|
||||
launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist
|
||||
npm run dev
|
||||
# When done testing:
|
||||
launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
|
||||
# Linux:
|
||||
# systemctl --user stop nanoclaw
|
||||
# npm run dev
|
||||
# systemctl --user start nanoclaw
|
||||
```
|
||||
|
||||
> Would you like to add Agent Swarm support? Each subagent appears as a different bot in the Telegram group. If interested, run `/add-telegram-swarm`.
|
||||
## Agent Swarms (Teams)
|
||||
|
||||
After completing the Telegram setup, ask the user:
|
||||
|
||||
> Would you like to add Agent Swarm support? Without it, Agent Teams still work — they just operate behind the scenes. With Swarm support, each subagent appears as a different bot in the Telegram group so you can see who's saying what and have interactive team sessions.
|
||||
|
||||
If they say yes, invoke the `/add-telegram-swarm` skill.
|
||||
|
||||
## Removal
|
||||
|
||||
To remove Telegram integration:
|
||||
|
||||
1. Delete `src/channels/telegram.ts`
|
||||
2. Remove `TelegramChannel` import and creation from `src/index.ts`
|
||||
3. Remove `channels` array and revert to using `whatsapp` directly in `processGroupMessages`, scheduler deps, and IPC deps
|
||||
4. Revert `getAvailableGroups()` filter to only include `@g.us` chats
|
||||
5. Remove Telegram config (`TELEGRAM_BOT_TOKEN`, `TELEGRAM_ONLY`) from `src/config.ts`
|
||||
6. Remove Telegram registrations from SQLite: `sqlite3 store/messages.db "DELETE FROM registered_groups WHERE jid LIKE 'tg:%'"`
|
||||
7. Uninstall: `npm uninstall grammy`
|
||||
8. Rebuild: `npm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `npm run build && systemctl --user restart nanoclaw` (Linux)
|
||||
|
||||
@@ -93,7 +93,8 @@ The container reads environment from `data/env/env`, not `.env` directly.
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# Linux: systemctl --user restart nanoclaw
|
||||
```
|
||||
|
||||
## Phase 4: Verify
|
||||
|
||||
@@ -92,8 +92,11 @@ Always tell the user:
|
||||
```bash
|
||||
# Rebuild and restart
|
||||
npm run build
|
||||
# macOS:
|
||||
launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist
|
||||
launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
|
||||
# Linux:
|
||||
# systemctl --user restart nanoclaw
|
||||
```
|
||||
|
||||
## Example Interaction
|
||||
|
||||
@@ -43,9 +43,11 @@ Set `LOG_LEVEL=debug` for verbose output:
|
||||
# For development
|
||||
LOG_LEVEL=debug npm run dev
|
||||
|
||||
# For launchd service, add to plist EnvironmentVariables:
|
||||
# For launchd service (macOS), add to plist EnvironmentVariables:
|
||||
<key>LOG_LEVEL</key>
|
||||
<string>debug</string>
|
||||
# For systemd service (Linux), add to unit [Service] section:
|
||||
# Environment=LOG_LEVEL=debug
|
||||
```
|
||||
|
||||
Debug level shows:
|
||||
|
||||
@@ -5,54 +5,46 @@ description: Run initial NanoClaw setup. Use when user wants to install dependen
|
||||
|
||||
# NanoClaw Setup
|
||||
|
||||
Run setup scripts automatically. Only pause when user action is required (WhatsApp authentication, configuration choices). Scripts live in `.claude/skills/setup/scripts/` and emit structured status blocks to stdout. Verbose logs go to `logs/setup.log`.
|
||||
Run setup steps automatically. Only pause when user action is required (WhatsApp authentication, configuration choices). Setup uses `bash setup.sh` for bootstrap, then `npx tsx src/setup/index.ts --step <name>` for all other steps. Steps emit structured status blocks to stdout. Verbose logs go to `logs/setup.log`.
|
||||
|
||||
**Principle:** When something is broken or missing, fix it. Don't tell the user to go fix it themselves unless it genuinely requires their manual action (e.g. scanning a QR code, pasting a secret token). If a dependency is missing, install it. If a service won't start, diagnose and repair. Ask the user for permission when needed, then do the work.
|
||||
|
||||
**UX Note:** Use `AskUserQuestion` for all user-facing questions.
|
||||
|
||||
## 1. Check Environment
|
||||
## 1. Bootstrap (Node.js + Dependencies)
|
||||
|
||||
Run `./.claude/skills/setup/scripts/01-check-environment.sh` and parse the status block.
|
||||
Run `bash setup.sh` and parse the status block.
|
||||
|
||||
- If NODE_OK=false → Node.js is missing or too old. Ask the user if they'd like you to install it:
|
||||
- macOS: `brew install node@22` (if brew available) or install nvm then `nvm install 22`
|
||||
- Linux: `curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && sudo apt-get install -y nodejs`, or nvm
|
||||
- After installing Node, re-run `bash setup.sh`
|
||||
- If DEPS_OK=false → Read `logs/setup.log`. Try: delete `node_modules` and `package-lock.json`, re-run `bash setup.sh`. If native module build fails, install build tools (`xcode-select --install` on macOS, `build-essential` on Linux), then retry.
|
||||
- If NATIVE_OK=false → better-sqlite3 failed to load. Install build tools and re-run.
|
||||
- Record PLATFORM and IS_WSL for later steps.
|
||||
|
||||
## 2. Check Environment
|
||||
|
||||
Run `npx tsx src/setup/index.ts --step environment` and parse the status block.
|
||||
|
||||
- If HAS_AUTH=true → note that WhatsApp auth exists, offer to skip step 5
|
||||
- If HAS_REGISTERED_GROUPS=true → note existing config, offer to skip or reconfigure
|
||||
- Record PLATFORM, APPLE_CONTAINER, and DOCKER values for step 3
|
||||
|
||||
**If NODE_OK=false:**
|
||||
|
||||
Node.js is missing or too old. Ask the user if they'd like you to install it. Offer options based on platform:
|
||||
|
||||
- macOS: `brew install node@22` (if brew available) or install nvm then `nvm install 22`
|
||||
- Linux: `curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && sudo apt-get install -y nodejs`, or nvm
|
||||
|
||||
If brew/nvm aren't installed, install them first (`/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"` for brew, `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash` for nvm). After installing Node, re-run the environment check to confirm NODE_OK=true.
|
||||
|
||||
## 2. Install Dependencies
|
||||
|
||||
Run `./.claude/skills/setup/scripts/02-install-deps.sh` and parse the status block.
|
||||
|
||||
**If failed:** Read the tail of `logs/setup.log` to diagnose. Common fixes to try automatically:
|
||||
1. Delete `node_modules` and `package-lock.json`, then re-run the script
|
||||
2. If permission errors: suggest running with corrected permissions
|
||||
3. If specific package fails to build (native modules like better-sqlite3): install build tools (`xcode-select --install` on macOS, `build-essential` on Linux), then retry
|
||||
|
||||
Only ask the user for help if multiple retries fail with the same error.
|
||||
- Record APPLE_CONTAINER and DOCKER values for step 3
|
||||
|
||||
## 3. Container Runtime
|
||||
|
||||
### 3a. Choose runtime
|
||||
|
||||
Check the preflight results for `APPLE_CONTAINER` and `DOCKER`.
|
||||
Check the preflight results for `APPLE_CONTAINER` and `DOCKER`, and the PLATFORM from step 1.
|
||||
|
||||
**If APPLE_CONTAINER=installed** (macOS only): Ask the user which runtime they'd like to use — Docker (default, cross-platform) or Apple Container (native macOS). If they choose Apple Container, run `/convert-to-apple-container` now before continuing, then skip to 3b.
|
||||
|
||||
**If APPLE_CONTAINER=not_found**: Use Docker (the default). Proceed to install/start Docker below.
|
||||
- PLATFORM=linux → Docker (only option)
|
||||
- PLATFORM=macos + APPLE_CONTAINER=installed → Ask user: Docker (default, cross-platform) or Apple Container (native macOS)? If Apple Container, run `/convert-to-apple-container` now, then skip to 3c.
|
||||
- PLATFORM=macos + APPLE_CONTAINER=not_found → Docker (default)
|
||||
|
||||
### 3a-docker. Install Docker
|
||||
|
||||
- DOCKER=running → continue to 3b
|
||||
- DOCKER=installed_not_running → start Docker: `open -a Docker` (macOS) or `sudo systemctl start docker` (Linux). Wait 15s, re-check with `docker info`. If still not running, tell the user Docker is starting up and poll a few more times.
|
||||
- DOCKER=installed_not_running → start Docker: `open -a Docker` (macOS) or `sudo systemctl start docker` (Linux). Wait 15s, re-check with `docker info`.
|
||||
- DOCKER=not_found → **ask the user for confirmation before installing.** Tell them Docker is required for running agents and ask if they'd like you to install it. If confirmed:
|
||||
- macOS: install via `brew install --cask docker`, then `open -a Docker` and wait for it to start. If brew not available, direct to Docker Desktop download at https://docker.com/products/docker-desktop
|
||||
- Linux: install with `curl -fsSL https://get.docker.com | sh && sudo usermod -aG docker $USER`. Note: user may need to log out/in for group membership.
|
||||
@@ -73,146 +65,122 @@ grep -q "CONTAINER_RUNTIME_BIN = 'container'" src/container-runtime.ts && echo "
|
||||
|
||||
### 3c. Build and test
|
||||
|
||||
Run `./.claude/skills/setup/scripts/03-setup-container.sh --runtime <chosen>` and parse the status block.
|
||||
Run `npx tsx src/setup/index.ts --step container -- --runtime <chosen>` and parse the status block.
|
||||
|
||||
**If BUILD_OK=false:** Read `logs/setup.log` tail for the build error.
|
||||
- If it's a cache issue (stale layers): run `docker builder prune -f`, then retry.
|
||||
- If Dockerfile syntax or missing files: diagnose from the log and fix.
|
||||
- Retry the build script after fixing.
|
||||
- Cache issue (stale layers): `docker builder prune -f` (Docker) or `container builder stop && container builder rm && container builder start` (Apple Container). Retry.
|
||||
- Dockerfile syntax or missing files: diagnose from the log and fix, then retry.
|
||||
|
||||
**If TEST_OK=false but BUILD_OK=true:** The image built but won't run. Check logs — common cause is runtime not fully started. Wait a moment and retry the test.
|
||||
|
||||
## 4. Claude Authentication (No Script)
|
||||
|
||||
If HAS_ENV=true from step 1, read `.env` and check if it already has `CLAUDE_CODE_OAUTH_TOKEN` or `ANTHROPIC_API_KEY`. If so, confirm with user: "You already have Claude credentials configured. Want to keep them or reconfigure?" If keeping, skip to step 5.
|
||||
If HAS_ENV=true from step 2, read `.env` and check for `CLAUDE_CODE_OAUTH_TOKEN` or `ANTHROPIC_API_KEY`. If present, confirm with user: keep or reconfigure?
|
||||
|
||||
AskUserQuestion: Claude subscription (Pro/Max) vs Anthropic API key?
|
||||
|
||||
**Subscription:** Tell the user:
|
||||
1. Open another terminal and run: `claude setup-token`
|
||||
2. Copy the token it outputs
|
||||
3. Add it to the `.env` file in the project root: `CLAUDE_CODE_OAUTH_TOKEN=<token>`
|
||||
4. Let me know when done
|
||||
**Subscription:** Tell user to run `claude setup-token` in another terminal, copy the token, add `CLAUDE_CODE_OAUTH_TOKEN=<token>` to `.env`. Do NOT collect the token in chat.
|
||||
|
||||
Do NOT ask the user to paste the token into the chat. Do NOT use AskUserQuestion to collect the token. Just tell them what to do, then wait for confirmation that they've added it to `.env`. Once confirmed, verify the `.env` file has the key.
|
||||
|
||||
**API key:** Tell the user to add `ANTHROPIC_API_KEY=<key>` to the `.env` file in the project root, then let you know when done. Once confirmed, verify the `.env` file has the key.
|
||||
**API key:** Tell user to add `ANTHROPIC_API_KEY=<key>` to `.env`.
|
||||
|
||||
## 5. WhatsApp Authentication
|
||||
|
||||
If HAS_AUTH=true from step 1, confirm with user: "WhatsApp credentials already exist. Want to keep them or re-authenticate?" If keeping, skip to step 6.
|
||||
If HAS_AUTH=true, confirm: keep or re-authenticate?
|
||||
|
||||
AskUserQuestion: QR code in browser (recommended) vs pairing code vs QR code in terminal?
|
||||
**Choose auth method based on environment (from step 2):**
|
||||
|
||||
- **QR browser:** Run `./.claude/skills/setup/scripts/04-auth-whatsapp.sh --method qr-browser` (Bash timeout: 150000ms)
|
||||
- **Pairing code:** Ask for phone number first (country code, no + or spaces, e.g. 14155551234). Run `./.claude/skills/setup/scripts/04-auth-whatsapp.sh --method pairing-code --phone NUMBER` (Bash timeout: 150000ms). Display the PAIRING_CODE from the status block with instructions.
|
||||
- **QR terminal:** Run `./.claude/skills/setup/scripts/04-auth-whatsapp.sh --method qr-terminal`. Tell user to run `cd PROJECT_PATH && npm run auth` in another terminal. Wait for confirmation.
|
||||
If IS_HEADLESS=true AND IS_WSL=false → AskUserQuestion: Pairing code (recommended) vs QR code in terminal?
|
||||
Otherwise (macOS, desktop Linux, or WSL) → AskUserQuestion: QR code in browser (recommended) vs pairing code vs QR code in terminal?
|
||||
|
||||
If AUTH_STATUS=already_authenticated → skip ahead.
|
||||
- **QR browser:** `npx tsx src/setup/index.ts --step whatsapp-auth -- --method qr-browser` (Bash timeout: 150000ms)
|
||||
- **Pairing code:** Ask for phone number first. `npx tsx src/setup/index.ts --step whatsapp-auth -- --method pairing-code --phone NUMBER` (Bash timeout: 150000ms). Display PAIRING_CODE.
|
||||
- **QR terminal:** `npx tsx src/setup/index.ts --step whatsapp-auth -- --method qr-terminal`. Tell user to run `npm run auth` in another terminal.
|
||||
|
||||
**If failed:**
|
||||
- qr_timeout → QR expired. Automatically re-run the auth script to generate a fresh QR. Tell user a new QR is ready.
|
||||
- logged_out → Delete `store/auth/` and re-run auth automatically.
|
||||
- 515 → Stream error during pairing. The auth script handles reconnection, but if it persists, re-run the auth script.
|
||||
- timeout → Auth took too long. Ask user if they scanned/entered the code, offer to retry.
|
||||
**If failed:** qr_timeout → re-run. logged_out → delete `store/auth/` and re-run. 515 → re-run. timeout → ask user, offer retry.
|
||||
|
||||
## 6. Configure Trigger and Channel Type
|
||||
|
||||
First, determine the phone number situation. Get the bot's WhatsApp number from `store/auth/creds.json`:
|
||||
`node -e "const c=require('./store/auth/creds.json');console.log(c.me.id.split(':')[0].split('@')[0])"`
|
||||
Get bot's WhatsApp number: `node -e "const c=require('./store/auth/creds.json');console.log(c.me.id.split(':')[0].split('@')[0])"`
|
||||
|
||||
AskUserQuestion: Does the bot share your personal WhatsApp number, or does it have its own dedicated phone number?
|
||||
AskUserQuestion: Shared number or dedicated? → AskUserQuestion: Trigger word? → AskUserQuestion: Main channel type?
|
||||
|
||||
AskUserQuestion: What trigger word? (default: Andy). In group chats, messages starting with @TriggerWord go to Claude. In the main channel, no prefix needed.
|
||||
|
||||
AskUserQuestion: Main channel type? (options depend on phone number setup)
|
||||
|
||||
**If bot shares user's number (same phone):**
|
||||
1. Self-chat (chat with yourself) — Recommended. You message yourself and the bot responds.
|
||||
2. Solo group (just you) — A group where you're the only member. Good if you want message history separate from self-chat.
|
||||
|
||||
**If bot has its own dedicated phone number:**
|
||||
1. DM with the bot — Recommended. You message the bot's number directly.
|
||||
2. Solo group with the bot — A group with just you and the bot, no one else.
|
||||
|
||||
Do NOT show options that don't apply to the user's setup. For example, don't offer "DM with the bot" if the bot shares the user's number (you can't DM yourself on WhatsApp).
|
||||
**Shared number:** Self-chat (recommended) or Solo group
|
||||
**Dedicated number:** DM with bot (recommended) or Solo group with bot
|
||||
|
||||
## 7. Sync and Select Group (If Group Channel)
|
||||
|
||||
**For personal chat:** The JID is the bot's own phone number from step 6. Construct as `NUMBER@s.whatsapp.net`.
|
||||
**Personal chat:** JID = `NUMBER@s.whatsapp.net`
|
||||
**DM with bot:** Ask for bot's number, JID = `NUMBER@s.whatsapp.net`
|
||||
|
||||
**For DM with bot's dedicated number:** Ask for the bot's phone number, construct JID as `NUMBER@s.whatsapp.net`.
|
||||
|
||||
**For group (solo or with bot):**
|
||||
1. Run `./.claude/skills/setup/scripts/05-sync-groups.sh` (Bash timeout: 60000ms)
|
||||
2. **If BUILD=failed:** Read `logs/setup.log`, fix the TypeScript error, re-run.
|
||||
3. **If GROUPS_IN_DB=0:** Check `logs/setup.log` for the sync output. Common causes: WhatsApp auth expired (re-run step 5), connection timeout (re-run sync script with longer timeout).
|
||||
4. Run `./.claude/skills/setup/scripts/05b-list-groups.sh` to get groups (pipe-separated JID|name lines). Do NOT display the output to the user.
|
||||
5. Pick the most likely candidates (e.g. groups with the trigger word or "NanoClaw" in the name, small/solo groups) and present them as AskUserQuestion options — show names only, not JIDs. Include an "Other" option if their group isn't listed. If they pick Other, search by name in the DB or re-run with a higher limit.
|
||||
**Group:**
|
||||
1. `npx tsx src/setup/index.ts --step groups` (Bash timeout: 60000ms)
|
||||
2. BUILD=failed → fix TypeScript, re-run. GROUPS_IN_DB=0 → check logs.
|
||||
3. `npx tsx src/setup/index.ts --step groups -- --list` for pipe-separated JID|name lines.
|
||||
4. Present candidates as AskUserQuestion (names only, not JIDs).
|
||||
|
||||
## 8. Register Channel
|
||||
|
||||
Run `./.claude/skills/setup/scripts/06-register-channel.sh` with args:
|
||||
- `--jid "JID"` — from step 7
|
||||
- `--name "main"` — always "main" for the first channel
|
||||
- `--trigger "@TriggerWord"` — from step 6
|
||||
- `--folder "main"` — always "main" for the first channel
|
||||
- `--no-trigger-required` — if personal chat, DM, or solo group
|
||||
- `--assistant-name "Name"` — if trigger word differs from "Andy"
|
||||
Run `npx tsx src/setup/index.ts --step register -- --jid "JID" --name "main" --trigger "@TriggerWord" --folder "main"` plus `--no-trigger-required` if personal/DM/solo, `--assistant-name "Name"` if not Andy.
|
||||
|
||||
## 9. Mount Allowlist
|
||||
|
||||
AskUserQuestion: Want the agent to access directories outside the NanoClaw project? (Git repos, project folders, documents, etc.)
|
||||
AskUserQuestion: Agent access to external directories?
|
||||
|
||||
**If no:** Run `./.claude/skills/setup/scripts/07-configure-mounts.sh --empty`
|
||||
|
||||
**If yes:** Collect directory paths and permissions (read-write vs read-only). Ask about non-main group read-only restriction (recommended: yes). Build the JSON and pipe it to the script:
|
||||
|
||||
`echo '{"allowedRoots":[...],"blockedPatterns":[],"nonMainReadOnly":true}' | ./.claude/skills/setup/scripts/07-configure-mounts.sh`
|
||||
|
||||
Tell user how to grant a group access: add `containerConfig.additionalMounts` to their entry in `data/registered_groups.json`.
|
||||
**No:** `npx tsx src/setup/index.ts --step mounts -- --empty`
|
||||
**Yes:** Collect paths/permissions. `npx tsx src/setup/index.ts --step mounts -- --json '{"allowedRoots":[...],"blockedPatterns":[],"nonMainReadOnly":true}'`
|
||||
|
||||
## 10. Start Service
|
||||
|
||||
If the service is already running (check `launchctl list | grep nanoclaw` on macOS), unload it first: `launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist` — then proceed with a clean install.
|
||||
If service already running: unload first.
|
||||
- macOS: `launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist`
|
||||
- Linux: `systemctl --user stop nanoclaw` (or `systemctl stop nanoclaw` if root)
|
||||
|
||||
Run `./.claude/skills/setup/scripts/08-setup-service.sh` and parse the status block.
|
||||
Run `npx tsx src/setup/index.ts --step service` and parse the status block.
|
||||
|
||||
**If FALLBACK=wsl_no_systemd:** WSL without systemd detected. Tell user they can either enable systemd in WSL (`echo -e "[boot]\nsystemd=true" | sudo tee /etc/wsl.conf` then restart WSL) or use the generated `start-nanoclaw.sh` wrapper.
|
||||
|
||||
**If DOCKER_GROUP_STALE=true:** The user was added to the docker group after their session started — the systemd service can't reach the Docker socket. Ask user to run these two commands:
|
||||
|
||||
1. Immediate fix: `sudo setfacl -m u:$(whoami):rw /var/run/docker.sock`
|
||||
2. Persistent fix (re-applies after every Docker restart):
|
||||
```bash
|
||||
sudo mkdir -p /etc/systemd/system/docker.service.d
|
||||
sudo tee /etc/systemd/system/docker.service.d/socket-acl.conf << 'EOF'
|
||||
[Service]
|
||||
ExecStartPost=/usr/bin/setfacl -m u:USERNAME:rw /var/run/docker.sock
|
||||
EOF
|
||||
sudo systemctl daemon-reload
|
||||
```
|
||||
Replace `USERNAME` with the actual username (from `whoami`). Run the two `sudo` commands separately — the `tee` heredoc first, then `daemon-reload`. After user confirms setfacl ran, re-run the service step.
|
||||
|
||||
**If SERVICE_LOADED=false:**
|
||||
- Read `logs/setup.log` for the error.
|
||||
- Common fix: plist already loaded with different path. Unload the old one first, then re-run.
|
||||
- On macOS: check `launchctl list | grep nanoclaw` to see if it's loaded with an error status. If the PID column is `-` and the status column is non-zero, the service is crashing. Read `logs/nanoclaw.error.log` for the crash reason and fix it (common: wrong Node path, missing .env, missing auth).
|
||||
- On Linux: check `systemctl --user status nanoclaw` for the error and fix accordingly.
|
||||
- Re-run the setup-service script after fixing.
|
||||
- macOS: check `launchctl list | grep nanoclaw`. If PID=`-` and status non-zero, read `logs/nanoclaw.error.log`.
|
||||
- Linux: check `systemctl --user status nanoclaw`.
|
||||
- Re-run the service step after fixing.
|
||||
|
||||
## 11. Verify
|
||||
|
||||
Run `./.claude/skills/setup/scripts/09-verify.sh` and parse the status block.
|
||||
Run `npx tsx src/setup/index.ts --step verify` and parse the status block.
|
||||
|
||||
**If STATUS=failed, fix each failing component:**
|
||||
- SERVICE=stopped → run `npm run build` first, then restart: `launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `systemctl --user restart nanoclaw` (Linux). Re-check.
|
||||
- SERVICE=not_found → re-run step 10.
|
||||
- CREDENTIALS=missing → re-run step 4.
|
||||
- WHATSAPP_AUTH=not_found → re-run step 5.
|
||||
- REGISTERED_GROUPS=0 → re-run steps 7-8.
|
||||
- MOUNT_ALLOWLIST=missing → run `./.claude/skills/setup/scripts/07-configure-mounts.sh --empty` to create a default.
|
||||
**If STATUS=failed, fix each:**
|
||||
- SERVICE=stopped → `npm run build`, then restart: `launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `systemctl --user restart nanoclaw` (Linux) or `bash start-nanoclaw.sh` (WSL nohup)
|
||||
- SERVICE=not_found → re-run step 10
|
||||
- CREDENTIALS=missing → re-run step 4
|
||||
- WHATSAPP_AUTH=not_found → re-run step 5
|
||||
- REGISTERED_GROUPS=0 → re-run steps 7-8
|
||||
- MOUNT_ALLOWLIST=missing → `npx tsx src/setup/index.ts --step mounts -- --empty`
|
||||
|
||||
After fixing, re-run `09-verify.sh` to confirm everything passes.
|
||||
|
||||
Tell user to test: send a message in their registered chat (with or without trigger depending on channel type).
|
||||
|
||||
Show the log tail command: `tail -f logs/nanoclaw.log`
|
||||
Tell user to test: send a message in their registered chat. Show: `tail -f logs/nanoclaw.log`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Service not starting:** Check `logs/nanoclaw.error.log`. Common causes: wrong Node path in plist (re-run step 10), missing `.env` (re-run step 4), missing WhatsApp auth (re-run step 5).
|
||||
**Service not starting:** Check `logs/nanoclaw.error.log`. Common: wrong Node path (re-run step 10), missing `.env` (step 4), missing auth (step 5).
|
||||
|
||||
**Container agent fails ("Claude Code process exited with code 1"):** Ensure the container runtime is running — start it with the appropriate command for your runtime. Check container logs in `groups/main/logs/container-*.log`.
|
||||
**Container agent fails ("Claude Code process exited with code 1"):** Ensure the container runtime is running — `open -a Docker` (macOS Docker), `container system start` (Apple Container), or `sudo systemctl start docker` (Linux). Check container logs in `groups/main/logs/container-*.log`.
|
||||
|
||||
**No response to messages:** Verify the trigger pattern matches. Main channel and personal/solo chats don't need a prefix. Check the registered JID in the database: `sqlite3 store/messages.db "SELECT * FROM registered_groups"`. Check `logs/nanoclaw.log`.
|
||||
**No response to messages:** Check trigger pattern. Main channel doesn't need prefix. Check DB: `npx tsx src/setup/index.ts --step verify`. Check `logs/nanoclaw.log`.
|
||||
|
||||
**Messages sent but not received (DMs):** WhatsApp may use LID (Linked Identity) JIDs. Check logs for LID translation. Verify the registered JID has no device suffix (should be `number@s.whatsapp.net`, not `number:0@s.whatsapp.net`).
|
||||
**WhatsApp disconnected:** `npm run auth` then rebuild and restart: `npm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `systemctl --user restart nanoclaw` (Linux).
|
||||
|
||||
**WhatsApp disconnected:** Run `npm run auth` to re-authenticate, then `npm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw`.
|
||||
|
||||
**Unload service:** `launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist`
|
||||
**Unload service:** macOS: `launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist` | Linux: `systemctl --user stop nanoclaw`
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# 01-check-environment.sh — Detect OS, Node, container runtimes, existing config
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
||||
LOG_FILE="$PROJECT_ROOT/logs/setup.log"
|
||||
|
||||
mkdir -p "$PROJECT_ROOT/logs"
|
||||
|
||||
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [check-environment] $*" >> "$LOG_FILE"; }
|
||||
|
||||
log "Starting environment check"
|
||||
|
||||
# Detect platform
|
||||
UNAME=$(uname -s)
|
||||
case "$UNAME" in
|
||||
Darwin*) PLATFORM="macos" ;;
|
||||
Linux*) PLATFORM="linux" ;;
|
||||
*) PLATFORM="unknown" ;;
|
||||
esac
|
||||
log "Platform: $PLATFORM ($UNAME)"
|
||||
|
||||
# Check Node
|
||||
NODE_OK="false"
|
||||
NODE_VERSION="not_found"
|
||||
if command -v node >/dev/null 2>&1; then
|
||||
NODE_VERSION=$(node --version 2>/dev/null | sed 's/^v//')
|
||||
MAJOR=$(echo "$NODE_VERSION" | cut -d. -f1)
|
||||
if [ "$MAJOR" -ge 20 ] 2>/dev/null; then
|
||||
NODE_OK="true"
|
||||
fi
|
||||
log "Node $NODE_VERSION found (major=$MAJOR, ok=$NODE_OK)"
|
||||
else
|
||||
log "Node not found"
|
||||
fi
|
||||
|
||||
# Check Apple Container
|
||||
APPLE_CONTAINER="not_found"
|
||||
if command -v container >/dev/null 2>&1; then
|
||||
APPLE_CONTAINER="installed"
|
||||
log "Apple Container: installed ($(which container))"
|
||||
else
|
||||
log "Apple Container: not found"
|
||||
fi
|
||||
|
||||
# Check Docker
|
||||
DOCKER="not_found"
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
if docker info >/dev/null 2>&1; then
|
||||
DOCKER="running"
|
||||
log "Docker: running"
|
||||
else
|
||||
DOCKER="installed_not_running"
|
||||
log "Docker: installed but not running"
|
||||
fi
|
||||
else
|
||||
log "Docker: not found"
|
||||
fi
|
||||
|
||||
# Check existing config
|
||||
HAS_ENV="false"
|
||||
if [ -f "$PROJECT_ROOT/.env" ]; then
|
||||
HAS_ENV="true"
|
||||
log ".env file found"
|
||||
fi
|
||||
|
||||
HAS_AUTH="false"
|
||||
if [ -d "$PROJECT_ROOT/store/auth" ] && [ "$(ls -A "$PROJECT_ROOT/store/auth" 2>/dev/null)" ]; then
|
||||
HAS_AUTH="true"
|
||||
log "WhatsApp auth credentials found"
|
||||
fi
|
||||
|
||||
HAS_REGISTERED_GROUPS="false"
|
||||
if [ -f "$PROJECT_ROOT/data/registered_groups.json" ]; then
|
||||
HAS_REGISTERED_GROUPS="true"
|
||||
log "Registered groups config found (JSON)"
|
||||
elif [ -f "$PROJECT_ROOT/store/messages.db" ]; then
|
||||
RG_COUNT=$(sqlite3 "$PROJECT_ROOT/store/messages.db" "SELECT COUNT(*) FROM registered_groups" 2>/dev/null || echo "0")
|
||||
if [ "$RG_COUNT" -gt 0 ] 2>/dev/null; then
|
||||
HAS_REGISTERED_GROUPS="true"
|
||||
log "Registered groups found in database ($RG_COUNT)"
|
||||
fi
|
||||
fi
|
||||
|
||||
log "Environment check complete"
|
||||
|
||||
# Output structured status block
|
||||
cat <<EOF
|
||||
=== NANOCLAW SETUP: CHECK_ENVIRONMENT ===
|
||||
PLATFORM: $PLATFORM
|
||||
NODE_VERSION: $NODE_VERSION
|
||||
NODE_OK: $NODE_OK
|
||||
APPLE_CONTAINER: $APPLE_CONTAINER
|
||||
DOCKER: $DOCKER
|
||||
HAS_ENV: $HAS_ENV
|
||||
HAS_AUTH: $HAS_AUTH
|
||||
HAS_REGISTERED_GROUPS: $HAS_REGISTERED_GROUPS
|
||||
STATUS: success
|
||||
LOG: logs/setup.log
|
||||
=== END ===
|
||||
EOF
|
||||
|
||||
# Exit 2 if Node is missing or too old
|
||||
if [ "$NODE_OK" = "false" ]; then
|
||||
exit 2
|
||||
fi
|
||||
@@ -1,62 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# 02-install-deps.sh — Run npm install and verify key packages
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
||||
LOG_FILE="$PROJECT_ROOT/logs/setup.log"
|
||||
|
||||
mkdir -p "$PROJECT_ROOT/logs"
|
||||
|
||||
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [install-deps] $*" >> "$LOG_FILE"; }
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
log "Running npm install"
|
||||
|
||||
if npm install >> "$LOG_FILE" 2>&1; then
|
||||
log "npm install succeeded"
|
||||
else
|
||||
log "npm install failed"
|
||||
cat <<EOF
|
||||
=== NANOCLAW SETUP: INSTALL_DEPS ===
|
||||
PACKAGES: failed
|
||||
STATUS: failed
|
||||
ERROR: npm_install_failed
|
||||
LOG: logs/setup.log
|
||||
=== END ===
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify key packages
|
||||
MISSING=""
|
||||
for pkg in @whiskeysockets/baileys better-sqlite3 pino qrcode; do
|
||||
if [ ! -d "$PROJECT_ROOT/node_modules/$pkg" ]; then
|
||||
MISSING="$MISSING $pkg"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$MISSING" ]; then
|
||||
log "Missing packages after install:$MISSING"
|
||||
cat <<EOF
|
||||
=== NANOCLAW SETUP: INSTALL_DEPS ===
|
||||
PACKAGES: failed
|
||||
STATUS: failed
|
||||
ERROR: missing_packages:$MISSING
|
||||
LOG: logs/setup.log
|
||||
=== END ===
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "All key packages verified"
|
||||
|
||||
cat <<EOF
|
||||
=== NANOCLAW SETUP: INSTALL_DEPS ===
|
||||
PACKAGES: installed
|
||||
STATUS: success
|
||||
LOG: logs/setup.log
|
||||
=== END ===
|
||||
EOF
|
||||
@@ -1,150 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# 03-setup-container.sh — Build container image and verify with test run
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
||||
LOG_FILE="$PROJECT_ROOT/logs/setup.log"
|
||||
|
||||
mkdir -p "$PROJECT_ROOT/logs"
|
||||
|
||||
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [setup-container] $*" >> "$LOG_FILE"; }
|
||||
|
||||
# Parse args
|
||||
RUNTIME=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--runtime) RUNTIME="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$RUNTIME" ]; then
|
||||
log "ERROR: --runtime flag is required (apple-container|docker)"
|
||||
cat <<EOF
|
||||
=== NANOCLAW SETUP: SETUP_CONTAINER ===
|
||||
RUNTIME: unknown
|
||||
IMAGE: nanoclaw-agent:latest
|
||||
BUILD_OK: false
|
||||
TEST_OK: false
|
||||
STATUS: failed
|
||||
ERROR: missing_runtime_flag
|
||||
LOG: logs/setup.log
|
||||
=== END ===
|
||||
EOF
|
||||
exit 4
|
||||
fi
|
||||
|
||||
IMAGE="nanoclaw-agent:latest"
|
||||
|
||||
# Determine build/run commands based on runtime
|
||||
case "$RUNTIME" in
|
||||
apple-container)
|
||||
if ! command -v container >/dev/null 2>&1; then
|
||||
log "Apple Container runtime not found"
|
||||
cat <<EOF
|
||||
=== NANOCLAW SETUP: SETUP_CONTAINER ===
|
||||
RUNTIME: apple-container
|
||||
IMAGE: $IMAGE
|
||||
BUILD_OK: false
|
||||
TEST_OK: false
|
||||
STATUS: failed
|
||||
ERROR: runtime_not_available
|
||||
LOG: logs/setup.log
|
||||
=== END ===
|
||||
EOF
|
||||
exit 2
|
||||
fi
|
||||
BUILD_CMD="container build"
|
||||
RUN_CMD="container"
|
||||
;;
|
||||
docker)
|
||||
if ! command -v docker >/dev/null 2>&1 || ! docker info >/dev/null 2>&1; then
|
||||
log "Docker runtime not available or not running"
|
||||
cat <<EOF
|
||||
=== NANOCLAW SETUP: SETUP_CONTAINER ===
|
||||
RUNTIME: docker
|
||||
IMAGE: $IMAGE
|
||||
BUILD_OK: false
|
||||
TEST_OK: false
|
||||
STATUS: failed
|
||||
ERROR: runtime_not_available
|
||||
LOG: logs/setup.log
|
||||
=== END ===
|
||||
EOF
|
||||
exit 2
|
||||
fi
|
||||
BUILD_CMD="docker build"
|
||||
RUN_CMD="docker"
|
||||
;;
|
||||
*)
|
||||
log "Unknown runtime: $RUNTIME"
|
||||
cat <<EOF
|
||||
=== NANOCLAW SETUP: SETUP_CONTAINER ===
|
||||
RUNTIME: $RUNTIME
|
||||
IMAGE: $IMAGE
|
||||
BUILD_OK: false
|
||||
TEST_OK: false
|
||||
STATUS: failed
|
||||
ERROR: unknown_runtime
|
||||
LOG: logs/setup.log
|
||||
=== END ===
|
||||
EOF
|
||||
exit 4
|
||||
;;
|
||||
esac
|
||||
|
||||
log "Building container with $RUNTIME"
|
||||
|
||||
# Build
|
||||
BUILD_OK="false"
|
||||
if (cd "$PROJECT_ROOT/container" && $BUILD_CMD -t "$IMAGE" .) >> "$LOG_FILE" 2>&1; then
|
||||
BUILD_OK="true"
|
||||
log "Container build succeeded"
|
||||
else
|
||||
log "Container build failed"
|
||||
cat <<EOF
|
||||
=== NANOCLAW SETUP: SETUP_CONTAINER ===
|
||||
RUNTIME: $RUNTIME
|
||||
IMAGE: $IMAGE
|
||||
BUILD_OK: false
|
||||
TEST_OK: false
|
||||
STATUS: failed
|
||||
ERROR: build_failed
|
||||
LOG: logs/setup.log
|
||||
=== END ===
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test
|
||||
TEST_OK="false"
|
||||
log "Testing container with echo command"
|
||||
TEST_OUTPUT=$(echo '{}' | $RUN_CMD run -i --rm --entrypoint /bin/echo "$IMAGE" "Container OK" 2>>"$LOG_FILE") || true
|
||||
if echo "$TEST_OUTPUT" | grep -q "Container OK"; then
|
||||
TEST_OK="true"
|
||||
log "Container test passed"
|
||||
else
|
||||
log "Container test failed: $TEST_OUTPUT"
|
||||
fi
|
||||
|
||||
STATUS="success"
|
||||
if [ "$BUILD_OK" = "false" ] || [ "$TEST_OK" = "false" ]; then
|
||||
STATUS="failed"
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
=== NANOCLAW SETUP: SETUP_CONTAINER ===
|
||||
RUNTIME: $RUNTIME
|
||||
IMAGE: $IMAGE
|
||||
BUILD_OK: $BUILD_OK
|
||||
TEST_OK: $TEST_OK
|
||||
STATUS: $STATUS
|
||||
LOG: logs/setup.log
|
||||
=== END ===
|
||||
EOF
|
||||
|
||||
if [ "$STATUS" = "failed" ]; then
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,347 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# 04-auth-whatsapp.sh — Full WhatsApp auth flow with polling
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
||||
LOG_FILE="$PROJECT_ROOT/logs/setup.log"
|
||||
|
||||
mkdir -p "$PROJECT_ROOT/logs"
|
||||
|
||||
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [auth-whatsapp] $*" >> "$LOG_FILE"; }
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# Parse args
|
||||
METHOD=""
|
||||
PHONE=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--method) METHOD="$2"; shift 2 ;;
|
||||
--phone) PHONE="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$METHOD" ]; then
|
||||
log "ERROR: --method flag is required"
|
||||
cat <<EOF
|
||||
=== NANOCLAW SETUP: AUTH_WHATSAPP ===
|
||||
AUTH_METHOD: unknown
|
||||
AUTH_STATUS: failed
|
||||
STATUS: failed
|
||||
ERROR: missing_method_flag
|
||||
LOG: logs/setup.log
|
||||
=== END ===
|
||||
EOF
|
||||
exit 4
|
||||
fi
|
||||
|
||||
# Background process PID for cleanup
|
||||
AUTH_PID=""
|
||||
cleanup() {
|
||||
if [ -n "$AUTH_PID" ] && kill -0 "$AUTH_PID" 2>/dev/null; then
|
||||
log "Cleaning up auth process (PID $AUTH_PID)"
|
||||
kill "$AUTH_PID" 2>/dev/null || true
|
||||
wait "$AUTH_PID" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# Helper: poll a file for a pattern
|
||||
# Usage: poll_file FILE PATTERN TIMEOUT_SECS INTERVAL_SECS
|
||||
poll_file() {
|
||||
local file="$1" pattern="$2" timeout="$3" interval="$4"
|
||||
local elapsed=0
|
||||
while [ "$elapsed" -lt "$timeout" ]; do
|
||||
if [ -f "$file" ]; then
|
||||
local content
|
||||
content=$(cat "$file" 2>/dev/null || echo "")
|
||||
if echo "$content" | grep -qE "$pattern"; then
|
||||
echo "$content"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
sleep "$interval"
|
||||
elapsed=$((elapsed + interval))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Helper: get phone number from auth creds if available
|
||||
get_phone_number() {
|
||||
if [ -f "$PROJECT_ROOT/store/auth/creds.json" ]; then
|
||||
node -e "
|
||||
const c = require('./store/auth/creds.json');
|
||||
if (c.me && c.me.id) {
|
||||
const phone = c.me.id.split(':')[0].split('@')[0];
|
||||
process.stdout.write(phone);
|
||||
}
|
||||
" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
clean_stale_state() {
|
||||
log "Cleaning stale auth state"
|
||||
rm -rf "$PROJECT_ROOT/store/auth" "$PROJECT_ROOT/store/qr-data.txt" "$PROJECT_ROOT/store/auth-status.txt"
|
||||
}
|
||||
|
||||
emit_status() {
|
||||
local auth_status="$1" status="$2" error="${3:-}" pairing_code="${4:-}"
|
||||
local phone_number
|
||||
phone_number=$(get_phone_number)
|
||||
|
||||
cat <<EOF
|
||||
=== NANOCLAW SETUP: AUTH_WHATSAPP ===
|
||||
AUTH_METHOD: $METHOD
|
||||
AUTH_STATUS: $auth_status
|
||||
EOF
|
||||
[ -n "$pairing_code" ] && echo "PAIRING_CODE: $pairing_code"
|
||||
[ -n "$phone_number" ] && echo "PHONE_NUMBER: $phone_number"
|
||||
echo "STATUS: $status"
|
||||
[ -n "$error" ] && echo "ERROR: $error"
|
||||
cat <<EOF
|
||||
LOG: logs/setup.log
|
||||
=== END ===
|
||||
EOF
|
||||
}
|
||||
|
||||
case "$METHOD" in
|
||||
|
||||
qr-browser)
|
||||
log "Starting QR browser auth flow"
|
||||
clean_stale_state
|
||||
|
||||
# Start auth in background
|
||||
npm run auth >> "$LOG_FILE" 2>&1 &
|
||||
AUTH_PID=$!
|
||||
log "Auth process started (PID $AUTH_PID)"
|
||||
|
||||
# Poll for QR data or already_authenticated
|
||||
log "Polling for QR data (15s timeout)"
|
||||
QR_READY="false"
|
||||
for i in $(seq 1 15); do
|
||||
if [ -f "$PROJECT_ROOT/store/auth-status.txt" ]; then
|
||||
STATUS_CONTENT=$(cat "$PROJECT_ROOT/store/auth-status.txt" 2>/dev/null || echo "")
|
||||
if [ "$STATUS_CONTENT" = "already_authenticated" ]; then
|
||||
log "Already authenticated"
|
||||
emit_status "already_authenticated" "success"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
if [ -f "$PROJECT_ROOT/store/qr-data.txt" ]; then
|
||||
QR_READY="true"
|
||||
break
|
||||
fi
|
||||
# Check if auth process died early
|
||||
if ! kill -0 "$AUTH_PID" 2>/dev/null; then
|
||||
log "Auth process exited prematurely"
|
||||
emit_status "failed" "failed" "auth_process_crashed"
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if [ "$QR_READY" = "false" ]; then
|
||||
log "Timeout waiting for QR data"
|
||||
emit_status "failed" "failed" "qr_timeout"
|
||||
exit 3
|
||||
fi
|
||||
|
||||
# Generate QR SVG and inject into HTML template
|
||||
log "Generating QR SVG"
|
||||
node -e "
|
||||
const QR = require('qrcode');
|
||||
const fs = require('fs');
|
||||
const qrData = fs.readFileSync('store/qr-data.txt', 'utf8');
|
||||
QR.toString(qrData, { type: 'svg' }, (err, svg) => {
|
||||
if (err) process.exit(1);
|
||||
const template = fs.readFileSync('.claude/skills/setup/scripts/qr-auth.html', 'utf8');
|
||||
fs.writeFileSync('store/qr-auth.html', template.replace('{{QR_SVG}}', svg));
|
||||
});
|
||||
" >> "$LOG_FILE" 2>&1
|
||||
|
||||
# Open in browser (macOS)
|
||||
if command -v open >/dev/null 2>&1; then
|
||||
open "$PROJECT_ROOT/store/qr-auth.html"
|
||||
log "Opened QR auth page in browser"
|
||||
else
|
||||
log "WARNING: 'open' command not found, cannot open browser"
|
||||
fi
|
||||
|
||||
# Poll for completion (120s, 2s intervals)
|
||||
log "Polling for auth completion (120s timeout)"
|
||||
for i in $(seq 1 60); do
|
||||
if [ -f "$PROJECT_ROOT/store/auth-status.txt" ]; then
|
||||
STATUS_CONTENT=$(cat "$PROJECT_ROOT/store/auth-status.txt" 2>/dev/null || echo "")
|
||||
case "$STATUS_CONTENT" in
|
||||
authenticated|already_authenticated)
|
||||
log "Authentication successful: $STATUS_CONTENT"
|
||||
# Replace QR page with success page so browser auto-refresh shows it
|
||||
cat > "$PROJECT_ROOT/store/qr-auth.html" <<'SUCCESSEOF'
|
||||
<!DOCTYPE html>
|
||||
<html><head><title>NanoClaw - Connected!</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #f5f5f5; }
|
||||
.card { background: white; border-radius: 16px; padding: 40px; box-shadow: 0 4px 24px rgba(0,0,0,0.1); text-align: center; max-width: 400px; }
|
||||
h2 { color: #27ae60; margin: 0 0 8px; }
|
||||
p { color: #666; }
|
||||
.check { font-size: 64px; margin-bottom: 16px; }
|
||||
</style></head><body>
|
||||
<div class="card">
|
||||
<div class="check">✓</div>
|
||||
<h2>Connected to WhatsApp</h2>
|
||||
<p>You can close this tab.</p>
|
||||
</div>
|
||||
<script>localStorage.removeItem('nanoclaw_qr_start');</script>
|
||||
</body></html>
|
||||
SUCCESSEOF
|
||||
emit_status "$STATUS_CONTENT" "success"
|
||||
exit 0
|
||||
;;
|
||||
failed:logged_out)
|
||||
log "Auth failed: logged out"
|
||||
emit_status "failed" "failed" "logged_out"
|
||||
exit 1
|
||||
;;
|
||||
failed:qr_timeout)
|
||||
log "Auth failed: QR timeout"
|
||||
emit_status "failed" "failed" "qr_timeout"
|
||||
exit 1
|
||||
;;
|
||||
failed:515)
|
||||
log "Auth failed: 515 stream error"
|
||||
emit_status "failed" "failed" "515"
|
||||
exit 1
|
||||
;;
|
||||
failed:*)
|
||||
log "Auth failed: $STATUS_CONTENT"
|
||||
emit_status "failed" "failed" "${STATUS_CONTENT#failed:}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
log "Timeout waiting for auth completion"
|
||||
emit_status "failed" "failed" "timeout"
|
||||
exit 3
|
||||
;;
|
||||
|
||||
pairing-code)
|
||||
if [ -z "$PHONE" ]; then
|
||||
log "ERROR: --phone is required for pairing-code method"
|
||||
cat <<EOF
|
||||
=== NANOCLAW SETUP: AUTH_WHATSAPP ===
|
||||
AUTH_METHOD: pairing-code
|
||||
AUTH_STATUS: failed
|
||||
STATUS: failed
|
||||
ERROR: missing_phone_number
|
||||
LOG: logs/setup.log
|
||||
=== END ===
|
||||
EOF
|
||||
exit 4
|
||||
fi
|
||||
|
||||
log "Starting pairing code auth flow (phone: $PHONE)"
|
||||
clean_stale_state
|
||||
|
||||
# Start auth with pairing code in background
|
||||
npx tsx src/whatsapp-auth.ts --pairing-code --phone "$PHONE" >> "$LOG_FILE" 2>&1 &
|
||||
AUTH_PID=$!
|
||||
log "Auth process started (PID $AUTH_PID)"
|
||||
|
||||
# Poll for pairing code or already_authenticated
|
||||
log "Polling for pairing code (15s timeout)"
|
||||
PAIRING_CODE=""
|
||||
for i in $(seq 1 15); do
|
||||
if [ -f "$PROJECT_ROOT/store/auth-status.txt" ]; then
|
||||
STATUS_CONTENT=$(cat "$PROJECT_ROOT/store/auth-status.txt" 2>/dev/null || echo "")
|
||||
case "$STATUS_CONTENT" in
|
||||
already_authenticated)
|
||||
log "Already authenticated"
|
||||
emit_status "already_authenticated" "success"
|
||||
exit 0
|
||||
;;
|
||||
pairing_code:*)
|
||||
PAIRING_CODE="${STATUS_CONTENT#pairing_code:}"
|
||||
log "Got pairing code: $PAIRING_CODE"
|
||||
break
|
||||
;;
|
||||
failed:*)
|
||||
log "Auth failed early: $STATUS_CONTENT"
|
||||
emit_status "failed" "failed" "${STATUS_CONTENT#failed:}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if [ -z "$PAIRING_CODE" ]; then
|
||||
log "Timeout waiting for pairing code"
|
||||
emit_status "failed" "failed" "pairing_code_timeout"
|
||||
exit 3
|
||||
fi
|
||||
|
||||
# Poll for completion (120s, 2s intervals)
|
||||
log "Polling for auth completion (120s timeout)"
|
||||
for i in $(seq 1 60); do
|
||||
if [ -f "$PROJECT_ROOT/store/auth-status.txt" ]; then
|
||||
STATUS_CONTENT=$(cat "$PROJECT_ROOT/store/auth-status.txt" 2>/dev/null || echo "")
|
||||
case "$STATUS_CONTENT" in
|
||||
authenticated|already_authenticated)
|
||||
log "Authentication successful: $STATUS_CONTENT"
|
||||
emit_status "$STATUS_CONTENT" "success" "" "$PAIRING_CODE"
|
||||
exit 0
|
||||
;;
|
||||
failed:logged_out)
|
||||
log "Auth failed: logged out"
|
||||
emit_status "failed" "failed" "logged_out" "$PAIRING_CODE"
|
||||
exit 1
|
||||
;;
|
||||
failed:*)
|
||||
log "Auth failed: $STATUS_CONTENT"
|
||||
emit_status "failed" "failed" "${STATUS_CONTENT#failed:}" "$PAIRING_CODE"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
log "Timeout waiting for auth completion"
|
||||
emit_status "failed" "failed" "timeout" "$PAIRING_CODE"
|
||||
exit 3
|
||||
;;
|
||||
|
||||
qr-terminal)
|
||||
log "QR terminal method selected — manual flow"
|
||||
cat <<EOF
|
||||
=== NANOCLAW SETUP: AUTH_WHATSAPP ===
|
||||
AUTH_METHOD: qr-terminal
|
||||
AUTH_STATUS: manual
|
||||
PROJECT_PATH: $PROJECT_ROOT
|
||||
STATUS: manual
|
||||
LOG: logs/setup.log
|
||||
=== END ===
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
|
||||
*)
|
||||
log "Unknown auth method: $METHOD"
|
||||
cat <<EOF
|
||||
=== NANOCLAW SETUP: AUTH_WHATSAPP ===
|
||||
AUTH_METHOD: $METHOD
|
||||
AUTH_STATUS: failed
|
||||
STATUS: failed
|
||||
ERROR: unknown_method
|
||||
LOG: logs/setup.log
|
||||
=== END ===
|
||||
EOF
|
||||
exit 4
|
||||
;;
|
||||
esac
|
||||
@@ -1,141 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# 05-sync-groups.sh — Connect to WhatsApp, fetch group metadata, write to DB, exit.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
||||
LOG_FILE="$PROJECT_ROOT/logs/setup.log"
|
||||
|
||||
mkdir -p "$PROJECT_ROOT/logs"
|
||||
|
||||
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [sync-groups] $*" >> "$LOG_FILE"; }
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# Build TypeScript
|
||||
log "Building TypeScript"
|
||||
BUILD="failed"
|
||||
if npm run build >> "$LOG_FILE" 2>&1; then
|
||||
BUILD="success"
|
||||
log "Build succeeded"
|
||||
else
|
||||
log "Build failed"
|
||||
cat <<EOF
|
||||
=== NANOCLAW SETUP: SYNC_GROUPS ===
|
||||
BUILD: failed
|
||||
SYNC: skipped
|
||||
GROUPS_IN_DB: 0
|
||||
STATUS: failed
|
||||
ERROR: build_failed
|
||||
LOG: logs/setup.log
|
||||
=== END ===
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Directly connect, fetch groups, write to DB, exit
|
||||
log "Fetching group metadata directly"
|
||||
SYNC="failed"
|
||||
|
||||
SYNC_OUTPUT=$(node -e "
|
||||
import makeWASocket, { useMultiFileAuthState, makeCacheableSignalKeyStore, Browsers } from '@whiskeysockets/baileys';
|
||||
import pino from 'pino';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import Database from 'better-sqlite3';
|
||||
|
||||
const logger = pino({ level: 'silent' });
|
||||
const authDir = path.join('store', 'auth');
|
||||
const dbPath = path.join('store', 'messages.db');
|
||||
|
||||
if (!fs.existsSync(authDir)) {
|
||||
console.error('NO_AUTH');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const db = new Database(dbPath);
|
||||
db.pragma('journal_mode = WAL');
|
||||
db.exec('CREATE TABLE IF NOT EXISTS chats (jid TEXT PRIMARY KEY, name TEXT, last_message_time TEXT)');
|
||||
|
||||
const upsert = db.prepare(
|
||||
'INSERT INTO chats (jid, name, last_message_time) VALUES (?, ?, ?) ON CONFLICT(jid) DO UPDATE SET name = excluded.name'
|
||||
);
|
||||
|
||||
const { state, saveCreds } = await useMultiFileAuthState(authDir);
|
||||
|
||||
const sock = makeWASocket({
|
||||
auth: { creds: state.creds, keys: makeCacheableSignalKeyStore(state.keys, logger) },
|
||||
printQRInTerminal: false,
|
||||
logger,
|
||||
browser: Browsers.macOS('Chrome'),
|
||||
});
|
||||
|
||||
// Timeout after 30s
|
||||
const timeout = setTimeout(() => {
|
||||
console.error('TIMEOUT');
|
||||
process.exit(1);
|
||||
}, 30000);
|
||||
|
||||
sock.ev.on('creds.update', saveCreds);
|
||||
|
||||
sock.ev.on('connection.update', async (update) => {
|
||||
if (update.connection === 'open') {
|
||||
try {
|
||||
const groups = await sock.groupFetchAllParticipating();
|
||||
const now = new Date().toISOString();
|
||||
let count = 0;
|
||||
for (const [jid, metadata] of Object.entries(groups)) {
|
||||
if (metadata.subject) {
|
||||
upsert.run(jid, metadata.subject, now);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
console.log('SYNCED:' + count);
|
||||
} catch (err) {
|
||||
console.error('FETCH_ERROR:' + err.message);
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
sock.end(undefined);
|
||||
db.close();
|
||||
process.exit(0);
|
||||
}
|
||||
} else if (update.connection === 'close') {
|
||||
clearTimeout(timeout);
|
||||
console.error('CONNECTION_CLOSED');
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
" --input-type=module 2>&1) || true
|
||||
|
||||
log "Sync output: $SYNC_OUTPUT"
|
||||
|
||||
if echo "$SYNC_OUTPUT" | grep -q "SYNCED:"; then
|
||||
SYNC="success"
|
||||
fi
|
||||
|
||||
# Check for groups in DB
|
||||
GROUPS_IN_DB=0
|
||||
if [ -f "$PROJECT_ROOT/store/messages.db" ]; then
|
||||
GROUPS_IN_DB=$(sqlite3 "$PROJECT_ROOT/store/messages.db" "SELECT COUNT(*) FROM chats WHERE jid LIKE '%@g.us' AND jid <> '__group_sync__'" 2>/dev/null || echo "0")
|
||||
log "Groups found in DB: $GROUPS_IN_DB"
|
||||
fi
|
||||
|
||||
STATUS="success"
|
||||
if [ "$SYNC" != "success" ]; then
|
||||
STATUS="failed"
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
=== NANOCLAW SETUP: SYNC_GROUPS ===
|
||||
BUILD: $BUILD
|
||||
SYNC: $SYNC
|
||||
GROUPS_IN_DB: $GROUPS_IN_DB
|
||||
STATUS: $STATUS
|
||||
LOG: logs/setup.log
|
||||
=== END ===
|
||||
EOF
|
||||
|
||||
if [ "$STATUS" = "failed" ]; then
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,18 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# 05b-list-groups.sh — Query WhatsApp groups from the database.
|
||||
# Output: pipe-separated JID|name lines, most recent first.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
||||
DB_PATH="$PROJECT_ROOT/store/messages.db"
|
||||
|
||||
LIMIT="${1:-30}"
|
||||
|
||||
if [ ! -f "$DB_PATH" ]; then
|
||||
echo "ERROR: database not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sqlite3 "$DB_PATH" "SELECT jid, name FROM chats WHERE jid LIKE '%@g.us' AND jid <> '__group_sync__' AND name <> jid ORDER BY last_message_time DESC LIMIT $LIMIT"
|
||||
@@ -1,106 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# 06-register-channel.sh — Write channel registration config, create group folders
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
||||
LOG_FILE="$PROJECT_ROOT/logs/setup.log"
|
||||
|
||||
mkdir -p "$PROJECT_ROOT/logs"
|
||||
|
||||
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [register-channel] $*" >> "$LOG_FILE"; }
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# Parse args
|
||||
JID=""
|
||||
NAME=""
|
||||
TRIGGER=""
|
||||
FOLDER=""
|
||||
REQUIRES_TRIGGER="true"
|
||||
ASSISTANT_NAME="Andy"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--jid) JID="$2"; shift 2 ;;
|
||||
--name) NAME="$2"; shift 2 ;;
|
||||
--trigger) TRIGGER="$2"; shift 2 ;;
|
||||
--folder) FOLDER="$2"; shift 2 ;;
|
||||
--no-trigger-required) REQUIRES_TRIGGER="false"; shift ;;
|
||||
--assistant-name) ASSISTANT_NAME="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate required args
|
||||
if [ -z "$JID" ] || [ -z "$NAME" ] || [ -z "$TRIGGER" ] || [ -z "$FOLDER" ]; then
|
||||
log "ERROR: Missing required args (--jid, --name, --trigger, --folder)"
|
||||
cat <<EOF
|
||||
=== NANOCLAW SETUP: REGISTER_CHANNEL ===
|
||||
STATUS: failed
|
||||
ERROR: missing_required_args
|
||||
LOG: logs/setup.log
|
||||
=== END ===
|
||||
EOF
|
||||
exit 4
|
||||
fi
|
||||
|
||||
log "Registering channel: jid=$JID name=$NAME trigger=$TRIGGER folder=$FOLDER requiresTrigger=$REQUIRES_TRIGGER"
|
||||
|
||||
# Create data directory
|
||||
mkdir -p "$PROJECT_ROOT/data"
|
||||
|
||||
# Write directly to SQLite (the DB and schema exist from the sync-groups step)
|
||||
TIMESTAMP=$(date -u '+%Y-%m-%dT%H:%M:%S.000Z')
|
||||
DB_PATH="$PROJECT_ROOT/store/messages.db"
|
||||
REQUIRES_TRIGGER_INT=$( [ "$REQUIRES_TRIGGER" = "true" ] && echo 1 || echo 0 )
|
||||
|
||||
sqlite3 "$DB_PATH" "INSERT OR REPLACE INTO registered_groups (jid, name, folder, trigger_pattern, added_at, container_config, requires_trigger) VALUES ('$JID', '$NAME', '$FOLDER', '$TRIGGER', '$TIMESTAMP', NULL, $REQUIRES_TRIGGER_INT);"
|
||||
|
||||
log "Wrote registration to SQLite"
|
||||
|
||||
# Create group folders
|
||||
mkdir -p "$PROJECT_ROOT/groups/$FOLDER/logs"
|
||||
log "Created groups/$FOLDER/logs/"
|
||||
|
||||
# Update assistant name in CLAUDE.md files if different from default
|
||||
NAME_UPDATED="false"
|
||||
if [ "$ASSISTANT_NAME" != "Andy" ]; then
|
||||
log "Updating assistant name from Andy to $ASSISTANT_NAME"
|
||||
|
||||
for md_file in groups/global/CLAUDE.md groups/main/CLAUDE.md; do
|
||||
if [ -f "$PROJECT_ROOT/$md_file" ]; then
|
||||
sed -i '' "s/^# Andy$/# $ASSISTANT_NAME/" "$PROJECT_ROOT/$md_file"
|
||||
sed -i '' "s/You are Andy/You are $ASSISTANT_NAME/g" "$PROJECT_ROOT/$md_file"
|
||||
log "Updated $md_file"
|
||||
else
|
||||
log "WARNING: $md_file not found, skipping name update"
|
||||
fi
|
||||
done
|
||||
|
||||
# Add ASSISTANT_NAME to .env so config.ts picks it up
|
||||
ENV_FILE="$PROJECT_ROOT/.env"
|
||||
if [ -f "$ENV_FILE" ] && grep -q '^ASSISTANT_NAME=' "$ENV_FILE"; then
|
||||
sed "s|^ASSISTANT_NAME=.*|ASSISTANT_NAME=\"$ASSISTANT_NAME\"|" "$ENV_FILE" > "$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE"
|
||||
else
|
||||
echo "ASSISTANT_NAME=\"$ASSISTANT_NAME\"" >> "$ENV_FILE"
|
||||
fi
|
||||
log "Set ASSISTANT_NAME=$ASSISTANT_NAME in .env"
|
||||
|
||||
NAME_UPDATED="true"
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
=== NANOCLAW SETUP: REGISTER_CHANNEL ===
|
||||
JID: $JID
|
||||
NAME: $NAME
|
||||
FOLDER: $FOLDER
|
||||
TRIGGER: $TRIGGER
|
||||
REQUIRES_TRIGGER: $REQUIRES_TRIGGER
|
||||
ASSISTANT_NAME: $ASSISTANT_NAME
|
||||
NAME_UPDATED: $NAME_UPDATED
|
||||
STATUS: success
|
||||
LOG: logs/setup.log
|
||||
=== END ===
|
||||
EOF
|
||||
@@ -1,80 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# 07-configure-mounts.sh — Write mount allowlist config file
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
||||
LOG_FILE="$PROJECT_ROOT/logs/setup.log"
|
||||
|
||||
mkdir -p "$PROJECT_ROOT/logs"
|
||||
|
||||
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [configure-mounts] $*" >> "$LOG_FILE"; }
|
||||
|
||||
CONFIG_DIR="$HOME/.config/nanoclaw"
|
||||
CONFIG_FILE="$CONFIG_DIR/mount-allowlist.json"
|
||||
|
||||
# Parse args
|
||||
EMPTY_MODE="false"
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--empty) EMPTY_MODE="true"; shift ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Create config directory
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
log "Ensured config directory: $CONFIG_DIR"
|
||||
|
||||
if [ "$EMPTY_MODE" = "true" ]; then
|
||||
log "Writing empty mount allowlist"
|
||||
cat > "$CONFIG_FILE" <<'JSONEOF'
|
||||
{
|
||||
"allowedRoots": [],
|
||||
"blockedPatterns": [],
|
||||
"nonMainReadOnly": true
|
||||
}
|
||||
JSONEOF
|
||||
ALLOWED_ROOTS=0
|
||||
NON_MAIN_READ_ONLY="true"
|
||||
else
|
||||
# Read JSON from stdin
|
||||
log "Reading mount allowlist from stdin"
|
||||
INPUT=$(cat)
|
||||
|
||||
# Validate JSON
|
||||
if ! echo "$INPUT" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{JSON.parse(d)}catch(e){process.exit(1)}})" 2>/dev/null; then
|
||||
log "ERROR: Invalid JSON input"
|
||||
cat <<EOF
|
||||
=== NANOCLAW SETUP: CONFIGURE_MOUNTS ===
|
||||
PATH: $CONFIG_FILE
|
||||
ALLOWED_ROOTS: 0
|
||||
NON_MAIN_READ_ONLY: unknown
|
||||
STATUS: failed
|
||||
ERROR: invalid_json
|
||||
LOG: logs/setup.log
|
||||
=== END ===
|
||||
EOF
|
||||
exit 4
|
||||
fi
|
||||
|
||||
echo "$INPUT" > "$CONFIG_FILE"
|
||||
log "Wrote mount allowlist from stdin"
|
||||
|
||||
# Extract values
|
||||
ALLOWED_ROOTS=$(node -e "const d=require('$CONFIG_FILE');console.log((d.allowedRoots||[]).length)" 2>/dev/null || echo "0")
|
||||
NON_MAIN_READ_ONLY=$(node -e "const d=require('$CONFIG_FILE');console.log(d.nonMainReadOnly===false?'false':'true')" 2>/dev/null || echo "true")
|
||||
fi
|
||||
|
||||
log "Allowlist configured: $ALLOWED_ROOTS roots, nonMainReadOnly=$NON_MAIN_READ_ONLY"
|
||||
|
||||
cat <<EOF
|
||||
=== NANOCLAW SETUP: CONFIGURE_MOUNTS ===
|
||||
PATH: $CONFIG_FILE
|
||||
ALLOWED_ROOTS: $ALLOWED_ROOTS
|
||||
NON_MAIN_READ_ONLY: $NON_MAIN_READ_ONLY
|
||||
STATUS: success
|
||||
LOG: logs/setup.log
|
||||
=== END ===
|
||||
EOF
|
||||
@@ -1,197 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# 08-setup-service.sh — Generate and load service manager config
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
||||
LOG_FILE="$PROJECT_ROOT/logs/setup.log"
|
||||
|
||||
mkdir -p "$PROJECT_ROOT/logs"
|
||||
|
||||
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [setup-service] $*" >> "$LOG_FILE"; }
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# Parse args
|
||||
PLATFORM=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--platform) PLATFORM="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Auto-detect platform
|
||||
if [ -z "$PLATFORM" ]; then
|
||||
case "$(uname -s)" in
|
||||
Darwin*) PLATFORM="macos" ;;
|
||||
Linux*) PLATFORM="linux" ;;
|
||||
*) PLATFORM="unknown" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
NODE_PATH=$(which node)
|
||||
PROJECT_PATH="$PROJECT_ROOT"
|
||||
HOME_PATH="$HOME"
|
||||
|
||||
log "Setting up service: platform=$PLATFORM node=$NODE_PATH project=$PROJECT_PATH"
|
||||
|
||||
# Build first
|
||||
log "Building TypeScript"
|
||||
if ! npm run build >> "$LOG_FILE" 2>&1; then
|
||||
log "Build failed"
|
||||
cat <<EOF
|
||||
=== NANOCLAW SETUP: SETUP_SERVICE ===
|
||||
SERVICE_TYPE: unknown
|
||||
NODE_PATH: $NODE_PATH
|
||||
PROJECT_PATH: $PROJECT_PATH
|
||||
STATUS: failed
|
||||
ERROR: build_failed
|
||||
LOG: logs/setup.log
|
||||
=== END ===
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create logs directory
|
||||
mkdir -p "$PROJECT_PATH/logs"
|
||||
|
||||
case "$PLATFORM" in
|
||||
|
||||
macos)
|
||||
PLIST_PATH="$HOME_PATH/Library/LaunchAgents/com.nanoclaw.plist"
|
||||
log "Generating launchd plist at $PLIST_PATH"
|
||||
|
||||
mkdir -p "$HOME_PATH/Library/LaunchAgents"
|
||||
|
||||
cat > "$PLIST_PATH" <<PLISTEOF
|
||||
<?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">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.nanoclaw</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>${NODE_PATH}</string>
|
||||
<string>${PROJECT_PATH}/dist/index.js</string>
|
||||
</array>
|
||||
<key>WorkingDirectory</key>
|
||||
<string>${PROJECT_PATH}</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}/.local/bin</string>
|
||||
<key>HOME</key>
|
||||
<string>${HOME_PATH}</string>
|
||||
</dict>
|
||||
<key>StandardOutPath</key>
|
||||
<string>${PROJECT_PATH}/logs/nanoclaw.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>${PROJECT_PATH}/logs/nanoclaw.error.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
PLISTEOF
|
||||
|
||||
log "Loading launchd service"
|
||||
if launchctl load "$PLIST_PATH" >> "$LOG_FILE" 2>&1; then
|
||||
log "launchctl load succeeded"
|
||||
else
|
||||
log "launchctl load failed (may already be loaded)"
|
||||
fi
|
||||
|
||||
# Verify
|
||||
SERVICE_LOADED="false"
|
||||
if launchctl list 2>/dev/null | grep -q "com.nanoclaw"; then
|
||||
SERVICE_LOADED="true"
|
||||
log "Service verified as loaded"
|
||||
else
|
||||
log "Service not found in launchctl list"
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
=== NANOCLAW SETUP: SETUP_SERVICE ===
|
||||
SERVICE_TYPE: launchd
|
||||
NODE_PATH: $NODE_PATH
|
||||
PROJECT_PATH: $PROJECT_PATH
|
||||
PLIST_PATH: $PLIST_PATH
|
||||
SERVICE_LOADED: $SERVICE_LOADED
|
||||
STATUS: success
|
||||
LOG: logs/setup.log
|
||||
=== END ===
|
||||
EOF
|
||||
;;
|
||||
|
||||
linux)
|
||||
UNIT_DIR="$HOME_PATH/.config/systemd/user"
|
||||
UNIT_PATH="$UNIT_DIR/nanoclaw.service"
|
||||
mkdir -p "$UNIT_DIR"
|
||||
log "Generating systemd unit at $UNIT_PATH"
|
||||
|
||||
cat > "$UNIT_PATH" <<UNITEOF
|
||||
[Unit]
|
||||
Description=NanoClaw Personal Assistant
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=${NODE_PATH} ${PROJECT_PATH}/dist/index.js
|
||||
WorkingDirectory=${PROJECT_PATH}
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
Environment=HOME=${HOME_PATH}
|
||||
Environment=PATH=/usr/local/bin:/usr/bin:/bin:${HOME_PATH}/.local/bin
|
||||
StandardOutput=append:${PROJECT_PATH}/logs/nanoclaw.log
|
||||
StandardError=append:${PROJECT_PATH}/logs/nanoclaw.error.log
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
UNITEOF
|
||||
|
||||
log "Enabling and starting systemd service"
|
||||
systemctl --user daemon-reload >> "$LOG_FILE" 2>&1 || true
|
||||
systemctl --user enable nanoclaw >> "$LOG_FILE" 2>&1 || true
|
||||
systemctl --user start nanoclaw >> "$LOG_FILE" 2>&1 || true
|
||||
|
||||
# Verify
|
||||
SERVICE_LOADED="false"
|
||||
if systemctl --user is-active nanoclaw >/dev/null 2>&1; then
|
||||
SERVICE_LOADED="true"
|
||||
log "Service verified as active"
|
||||
else
|
||||
log "Service not active"
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
=== NANOCLAW SETUP: SETUP_SERVICE ===
|
||||
SERVICE_TYPE: systemd
|
||||
NODE_PATH: $NODE_PATH
|
||||
PROJECT_PATH: $PROJECT_PATH
|
||||
UNIT_PATH: $UNIT_PATH
|
||||
SERVICE_LOADED: $SERVICE_LOADED
|
||||
STATUS: success
|
||||
LOG: logs/setup.log
|
||||
=== END ===
|
||||
EOF
|
||||
;;
|
||||
|
||||
*)
|
||||
log "Unsupported platform: $PLATFORM"
|
||||
cat <<EOF
|
||||
=== NANOCLAW SETUP: SETUP_SERVICE ===
|
||||
SERVICE_TYPE: unknown
|
||||
NODE_PATH: $NODE_PATH
|
||||
PROJECT_PATH: $PROJECT_PATH
|
||||
STATUS: failed
|
||||
ERROR: unsupported_platform
|
||||
LOG: logs/setup.log
|
||||
=== END ===
|
||||
EOF
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -1,109 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# 09-verify.sh — End-to-end health check of the full installation
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
||||
LOG_FILE="$PROJECT_ROOT/logs/setup.log"
|
||||
|
||||
mkdir -p "$PROJECT_ROOT/logs"
|
||||
|
||||
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [verify] $*" >> "$LOG_FILE"; }
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
log "Starting verification"
|
||||
|
||||
# Detect platform
|
||||
case "$(uname -s)" in
|
||||
Darwin*) PLATFORM="macos" ;;
|
||||
Linux*) PLATFORM="linux" ;;
|
||||
*) PLATFORM="unknown" ;;
|
||||
esac
|
||||
|
||||
# 1. Check service status
|
||||
SERVICE="not_found"
|
||||
if [ "$PLATFORM" = "macos" ]; then
|
||||
if launchctl list 2>/dev/null | grep -q "com.nanoclaw"; then
|
||||
# Check if it has a PID (actually running)
|
||||
LAUNCHCTL_LINE=$(launchctl list 2>/dev/null | grep "com.nanoclaw" || true)
|
||||
PID_FIELD=$(echo "$LAUNCHCTL_LINE" | awk '{print $1}')
|
||||
if [ "$PID_FIELD" != "-" ] && [ -n "$PID_FIELD" ]; then
|
||||
SERVICE="running"
|
||||
else
|
||||
SERVICE="stopped"
|
||||
fi
|
||||
fi
|
||||
elif [ "$PLATFORM" = "linux" ]; then
|
||||
if systemctl --user is-active nanoclaw >/dev/null 2>&1; then
|
||||
SERVICE="running"
|
||||
elif systemctl --user list-unit-files 2>/dev/null | grep -q "nanoclaw"; then
|
||||
SERVICE="stopped"
|
||||
fi
|
||||
fi
|
||||
log "Service: $SERVICE"
|
||||
|
||||
# 2. Check container runtime
|
||||
CONTAINER_RUNTIME="none"
|
||||
if command -v container >/dev/null 2>&1; then
|
||||
CONTAINER_RUNTIME="apple-container"
|
||||
elif command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
|
||||
CONTAINER_RUNTIME="docker"
|
||||
fi
|
||||
log "Container runtime: $CONTAINER_RUNTIME"
|
||||
|
||||
# 3. Check credentials
|
||||
CREDENTIALS="missing"
|
||||
if [ -f "$PROJECT_ROOT/.env" ]; then
|
||||
if grep -qE "^(CLAUDE_CODE_OAUTH_TOKEN|ANTHROPIC_API_KEY)=" "$PROJECT_ROOT/.env" 2>/dev/null; then
|
||||
CREDENTIALS="configured"
|
||||
fi
|
||||
fi
|
||||
log "Credentials: $CREDENTIALS"
|
||||
|
||||
# 4. Check WhatsApp auth
|
||||
WHATSAPP_AUTH="not_found"
|
||||
if [ -d "$PROJECT_ROOT/store/auth" ] && [ "$(ls -A "$PROJECT_ROOT/store/auth" 2>/dev/null)" ]; then
|
||||
WHATSAPP_AUTH="authenticated"
|
||||
fi
|
||||
log "WhatsApp auth: $WHATSAPP_AUTH"
|
||||
|
||||
# 5. Check registered groups (in SQLite — the JSON file gets migrated away on startup)
|
||||
REGISTERED_GROUPS=0
|
||||
if [ -f "$PROJECT_ROOT/store/messages.db" ]; then
|
||||
REGISTERED_GROUPS=$(sqlite3 "$PROJECT_ROOT/store/messages.db" "SELECT COUNT(*) FROM registered_groups" 2>/dev/null || echo "0")
|
||||
fi
|
||||
log "Registered groups: $REGISTERED_GROUPS"
|
||||
|
||||
# 6. Check mount allowlist
|
||||
MOUNT_ALLOWLIST="missing"
|
||||
if [ -f "$HOME/.config/nanoclaw/mount-allowlist.json" ]; then
|
||||
MOUNT_ALLOWLIST="configured"
|
||||
fi
|
||||
log "Mount allowlist: $MOUNT_ALLOWLIST"
|
||||
|
||||
# Determine overall status
|
||||
STATUS="success"
|
||||
if [ "$SERVICE" != "running" ] || [ "$CREDENTIALS" = "missing" ] || [ "$WHATSAPP_AUTH" = "not_found" ] || [ "$REGISTERED_GROUPS" -eq 0 ] 2>/dev/null; then
|
||||
STATUS="failed"
|
||||
fi
|
||||
|
||||
log "Verification complete: $STATUS"
|
||||
|
||||
cat <<EOF
|
||||
=== NANOCLAW SETUP: VERIFY ===
|
||||
SERVICE: $SERVICE
|
||||
CONTAINER_RUNTIME: $CONTAINER_RUNTIME
|
||||
CREDENTIALS: $CREDENTIALS
|
||||
WHATSAPP_AUTH: $WHATSAPP_AUTH
|
||||
REGISTERED_GROUPS: $REGISTERED_GROUPS
|
||||
MOUNT_ALLOWLIST: $MOUNT_ALLOWLIST
|
||||
STATUS: $STATUS
|
||||
LOG: logs/setup.log
|
||||
=== END ===
|
||||
EOF
|
||||
|
||||
if [ "$STATUS" = "failed" ]; then
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,35 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html><head><title>NanoClaw - WhatsApp Auth</title>
|
||||
<meta http-equiv="refresh" content="3">
|
||||
<style>
|
||||
body { font-family: -apple-system, sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #f5f5f5; }
|
||||
.card { background: white; border-radius: 16px; padding: 40px; box-shadow: 0 4px 24px rgba(0,0,0,0.1); text-align: center; max-width: 400px; }
|
||||
h2 { margin: 0 0 8px; }
|
||||
.timer { font-size: 18px; color: #666; margin: 12px 0; }
|
||||
.timer.urgent { color: #e74c3c; font-weight: bold; }
|
||||
.instructions { color: #666; font-size: 14px; margin-top: 16px; }
|
||||
svg { width: 280px; height: 280px; }
|
||||
</style></head><body>
|
||||
<div class="card">
|
||||
<h2>Scan with WhatsApp</h2>
|
||||
<div class="timer" id="timer">Expires in <span id="countdown">60</span>s</div>
|
||||
<div id="qr">{{QR_SVG}}</div>
|
||||
<div class="instructions">Settings → Linked Devices → Link a Device</div>
|
||||
</div>
|
||||
<script>
|
||||
// Persist start time across auto-refreshes
|
||||
var startKey = 'nanoclaw_qr_start';
|
||||
var start = localStorage.getItem(startKey);
|
||||
if (!start) { start = Date.now().toString(); localStorage.setItem(startKey, start); }
|
||||
var elapsed = Math.floor((Date.now() - parseInt(start)) / 1000);
|
||||
var remaining = Math.max(0, 60 - elapsed);
|
||||
var countdown = document.getElementById('countdown');
|
||||
var timer = document.getElementById('timer');
|
||||
countdown.textContent = remaining;
|
||||
if (remaining <= 10) timer.classList.add('urgent');
|
||||
if (remaining <= 0) {
|
||||
timer.textContent = 'QR code expired — a new one will appear shortly';
|
||||
timer.classList.add('urgent');
|
||||
localStorage.removeItem(startKey);
|
||||
}
|
||||
</script></body></html>
|
||||
@@ -49,8 +49,9 @@ npx dotenv -e .env -- npx tsx .claude/skills/x-integration/scripts/setup.ts
|
||||
|
||||
# 3. Rebuild host and restart service
|
||||
npm run build
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
|
||||
# Verify: launchctl list | grep nanoclaw shows PID and exit code 0
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# Linux: systemctl --user restart nanoclaw
|
||||
# Verify: launchctl list | grep nanoclaw (macOS) or systemctl --user status nanoclaw (Linux)
|
||||
```
|
||||
|
||||
## Configuration
|
||||
@@ -271,12 +272,14 @@ cat data/x-auth.json # Should show {"authenticated": true, ...}
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# Linux: systemctl --user restart nanoclaw
|
||||
```
|
||||
|
||||
**Verify success:**
|
||||
```bash
|
||||
launchctl list | grep nanoclaw # Should show PID and exit code 0 or -
|
||||
launchctl list | grep nanoclaw # macOS — should show PID and exit code 0 or -
|
||||
# Linux: systemctl --user status nanoclaw
|
||||
```
|
||||
|
||||
## Usage via WhatsApp
|
||||
@@ -342,7 +345,8 @@ echo '{"content":"Test"}' | npx tsx .claude/skills/x-integration/scripts/post.ts
|
||||
|
||||
```bash
|
||||
npx dotenv -e .env -- npx tsx .claude/skills/x-integration/scripts/setup.ts
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
|
||||
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
|
||||
# Linux: systemctl --user restart nanoclaw
|
||||
```
|
||||
|
||||
### Browser Lock Files
|
||||
|
||||
Reference in New Issue
Block a user