#!/bin/bash # ============================================================================= # Agent Second Brain - VPS Setup Script # ============================================================================= # One-command setup for your personal AI assistant # Usage: curl -fsSL https://raw.githubusercontent.com/YOUR_USER/agent-second-brain/main/setup.sh | bash # Or: bash setup.sh # ============================================================================= set -e # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' PURPLE='\033[0;35m' CYAN='\033[0;36m' NC='\033[0m' # No Color BOLD='\033[1m' # Emojis (safe for most terminals) CHECK="[OK]" CROSS="[X]" WARN="[!]" ARROW="-->" GEAR="[*]" # ============================================================================= # Helper functions # ============================================================================= print_banner() { echo "" echo -e "${PURPLE}${BOLD}" echo " ╔═══════════════════════════════════════════════════════════╗" echo " ║ ║" echo " ║ AGENT SECOND BRAIN - VPS SETUP ║" echo " ║ ║" echo " ║ Personal AI Assistant with Long-term Memory ║" echo " ║ ║" echo " ╚═══════════════════════════════════════════════════════════╝" echo -e "${NC}" echo "" } info() { echo -e "${BLUE}${GEAR}${NC} $1" } success() { echo -e "${GREEN}${CHECK}${NC} $1" } warn() { echo -e "${YELLOW}${WARN}${NC} $1" } error() { echo -e "${RED}${CROSS}${NC} $1" } step() { echo "" echo -e "${CYAN}${BOLD}${ARROW} $1${NC}" echo -e "${CYAN}$(printf '%.0s─' {1..60})${NC}" } ask() { echo -e "${YELLOW}?${NC} $1" } # ============================================================================= # Validation functions # ============================================================================= validate_telegram_token() { local token="$1" # Telegram bot token format: 123456789:ABC-DEF1234ghIkl-zyx57W2v1u123ew11 if [[ $token =~ ^[0-9]+:[A-Za-z0-9_-]+$ ]]; then return 0 fi return 1 } validate_telegram_id() { local id="$1" # Telegram ID is a positive integer if [[ $id =~ ^[0-9]+$ ]] && [ "$id" -gt 0 ]; then return 0 fi return 1 } validate_deepgram_key() { local key="$1" # Deepgram key is alphanumeric, typically 40+ chars if [[ $key =~ ^[A-Za-z0-9]+$ ]] && [ ${#key} -ge 20 ]; then return 0 fi return 1 } validate_todoist_key() { local key="$1" # Todoist API token is alphanumeric, 40 chars if [[ $key =~ ^[A-Za-z0-9]+$ ]] && [ ${#key} -ge 30 ]; then return 0 fi return 1 } # ============================================================================= # Check functions # ============================================================================= check_root() { if [ "$EUID" -eq 0 ]; then error "Do not run this script as root!" echo " Create a regular user first: adduser myuser && usermod -aG sudo myuser" echo " Then run: su - myuser && bash setup.sh" exit 1 fi } check_os() { if [ -f /etc/os-release ]; then . /etc/os-release if [[ "$ID" != "ubuntu" && "$ID" != "debian" ]]; then warn "This script is tested on Ubuntu/Debian. You're running: $ID" read -p "Continue anyway? (y/N): " -r REPLY if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1 fi fi fi } check_command() { command -v "$1" &> /dev/null } # ============================================================================= # Installation functions # ============================================================================= install_system_deps() { step "Installing system dependencies" info "Updating package list..." sudo apt-get update -qq info "Installing git, curl, wget, build-essential..." sudo apt-get install -y -qq git curl wget build-essential software-properties-common success "System dependencies installed" } install_python() { step "Installing Python 3.12" if check_command python3.12; then success "Python 3.12 already installed" return fi info "Adding deadsnakes PPA..." sudo add-apt-repository -y ppa:deadsnakes/ppa sudo apt-get update -qq info "Installing Python 3.12..." sudo apt-get install -y -qq python3.12 python3.12-venv python3.12-dev success "Python 3.12 installed" } install_uv() { step "Installing uv (Python package manager)" if check_command uv; then success "uv already installed" return fi info "Downloading and installing uv..." curl -LsSf https://astral.sh/uv/install.sh | sh # Add to current session export PATH="$HOME/.local/bin:$PATH" # Add to .bashrc if not already there if ! grep -q 'export PATH="$HOME/.local/bin:$PATH"' ~/.bashrc 2>/dev/null; then echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc fi success "uv installed" } install_nodejs() { step "Installing Node.js 20" if check_command node; then NODE_VERSION=$(node --version | cut -d'v' -f2 | cut -d'.' -f1) if [ "$NODE_VERSION" -ge 18 ]; then success "Node.js $(node --version) already installed" return fi fi info "Adding NodeSource repository..." curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - info "Installing Node.js..." sudo apt-get install -y -qq nodejs success "Node.js $(node --version) installed" } install_claude_cli() { step "Installing Claude CLI" if check_command claude; then success "Claude CLI already installed: $(claude --version 2>/dev/null || echo 'version unknown')" return fi info "Installing @anthropic-ai/claude-code globally..." sudo npm install -g @anthropic-ai/claude-code success "Claude CLI installed" } # ============================================================================= # Configuration functions # ============================================================================= clone_repository() { step "Setting up project" PROJECTS_DIR="$HOME/projects" PROJECT_DIR="$PROJECTS_DIR/agent-second-brain" mkdir -p "$PROJECTS_DIR" if [ -d "$PROJECT_DIR" ]; then warn "Project directory already exists: $PROJECT_DIR" read -p "Remove and re-clone? (y/N): " -r REPLY if [[ $REPLY =~ ^[Yy]$ ]]; then rm -rf "$PROJECT_DIR" else cd "$PROJECT_DIR" success "Using existing directory" return fi fi ask "Enter your GitHub username (the one where you forked the repo):" read -r GITHUB_USER if [ -z "$GITHUB_USER" ]; then error "GitHub username cannot be empty" exit 1 fi REPO_URL="https://github.com/$GITHUB_USER/agent-second-brain.git" info "Cloning from $REPO_URL..." if ! git clone "$REPO_URL" "$PROJECT_DIR" 2>/dev/null; then error "Failed to clone repository!" echo " Make sure you've forked the repo and the username is correct" exit 1 fi cd "$PROJECT_DIR" success "Repository cloned to $PROJECT_DIR" # Save GitHub user for later echo "$GITHUB_USER" > .github_user } collect_tokens() { step "Collecting API tokens" echo "" echo "You'll need these tokens (get them from the services):" echo " - Telegram Bot Token (from @BotFather)" echo " - Your Telegram ID (from @userinfobot)" echo " - Deepgram API Key (from console.deepgram.com)" echo " - Todoist API Token (from Todoist Settings > Integrations > Developer)" echo "" # Telegram Bot Token while true; do ask "Telegram Bot Token (from @BotFather):" read -r TELEGRAM_BOT_TOKEN if validate_telegram_token "$TELEGRAM_BOT_TOKEN"; then success "Token format valid" break else error "Invalid token format. Should be like: 123456789:ABC-DEF1234ghIkl-zyx57W2v" fi done # Telegram User ID while true; do ask "Your Telegram User ID (from @userinfobot):" read -r TELEGRAM_USER_ID if validate_telegram_id "$TELEGRAM_USER_ID"; then success "User ID valid" break else error "Invalid User ID. Should be a number like: 123456789" fi done # Deepgram API Key while true; do ask "Deepgram API Key (from console.deepgram.com):" read -r DEEPGRAM_API_KEY if validate_deepgram_key "$DEEPGRAM_API_KEY"; then success "API Key format valid" break else error "Invalid API key format. Should be alphanumeric, 20+ characters" fi done # Todoist API Token while true; do ask "Todoist API Token (from Settings > Integrations > Developer):" read -r TODOIST_API_KEY if validate_todoist_key "$TODOIST_API_KEY"; then success "API Token format valid" break else error "Invalid API token format. Should be alphanumeric, 30+ characters" fi done } create_env_file() { step "Creating .env file" ENV_FILE="$PROJECT_DIR/.env" if [ -f "$ENV_FILE" ]; then warn ".env file already exists" read -p "Overwrite? (y/N): " -r REPLY if [[ ! $REPLY =~ ^[Yy]$ ]]; then success "Keeping existing .env" return fi fi cat > "$ENV_FILE" << EOF # Telegram Bot API token from @BotFather TELEGRAM_BOT_TOKEN=$TELEGRAM_BOT_TOKEN # Deepgram API key for voice transcription DEEPGRAM_API_KEY=$DEEPGRAM_API_KEY # Todoist API key for task management TODOIST_API_KEY=$TODOIST_API_KEY # Path to Obsidian vault directory VAULT_PATH=./vault # JSON array of Telegram user IDs allowed to use the bot ALLOWED_USER_IDS=[$TELEGRAM_USER_ID] EOF chmod 600 "$ENV_FILE" success ".env file created (permissions: 600)" } install_dependencies() { step "Installing Python dependencies" cd "$PROJECT_DIR" info "Running uv sync..." "$HOME/.local/bin/uv" sync success "Dependencies installed" } configure_systemd() { step "Configuring systemd service" SERVICE_FILE="/etc/systemd/system/d-brain-bot.service" SERVICE_NAME="d-brain-bot" # Generate service file content SERVICE_CONTENT="[Unit] Description=d-brain Telegram Bot After=network.target [Service] Type=simple User=$USER WorkingDirectory=$PROJECT_DIR ExecStart=$HOME/.local/bin/uv run python -m d_brain Restart=always RestartSec=10 Environment=PYTHONUNBUFFERED=1 [Install] WantedBy=multi-user.target" info "Creating systemd service..." echo "$SERVICE_CONTENT" | sudo tee "$SERVICE_FILE" > /dev/null info "Reloading systemd daemon..." sudo systemctl daemon-reload info "Enabling service to start on boot..." sudo systemctl enable "$SERVICE_NAME" info "Starting service..." sudo systemctl start "$SERVICE_NAME" # Wait a moment for service to start sleep 2 success "Systemd service configured" } configure_git_remote() { step "Configuring Git for push access" cd "$PROJECT_DIR" # Set git config git config user.name "Agent Second Brain Bot" git config user.email "bot@localhost" ask "Do you want to configure GitHub push access? (for auto-sync)" read -p "(y/N): " -r REPLY if [[ ! $REPLY =~ ^[Yy]$ ]]; then warn "Skipping GitHub push configuration" return fi echo "" echo "Create a Personal Access Token on GitHub:" echo " 1. Go to: github.com > Settings > Developer settings > Personal access tokens > Tokens (classic)" echo " 2. Generate new token with 'repo' scope" echo " 3. Copy the token" echo "" ask "Enter your GitHub Personal Access Token:" read -rs GITHUB_TOKEN echo "" if [ -z "$GITHUB_TOKEN" ]; then warn "No token provided, skipping" return fi GITHUB_USER=$(cat .github_user 2>/dev/null || echo "") if [ -z "$GITHUB_USER" ]; then ask "Enter your GitHub username:" read -r GITHUB_USER fi git remote set-url origin "https://$GITHUB_TOKEN@github.com/$GITHUB_USER/agent-second-brain.git" success "Git remote configured for push" } # ============================================================================= # Status and verification # ============================================================================= check_status() { step "Final status check" ERRORS=() WARNINGS=() echo "" # Check Python if check_command python3.12; then success "Python 3.12: $(python3.12 --version)" else ERRORS+=("Python 3.12 not found") fi # Check uv if check_command uv || [ -f "$HOME/.local/bin/uv" ]; then success "uv: $($HOME/.local/bin/uv --version 2>/dev/null || uv --version)" else ERRORS+=("uv not found") fi # Check Node.js if check_command node; then success "Node.js: $(node --version)" else ERRORS+=("Node.js not found") fi # Check Claude CLI if check_command claude; then success "Claude CLI: installed" else WARNINGS+=("Claude CLI not found (needed for AI processing)") fi # Check .env if [ -f "$PROJECT_DIR/.env" ]; then success ".env file: exists" else ERRORS+=(".env file not found") fi # Check systemd service if systemctl is-active --quiet d-brain-bot; then success "Bot service: running" else STATUS=$(systemctl is-active d-brain-bot 2>/dev/null || echo "unknown") ERRORS+=("Bot service not running (status: $STATUS)") fi echo "" # Summary if [ ${#ERRORS[@]} -eq 0 ] && [ ${#WARNINGS[@]} -eq 0 ]; then echo -e "${GREEN}${BOLD}" echo " ╔═══════════════════════════════════════════════════════════╗" echo " ║ ║" echo " ║ SETUP COMPLETE! ║" echo " ║ ║" echo " ╚═══════════════════════════════════════════════════════════╝" echo -e "${NC}" echo "" echo " Next steps:" echo " 1. Open Telegram and find your bot" echo " 2. Send /start to test" echo " 3. Send a voice message!" echo "" echo " Useful commands:" echo " - View logs: sudo journalctl -u d-brain-bot -f" echo " - Restart bot: sudo systemctl restart d-brain-bot" echo " - Stop bot: sudo systemctl stop d-brain-bot" echo "" else if [ ${#ERRORS[@]} -gt 0 ]; then echo -e "${RED}${BOLD}ERRORS:${NC}" for err in "${ERRORS[@]}"; do error "$err" done echo "" fi if [ ${#WARNINGS[@]} -gt 0 ]; then echo -e "${YELLOW}${BOLD}WARNINGS:${NC}" for wrn in "${WARNINGS[@]}"; do warn "$wrn" done echo "" fi echo "Troubleshooting:" echo " - Check logs: sudo journalctl -u d-brain-bot -n 50" echo " - Check .env: cat $PROJECT_DIR/.env" echo " - Manual start: cd $PROJECT_DIR && uv run python -m d_brain" echo "" fi } authorize_claude() { step "Claude CLI Authorization" if claude auth status 2>/dev/null | grep -q "Logged in"; then success "Claude CLI already authorized" return fi warn "Claude CLI needs authorization" echo "" echo "Run this command manually:" echo -e " ${CYAN}claude auth login${NC}" echo "" echo "This will open a browser for authentication." echo "After authorizing, the bot will be able to use Claude for AI processing." echo "" } # ============================================================================= # Main script # ============================================================================= main() { print_banner check_root check_os echo "This script will:" echo " 1. Install required software (Python, Node.js, uv, Claude CLI)" echo " 2. Clone your fork of the repository" echo " 3. Ask for your API tokens" echo " 4. Create configuration files" echo " 5. Set up auto-start service" echo "" read -p "Ready to start? (Y/n): " -r REPLY if [[ $REPLY =~ ^[Nn]$ ]]; then echo "Setup cancelled." exit 0 fi # Installation install_system_deps install_python install_uv install_nodejs install_claude_cli # Configuration clone_repository collect_tokens create_env_file install_dependencies configure_systemd configure_git_remote authorize_claude # Final check check_status } # Run main if script is executed (not sourced) if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then main "$@" fi