feat: add setup skill with scripted steps (#258)

Replace inline SKILL.md instructions with executable shell scripts
for each setup phase (environment check, deps, container, auth,
groups, channels, mounts, service, verify). Scripts emit structured
status blocks for reliable parsing.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-02-16 00:23:49 +02:00
committed by GitHub
parent 5694ac9b87
commit 88140ec1bb
15 changed files with 1883 additions and 667 deletions

View File

@@ -0,0 +1,108 @@
#!/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

View File

@@ -0,0 +1,62 @@
#!/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

View File

@@ -0,0 +1,150 @@
#!/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

View File

@@ -0,0 +1,347 @@
#!/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">&#10003;</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

View File

@@ -0,0 +1,141 @@
#!/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

View File

@@ -0,0 +1,18 @@
#!/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"

View File

@@ -0,0 +1,97 @@
#!/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
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

View File

@@ -0,0 +1,80 @@
#!/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

View File

@@ -0,0 +1,197 @@
#!/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

View File

@@ -0,0 +1,109 @@
#!/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

View File

@@ -0,0 +1,35 @@
<!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>