#!/usr/bin/env bash # ═══════════════════════════════════════════════════════════════════════════════ # Echo Core — Interactive Setup Wizard # Re-run safe: detects existing venv, config, secrets. Backs up before writing. # ═══════════════════════════════════════════════════════════════════════════════ set -uo pipefail # --------------------------------------------------------------------------- # Globals # --------------------------------------------------------------------------- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CONFIG_FILE="$SCRIPT_DIR/config.json" META_FILE="$SCRIPT_DIR/.setup-meta.json" VENV_DIR="$SCRIPT_DIR/.venv" REQUIREMENTS="$SCRIPT_DIR/requirements.txt" WA_BRIDGE_DIR="$SCRIPT_DIR/bridge/whatsapp" SYSTEMD_DIR="$HOME/.config/systemd/user" KEYRING_SERVICE="echo-core" # State collected during wizard BOT_NAME="" OWNER_DISCORD_ID="" ADMIN_IDS="" DISCORD_ENABLED=false DISCORD_TOKEN="" DISCORD_CHANNEL_ID="" DISCORD_CHANNEL_ALIAS="" TELEGRAM_ENABLED=false TELEGRAM_TOKEN="" TELEGRAM_ADMIN_IDS="" WHATSAPP_ENABLED=false WHATSAPP_OWNER_PHONE="" NODE_AVAILABLE=false SERVICES_INSTALLED=false START_NOW=false CURRENT_STEP=0 # Prereq results HAS_PYTHON=false HAS_PIP=false HAS_NODE=false HAS_CLAUDE=false HAS_CURL=false HAS_SYSTEMCTL=false PYTHON_BIN="" # --------------------------------------------------------------------------- # Colors & UI # --------------------------------------------------------------------------- if [[ -t 1 ]]; then C_RESET='\033[0m' C_BOLD='\033[1m' C_DIM='\033[2m' C_ORANGE='\033[38;5;208m' C_GREEN='\033[38;5;82m' C_RED='\033[38;5;196m' C_YELLOW='\033[38;5;220m' C_CYAN='\033[38;5;45m' C_GRAY='\033[38;5;245m' else C_RESET='' C_BOLD='' C_DIM='' C_ORANGE='' C_GREEN='' C_RED='' C_YELLOW='' C_CYAN='' C_GRAY='' fi info() { echo -e "${C_GRAY} $*${C_RESET}"; } success() { echo -e "${C_GREEN} ✓ $*${C_RESET}"; } warn() { echo -e "${C_YELLOW} ⚠ $*${C_RESET}"; } error() { echo -e "${C_RED} ✗ $*${C_RESET}"; } accent() { echo -e "${C_ORANGE}$*${C_RESET}"; } dim() { echo -e "${C_DIM}$*${C_RESET}"; } # Box header for each step step_header() { local num="$1" title="$2" echo "" echo -e "${C_ORANGE}┌──────────────────────────────────────────────────────────┐${C_RESET}" echo -e "${C_ORANGE}│${C_RESET} ${C_BOLD}Step $num: $title${C_RESET}" echo -e "${C_ORANGE}└──────────────────────────────────────────────────────────┘${C_RESET}" } # Braille spinner spinner() { local pid=$1 msg="$2" local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏') local i=0 while kill -0 "$pid" 2>/dev/null; do printf "\r ${C_CYAN}%s${C_RESET} %s" "${frames[$i]}" "$msg" i=$(( (i + 1) % ${#frames[@]} )) sleep 0.1 done printf "\r\033[K" } # Prompt with default — returns answer in $REPLY ask() { local prompt="$1" default="${2:-}" if [[ -n "$default" ]]; then printf " ${C_ORANGE}▸${C_RESET} %s [${C_DIM}%s${C_RESET}]: " "$prompt" "$default" else printf " ${C_ORANGE}▸${C_RESET} %s: " "$prompt" fi read -r REPLY [[ -z "$REPLY" ]] && REPLY="$default" } # Yes/no prompt — sets REPLY to y or n ask_yn() { local prompt="$1" default="${2:-y}" local hint="Y/n" [[ "$default" == "n" ]] && hint="y/N" printf " ${C_ORANGE}▸${C_RESET} %s [%s]: " "$prompt" "$hint" read -r REPLY REPLY="${REPLY:-$default}" REPLY="${REPLY,,}" # lowercase [[ "$REPLY" == y* ]] && REPLY="y" || REPLY="n" } # Secret input (masked) ask_secret() { local prompt="$1" printf " ${C_ORANGE}▸${C_RESET} %s: " "$prompt" read -rs REPLY echo "" } # --------------------------------------------------------------------------- # Ctrl+C handler # --------------------------------------------------------------------------- cleanup() { echo "" echo -e "${C_YELLOW}Setup interrupted at step $CURRENT_STEP. Re-run anytime to continue.${C_RESET}" exit 130 } trap cleanup INT # Step failure handler — offers to continue step_failed() { local step_name="$1" error "$step_name failed" ask_yn "Continue with remaining steps?" "y" [[ "$REPLY" == "n" ]] && exit 1 } # --------------------------------------------------------------------------- # Step 0: Welcome # --------------------------------------------------------------------------- step_welcome() { CURRENT_STEP=0 echo "" echo -e "${C_ORANGE}${C_BOLD}" cat << 'LOGO' ███████╗ ██████╗██╗ ██╗ ██████╗ ██╔════╝██╔════╝██║ ██║██╔═══██╗ █████╗ ██║ ███████║██║ ██║ ██╔══╝ ██║ ██╔══██║██║ ██║ ███████╗╚██████╗██║ ██║╚██████╔╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝ ╚═════╝ LOGO echo -e "${C_RESET}" echo -e " ${C_BOLD}Echo Core — Interactive Setup Wizard${C_RESET}" # Show version from HANDOFF.md or git if command -v git &>/dev/null && git -C "$SCRIPT_DIR" rev-parse --short HEAD &>/dev/null 2>&1; then local ver ver=$(git -C "$SCRIPT_DIR" rev-parse --short HEAD 2>/dev/null) dim " Version: git@$ver" fi echo "" # Detect previous setup if [[ -f "$META_FILE" ]]; then local prev_date prev_date=$(python3 -c "import json; print(json.load(open('$META_FILE')).get('date','unknown'))" 2>/dev/null || echo "unknown") warn "Previous setup detected (from $prev_date)" info "Existing config and secrets will be preserved unless you choose to replace them." echo "" fi if [[ -f "$CONFIG_FILE" ]]; then info "Existing config.json found — will merge, not overwrite." fi echo -e " ${C_DIM}This wizard will guide you through configuring Echo Core.${C_RESET}" echo -e " ${C_DIM}Press Ctrl+C at any time to abort.${C_RESET}" echo "" ask_yn "Ready to begin?" "y" [[ "$REPLY" == "n" ]] && { echo " Aborted."; exit 0; } } # --------------------------------------------------------------------------- # Step 1: Prerequisites # --------------------------------------------------------------------------- step_prerequisites() { CURRENT_STEP=1 step_header 1 "Prerequisites Check" local fail=false # Python 3.12+ if command -v python3 &>/dev/null; then PYTHON_BIN="$(command -v python3)" local pyver pyver=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null) local major minor major=$(echo "$pyver" | cut -d. -f1) minor=$(echo "$pyver" | cut -d. -f2) if [[ "$major" -ge 3 && "$minor" -ge 12 ]]; then success "Python $pyver ($PYTHON_BIN)" HAS_PYTHON=true else error "Python $pyver found — need 3.12+" fail=true fi else error "Python 3 not found" fail=true fi # pip if python3 -m pip --version &>/dev/null 2>&1; then success "pip $(python3 -m pip --version 2>/dev/null | awk '{print $2}')" HAS_PIP=true else error "pip not found (python3 -m pip)" fail=true fi # Node.js 22+ (warn only) if command -v node &>/dev/null; then local nodever nodever=$(node --version 2>/dev/null | sed 's/^v//') local nodemajor nodemajor=$(echo "$nodever" | cut -d. -f1) if [[ "$nodemajor" -ge 22 ]]; then success "Node.js v$nodever" HAS_NODE=true NODE_AVAILABLE=true else warn "Node.js v$nodever — need 22+ for WhatsApp bridge" NODE_AVAILABLE=false fi else warn "Node.js not found (needed only for WhatsApp bridge)" NODE_AVAILABLE=false fi # Claude CLI if command -v claude &>/dev/null; then success "Claude CLI found" HAS_CLAUDE=true else error "Claude CLI not found — required for bot operation" fail=true fi # curl (warn) if command -v curl &>/dev/null; then success "curl" HAS_CURL=true else warn "curl not found — token validation will be skipped" fi # systemctl (warn) if command -v systemctl &>/dev/null; then success "systemctl" HAS_SYSTEMCTL=true else warn "systemctl not found — service installation will be skipped" fi if $fail; then error "Required prerequisites missing. Install them and re-run." exit 1 fi echo "" success "All required prerequisites OK" } # --------------------------------------------------------------------------- # Step 2: Python Virtual Environment # --------------------------------------------------------------------------- step_venv() { CURRENT_STEP=2 step_header 2 "Python Virtual Environment" if [[ -d "$VENV_DIR" && -f "$VENV_DIR/bin/python3" ]]; then success "Virtual environment exists at .venv/" ask_yn "Reinstall dependencies from requirements.txt?" "n" if [[ "$REPLY" == "n" ]]; then info "Skipping dependency installation." return 0 fi else info "Creating virtual environment..." if ! python3 -m venv "$VENV_DIR" 2>/dev/null; then error "Failed to create venv" step_failed "Venv creation" return 1 fi success "Created .venv/" fi if [[ ! -f "$REQUIREMENTS" ]]; then warn "requirements.txt not found — skipping pip install" return 0 fi info "Installing dependencies..." "$VENV_DIR/bin/pip" install --upgrade pip -q &>/dev/null & spinner $! "Upgrading pip" success "pip upgraded" "$VENV_DIR/bin/pip" install -r "$REQUIREMENTS" -q &>/dev/null & spinner $! "Installing requirements" if "$VENV_DIR/bin/pip" install -r "$REQUIREMENTS" -q &>/dev/null; then success "Dependencies installed" else error "pip install failed" step_failed "Dependency installation" fi } # --------------------------------------------------------------------------- # Step 3: Bot Identity # --------------------------------------------------------------------------- step_identity() { CURRENT_STEP=3 step_header 3 "Bot Identity" # Read existing values from config local existing_name="" existing_owner="" existing_admins="" if [[ -f "$CONFIG_FILE" ]]; then existing_name=$("$VENV_DIR/bin/python3" -c " import json c = json.load(open('$CONFIG_FILE')) print(c.get('bot',{}).get('name',''))" 2>/dev/null || echo "") existing_owner=$("$VENV_DIR/bin/python3" -c " import json c = json.load(open('$CONFIG_FILE')) print(c.get('bot',{}).get('owner',''))" 2>/dev/null || echo "") existing_admins=$("$VENV_DIR/bin/python3" -c " import json c = json.load(open('$CONFIG_FILE')) a = c.get('bot',{}).get('admins',[]) print(','.join(a))" 2>/dev/null || echo "") fi ask "Bot name" "${existing_name:-Echo}" BOT_NAME="$REPLY" ask "Owner Discord user ID" "$existing_owner" OWNER_DISCORD_ID="$REPLY" if [[ -n "$existing_admins" ]]; then info "Current admin IDs: $existing_admins" fi ask "Additional admin IDs (comma-separated, or blank)" "$existing_admins" ADMIN_IDS="$REPLY" echo "" success "Identity: ${C_BOLD}$BOT_NAME${C_RESET}${C_GREEN} (owner: $OWNER_DISCORD_ID)" } # --------------------------------------------------------------------------- # Step 4: Discord Setup # --------------------------------------------------------------------------- validate_discord_token() { local token="$1" if ! $HAS_CURL; then warn "curl not available — skipping token validation" return 0 fi local response response=$(curl -sf -H "Authorization: Bot $token" \ "https://discord.com/api/v10/users/@me" 2>/dev/null) if [[ $? -eq 0 && -n "$response" ]]; then local botname botname=$(echo "$response" | "$VENV_DIR/bin/python3" -c "import json,sys; print(json.load(sys.stdin).get('username','?'))" 2>/dev/null || echo "?") success "Discord token valid — bot: $botname" return 0 fi return 1 } step_discord() { CURRENT_STEP=4 step_header 4 "Discord Bridge" ask_yn "Configure Discord bot?" "y" if [[ "$REPLY" == "n" ]]; then info "Skipping Discord setup." return 0 fi DISCORD_ENABLED=true # Check existing token in keyring local existing_token existing_token=$("$VENV_DIR/bin/python3" -c " import sys; sys.path.insert(0, '$SCRIPT_DIR') from src.credential_store import get_secret t = get_secret('discord_token') print('yes' if t else 'no')" 2>/dev/null || echo "no") if [[ "$existing_token" == "yes" ]]; then success "Discord token already stored in keyring" ask_yn "Replace existing token?" "n" if [[ "$REPLY" == "n" ]]; then info "Keeping existing token." # Ask for optional channel _ask_discord_channel return 0 fi fi echo "" dim " To get a Discord bot token:" dim " 1. Go to https://discord.com/developers/applications" dim " 2. Create application → Bot → Reset Token → Copy" dim " 3. Enable MESSAGE CONTENT intent under Bot → Privileged Intents" dim " 4. Invite with: OAuth2 → URL Generator → bot + applications.commands" echo "" local attempts=0 while [[ $attempts -lt 3 ]]; do ask_secret "Discord bot token" DISCORD_TOKEN="$REPLY" if [[ -z "$DISCORD_TOKEN" ]]; then warn "Empty token — skipping Discord" DISCORD_ENABLED=false return 0 fi if validate_discord_token "$DISCORD_TOKEN"; then # Store via Python stdin (not CLI argument) echo "$DISCORD_TOKEN" | "$VENV_DIR/bin/python3" -c " import sys; sys.path.insert(0, '$SCRIPT_DIR') from src.credential_store import set_secret set_secret('discord_token', sys.stdin.readline().strip())" success "Token stored in keyring" break else error "Token validation failed" attempts=$((attempts + 1)) if [[ $attempts -ge 3 ]]; then warn "3 failed attempts — skipping Discord token" DISCORD_ENABLED=false return 0 fi fi done _ask_discord_channel } _ask_discord_channel() { echo "" ask "Default Discord channel ID (blank to skip)" "" DISCORD_CHANNEL_ID="$REPLY" if [[ -n "$DISCORD_CHANNEL_ID" ]]; then ask "Channel alias" "general" DISCORD_CHANNEL_ALIAS="$REPLY" fi } # --------------------------------------------------------------------------- # Step 5: Telegram Setup # --------------------------------------------------------------------------- validate_telegram_token() { local token="$1" if ! $HAS_CURL; then warn "curl not available — skipping token validation" return 0 fi local response response=$(curl -sf "https://api.telegram.org/bot${token}/getMe" 2>/dev/null) if [[ $? -eq 0 ]] && echo "$response" | "$VENV_DIR/bin/python3" -c "import json,sys; d=json.load(sys.stdin); exit(0 if d.get('ok') else 1)" 2>/dev/null; then local botname botname=$(echo "$response" | "$VENV_DIR/bin/python3" -c "import json,sys; print(json.load(sys.stdin)['result']['username'])" 2>/dev/null || echo "?") success "Telegram token valid — bot: @$botname" return 0 fi return 1 } step_telegram() { CURRENT_STEP=5 step_header 5 "Telegram Bridge" ask_yn "Configure Telegram bot?" "n" if [[ "$REPLY" == "n" ]]; then info "Skipping Telegram setup." return 0 fi TELEGRAM_ENABLED=true # Check existing token local existing_token existing_token=$("$VENV_DIR/bin/python3" -c " import sys; sys.path.insert(0, '$SCRIPT_DIR') from src.credential_store import get_secret t = get_secret('telegram_token') print('yes' if t else 'no')" 2>/dev/null || echo "no") if [[ "$existing_token" == "yes" ]]; then success "Telegram token already stored in keyring" ask_yn "Replace existing token?" "n" if [[ "$REPLY" == "n" ]]; then info "Keeping existing token." _ask_telegram_admins return 0 fi fi echo "" dim " To get a Telegram bot token:" dim " 1. Open Telegram, search for @BotFather" dim " 2. Send /newbot and follow the prompts" dim " 3. Copy the HTTP API token" echo "" local attempts=0 while [[ $attempts -lt 3 ]]; do ask_secret "Telegram bot token" local tg_token="$REPLY" if [[ -z "$tg_token" ]]; then warn "Empty token — skipping Telegram" TELEGRAM_ENABLED=false return 0 fi if validate_telegram_token "$tg_token"; then echo "$tg_token" | "$VENV_DIR/bin/python3" -c " import sys; sys.path.insert(0, '$SCRIPT_DIR') from src.credential_store import set_secret set_secret('telegram_token', sys.stdin.readline().strip())" success "Token stored in keyring" TELEGRAM_TOKEN="$tg_token" break else error "Token validation failed" attempts=$((attempts + 1)) if [[ $attempts -ge 3 ]]; then warn "3 failed attempts — skipping Telegram token" TELEGRAM_ENABLED=false return 0 fi fi done _ask_telegram_admins } _ask_telegram_admins() { ask "Telegram admin user IDs (comma-separated, blank for none)" "" TELEGRAM_ADMIN_IDS="$REPLY" } # --------------------------------------------------------------------------- # Step 6: WhatsApp Setup # --------------------------------------------------------------------------- step_whatsapp() { CURRENT_STEP=6 step_header 6 "WhatsApp Bridge" if ! $NODE_AVAILABLE; then info "Node.js 22+ not available — skipping WhatsApp setup." return 0 fi ask_yn "Configure WhatsApp bridge?" "n" if [[ "$REPLY" == "n" ]]; then info "Skipping WhatsApp setup." return 0 fi WHATSAPP_ENABLED=true # npm install if [[ -d "$WA_BRIDGE_DIR/node_modules" ]]; then success "node_modules/ already exists" ask_yn "Re-run npm install?" "n" if [[ "$REPLY" == "y" ]]; then info "Running npm install..." (cd "$WA_BRIDGE_DIR" && npm install --no-fund --no-audit 2>&1) &>/dev/null & spinner $! "Installing npm packages" if (cd "$WA_BRIDGE_DIR" && npm install --no-fund --no-audit &>/dev/null); then success "npm packages installed" else warn "npm install had issues — bridge may still work" fi fi else info "Running npm install in bridge/whatsapp/..." (cd "$WA_BRIDGE_DIR" && npm install --no-fund --no-audit 2>&1) &>/dev/null & spinner $! "Installing npm packages" if (cd "$WA_BRIDGE_DIR" && npm install --no-fund --no-audit &>/dev/null); then success "npm packages installed" else error "npm install failed" step_failed "WhatsApp npm install" return 1 fi fi # Owner phone local existing_phone="" if [[ -f "$CONFIG_FILE" ]]; then existing_phone=$("$VENV_DIR/bin/python3" -c " import json c = json.load(open('$CONFIG_FILE')) print(c.get('whatsapp',{}).get('owner',''))" 2>/dev/null || echo "") fi ask "Owner phone number (international, no +)" "$existing_phone" WHATSAPP_OWNER_PHONE="$REPLY" echo "" dim " WhatsApp QR pairing will happen when you start the bridge:" dim " systemctl --user start echo-whatsapp-bridge" dim " journalctl --user -u echo-whatsapp-bridge -f" dim " Or open http://127.0.0.1:8098/qr in a browser." echo "" success "WhatsApp bridge prepared" } # --------------------------------------------------------------------------- # Step 7: Configuration (merge into config.json) # --------------------------------------------------------------------------- step_config() { CURRENT_STEP=7 step_header 7 "Configuration" # Backup existing config if [[ -f "$CONFIG_FILE" ]]; then local backup="$CONFIG_FILE.bak.$(date +%Y%m%d%H%M%S)" cp "$CONFIG_FILE" "$backup" success "Backup: $(basename "$backup")" fi # Build patch JSON and merge via Python "$VENV_DIR/bin/python3" << 'PYEOF' - \ "$CONFIG_FILE" \ "$BOT_NAME" \ "$OWNER_DISCORD_ID" \ "$ADMIN_IDS" \ "$DISCORD_ENABLED" \ "$DISCORD_CHANNEL_ID" \ "$DISCORD_CHANNEL_ALIAS" \ "$TELEGRAM_ENABLED" \ "$TELEGRAM_ADMIN_IDS" \ "$WHATSAPP_ENABLED" \ "$WHATSAPP_OWNER_PHONE" import json, sys, os from pathlib import Path args = sys.argv[1:] config_path = args[0] bot_name = args[1] owner_discord = args[2] admin_ids_raw = args[3] discord_on = args[4] == "true" discord_ch_id = args[5] discord_ch_alias= args[6] telegram_on = args[7] == "true" tg_admin_raw = args[8] whatsapp_on = args[9] == "true" wa_phone = args[10] # Load existing or create empty if Path(config_path).exists(): with open(config_path) as f: config = json.load(f) else: config = {} # --- bot section --- bot = config.setdefault("bot", {}) if bot_name: bot["name"] = bot_name bot.setdefault("default_model", "opus") if owner_discord: bot["owner"] = owner_discord if admin_ids_raw: bot["admins"] = [a.strip() for a in admin_ids_raw.split(",") if a.strip()] # --- channels (preserve existing) --- config.setdefault("channels", {}) if discord_on and discord_ch_id and discord_ch_alias: config["channels"][discord_ch_alias] = { "id": discord_ch_id, "default_model": "opus" } # --- telegram_channels (preserve existing) --- config.setdefault("telegram_channels", {}) # --- whatsapp --- wa = config.setdefault("whatsapp", {}) wa["enabled"] = whatsapp_on wa.setdefault("bridge_url", "http://127.0.0.1:8098") if wa_phone: wa["owner"] = wa_phone wa.setdefault("admins", []) config.setdefault("whatsapp_channels", {}) # --- heartbeat --- hb = config.setdefault("heartbeat", {}) hb.setdefault("enabled", True) hb.setdefault("interval_minutes", 30) # --- ollama --- ol = config.setdefault("ollama", {}) ol.setdefault("url", "http://localhost:11434") # --- paths --- paths = config.setdefault("paths", {}) paths.setdefault("personality", "personality/") paths.setdefault("tools", "tools/") paths.setdefault("memory", "memory/") paths.setdefault("logs", "logs/") paths.setdefault("sessions", "sessions/") # Write with open(config_path, "w") as f: json.dump(config, f, indent=2, ensure_ascii=False) f.write("\n") print("OK") PYEOF if [[ $? -eq 0 ]]; then success "config.json written" else error "Failed to write config.json" step_failed "Config write" fi } # --------------------------------------------------------------------------- # Step 8: Systemd Services # --------------------------------------------------------------------------- step_systemd() { CURRENT_STEP=8 step_header 8 "Systemd Services" if ! $HAS_SYSTEMCTL; then info "systemctl not available — skipping service installation." return 0 fi mkdir -p "$SYSTEMD_DIR" # --- echo-core.service --- local svc_core="$SYSTEMD_DIR/echo-core.service" local core_changed=false local core_content core_content=$(cat << EOF [Unit] Description=Echo Core Bot (Discord + Telegram + WhatsApp) After=network-online.target Wants=network-online.target [Service] Type=simple WorkingDirectory=$SCRIPT_DIR ExecStart=$VENV_DIR/bin/python3 src/main.py Restart=always RestartSec=5 KillMode=mixed TimeoutStopSec=10 Environment=HOME=$HOME Environment="PATH=$HOME/.local/bin:$VENV_DIR/bin:/usr/local/bin:/usr/bin:/bin" [Install] WantedBy=default.target EOF ) if [[ -f "$svc_core" ]]; then success "echo-core.service exists" ask_yn "Regenerate echo-core.service?" "n" [[ "$REPLY" == "y" ]] && core_changed=true else core_changed=true fi if $core_changed; then echo "$core_content" > "$svc_core" success "Wrote echo-core.service" fi # --- echo-whatsapp-bridge.service --- local wa_changed=false if $WHATSAPP_ENABLED; then local svc_wa="$SYSTEMD_DIR/echo-whatsapp-bridge.service" local wa_content wa_content=$(cat << EOF [Unit] Description=Echo Core WhatsApp Bridge (Baileys) After=network-online.target Wants=network-online.target Before=echo-core.service [Service] Type=simple WorkingDirectory=$WA_BRIDGE_DIR ExecStart=/usr/bin/node index.js Restart=always RestartSec=5 KillMode=process Environment=HOME=$HOME Environment="PATH=/usr/local/bin:/usr/bin:/bin" [Install] WantedBy=default.target EOF ) if [[ -f "$svc_wa" ]]; then success "echo-whatsapp-bridge.service exists" ask_yn "Regenerate echo-whatsapp-bridge.service?" "n" [[ "$REPLY" == "y" ]] && wa_changed=true else wa_changed=true fi if $wa_changed; then echo "$wa_content" > "$svc_wa" success "Wrote echo-whatsapp-bridge.service" fi fi # daemon-reload + enable if $core_changed || $wa_changed; then systemctl --user daemon-reload success "daemon-reload done" fi systemctl --user enable echo-core.service &>/dev/null success "echo-core.service enabled" if $WHATSAPP_ENABLED; then systemctl --user enable echo-whatsapp-bridge.service &>/dev/null success "echo-whatsapp-bridge.service enabled" fi SERVICES_INSTALLED=true # Install `eco` CLI command local cli_target="$HOME/.local/bin/eco" mkdir -p "$HOME/.local/bin" if [[ -L "$cli_target" || -f "$cli_target" ]]; then success "eco CLI already installed at ~/.local/bin/eco" else ln -s "$SCRIPT_DIR/cli.py" "$cli_target" success "Installed eco CLI → ~/.local/bin/eco" fi # Start now? echo "" ask_yn "Start services now?" "n" if [[ "$REPLY" == "y" ]]; then START_NOW=true if $WHATSAPP_ENABLED; then systemctl --user start echo-whatsapp-bridge.service info "Starting WhatsApp bridge..." sleep 2 fi systemctl --user start echo-core.service info "Starting Echo Core..." sleep 2 local status status=$(systemctl --user is-active echo-core.service 2>/dev/null) if [[ "$status" == "active" ]]; then success "echo-core.service is running" else warn "echo-core.service status: $status" fi if $WHATSAPP_ENABLED; then status=$(systemctl --user is-active echo-whatsapp-bridge.service 2>/dev/null) if [[ "$status" == "active" ]]; then success "echo-whatsapp-bridge.service is running" else warn "echo-whatsapp-bridge.service status: $status" fi fi fi } # --------------------------------------------------------------------------- # Step 9: Health Check # --------------------------------------------------------------------------- step_healthcheck() { CURRENT_STEP=9 step_header 9 "Health Check" local issues=0 # Config valid JSON if [[ -f "$CONFIG_FILE" ]] && "$VENV_DIR/bin/python3" -c "import json; json.load(open('$CONFIG_FILE'))" 2>/dev/null; then success "config.json is valid JSON" else error "config.json is missing or invalid" issues=$((issues + 1)) fi # Secrets in keyring local secrets_report secrets_report=$("$VENV_DIR/bin/python3" -c " import sys; sys.path.insert(0, '$SCRIPT_DIR') from src.credential_store import get_secret dt = 'yes' if get_secret('discord_token') else 'no' tt = 'yes' if get_secret('telegram_token') else 'no' print(f'{dt},{tt}')" 2>/dev/null || echo "no,no") local dt tt dt=$(echo "$secrets_report" | cut -d, -f1) tt=$(echo "$secrets_report" | cut -d, -f2) if $DISCORD_ENABLED; then if [[ "$dt" == "yes" ]]; then success "Discord token in keyring" else error "Discord token NOT in keyring" issues=$((issues + 1)) fi fi if $TELEGRAM_ENABLED; then if [[ "$tt" == "yes" ]]; then success "Telegram token in keyring" else error "Telegram token NOT in keyring" issues=$((issues + 1)) fi fi # Directories writable for d in logs sessions memory; do local full="$SCRIPT_DIR/$d" if [[ -d "$full" && -w "$full" ]]; then success "$d/ writable" elif [[ ! -d "$full" ]]; then mkdir -p "$full" 2>/dev/null if [[ -d "$full" ]]; then success "$d/ created" else error "Cannot create $d/" issues=$((issues + 1)) fi else error "$d/ not writable" issues=$((issues + 1)) fi done # Claude CLI if $HAS_CLAUDE; then success "Claude CLI available" else error "Claude CLI not found" issues=$((issues + 1)) fi # Service status (if installed) if $SERVICES_INSTALLED && $HAS_SYSTEMCTL; then local core_status core_status=$(systemctl --user is-active echo-core.service 2>/dev/null || echo "not-found") if [[ "$core_status" == "active" ]]; then success "echo-core.service: active" elif [[ "$core_status" == "inactive" ]]; then info "echo-core.service: inactive (not started)" else warn "echo-core.service: $core_status" fi fi echo "" if [[ $issues -eq 0 ]]; then success "All health checks passed" else warn "$issues issue(s) found" fi } # --------------------------------------------------------------------------- # Step 10: Summary # --------------------------------------------------------------------------- step_summary() { CURRENT_STEP=10 step_header 10 "Summary" local check="${C_GREEN}✓${C_RESET}" local cross="${C_RED}✗${C_RESET}" echo "" echo -e "${C_ORANGE}┌────────────────────────────────────────┐${C_RESET}" echo -e "${C_ORANGE}│${C_RESET} ${C_BOLD}Echo Core Setup Complete${C_RESET} ${C_ORANGE}│${C_RESET}" echo -e "${C_ORANGE}├────────────────────────────────────────┤${C_RESET}" printf "${C_ORANGE}│${C_RESET} Bot name: %-23s ${C_ORANGE}│${C_RESET}\n" "$BOT_NAME" printf "${C_ORANGE}│${C_RESET} Owner: %-23s ${C_ORANGE}│${C_RESET}\n" "$OWNER_DISCORD_ID" echo -e "${C_ORANGE}├────────────────────────────────────────┤${C_RESET}" echo -e "${C_ORANGE}│${C_RESET} Bridges: ${C_ORANGE}│${C_RESET}" if $DISCORD_ENABLED; then echo -e "${C_ORANGE}│${C_RESET} $check Discord ${C_ORANGE}│${C_RESET}" else echo -e "${C_ORANGE}│${C_RESET} $cross Discord (skipped) ${C_ORANGE}│${C_RESET}" fi if $TELEGRAM_ENABLED; then echo -e "${C_ORANGE}│${C_RESET} $check Telegram ${C_ORANGE}│${C_RESET}" else echo -e "${C_ORANGE}│${C_RESET} $cross Telegram (skipped) ${C_ORANGE}│${C_RESET}" fi if $WHATSAPP_ENABLED; then echo -e "${C_ORANGE}│${C_RESET} $check WhatsApp ${C_ORANGE}│${C_RESET}" else echo -e "${C_ORANGE}│${C_RESET} $cross WhatsApp (skipped) ${C_ORANGE}│${C_RESET}" fi echo -e "${C_ORANGE}├────────────────────────────────────────┤${C_RESET}" if $SERVICES_INSTALLED; then echo -e "${C_ORANGE}│${C_RESET} $check Systemd services installed ${C_ORANGE}│${C_RESET}" else echo -e "${C_ORANGE}│${C_RESET} $cross Systemd services (skipped) ${C_ORANGE}│${C_RESET}" fi echo -e "${C_ORANGE}└────────────────────────────────────────┘${C_RESET}" # Write .setup-meta.json "$VENV_DIR/bin/python3" << METAEOF import json from datetime import datetime meta = { "date": datetime.now().isoformat(), "bot_name": "$BOT_NAME", "discord": $($DISCORD_ENABLED && echo "True" || echo "False"), "telegram": $($TELEGRAM_ENABLED && echo "True" || echo "False"), "whatsapp": $($WHATSAPP_ENABLED && echo "True" || echo "False"), "services_installed": $($SERVICES_INSTALLED && echo "True" || echo "False") } with open("$META_FILE", "w") as f: json.dump(meta, f, indent=2) f.write("\n") METAEOF success "Wrote .setup-meta.json" # Next steps echo "" echo -e " ${C_BOLD}Next steps:${C_RESET}" echo "" if ! $START_NOW && $SERVICES_INSTALLED; then dim " Start the bot:" echo -e " ${C_CYAN}systemctl --user start echo-core${C_RESET}" echo "" fi if $WHATSAPP_ENABLED && ! $START_NOW; then dim " Start WhatsApp bridge + pair via QR:" echo -e " ${C_CYAN}systemctl --user start echo-whatsapp-bridge${C_RESET}" echo -e " ${C_CYAN}curl http://127.0.0.1:8098/status${C_RESET}" echo "" fi dim " View logs:" echo -e " ${C_CYAN}journalctl --user -u echo-core -f${C_RESET}" echo "" dim " Check status:" echo -e " ${C_CYAN}./cli.py status${C_RESET}" echo -e " ${C_CYAN}./cli.py doctor${C_RESET}" echo "" dim " Manage secrets:" echo -e " ${C_CYAN}./cli.py secrets list${C_RESET}" echo "" echo -e " ${C_GREEN}${C_BOLD}Setup complete! 🎉${C_RESET}" echo "" } # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- main() { step_welcome step_prerequisites step_venv step_identity step_discord step_telegram step_whatsapp step_config step_systemd step_healthcheck step_summary } main "$@"