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:
108
.claude/skills/setup/scripts/01-check-environment.sh
Executable file
108
.claude/skills/setup/scripts/01-check-environment.sh
Executable 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
|
||||
62
.claude/skills/setup/scripts/02-install-deps.sh
Executable file
62
.claude/skills/setup/scripts/02-install-deps.sh
Executable 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
|
||||
150
.claude/skills/setup/scripts/03-setup-container.sh
Executable file
150
.claude/skills/setup/scripts/03-setup-container.sh
Executable 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
|
||||
347
.claude/skills/setup/scripts/04-auth-whatsapp.sh
Executable file
347
.claude/skills/setup/scripts/04-auth-whatsapp.sh
Executable 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">✓</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
|
||||
141
.claude/skills/setup/scripts/05-sync-groups.sh
Executable file
141
.claude/skills/setup/scripts/05-sync-groups.sh
Executable 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
|
||||
18
.claude/skills/setup/scripts/05b-list-groups.sh
Executable file
18
.claude/skills/setup/scripts/05b-list-groups.sh
Executable 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"
|
||||
97
.claude/skills/setup/scripts/06-register-channel.sh
Executable file
97
.claude/skills/setup/scripts/06-register-channel.sh
Executable 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
|
||||
80
.claude/skills/setup/scripts/07-configure-mounts.sh
Executable file
80
.claude/skills/setup/scripts/07-configure-mounts.sh
Executable 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
|
||||
197
.claude/skills/setup/scripts/08-setup-service.sh
Executable file
197
.claude/skills/setup/scripts/08-setup-service.sh
Executable 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
|
||||
109
.claude/skills/setup/scripts/09-verify.sh
Executable file
109
.claude/skills/setup/scripts/09-verify.sh
Executable 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
|
||||
35
.claude/skills/setup/scripts/qr-auth.html
Normal file
35
.claude/skills/setup/scripts/qr-auth.html
Normal 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>
|
||||
Reference in New Issue
Block a user