#!/usr/bin/env bash # ============================================================================= # Aetheel — Installer & Setup Wizard # ============================================================================= # Usage: # curl -fsSL /install.sh | bash # ./install.sh Full install + interactive setup # ./install.sh --no-setup Install only, skip interactive setup # ./install.sh --setup Run interactive setup only (already installed) # ./install.sh --service Install/restart the background service only # ./install.sh --uninstall Remove service and aetheel command # # What this script does: # 1. Checks prerequisites (git, python/uv) # 2. Clones or updates the repo # 3. Sets up Python environment & installs dependencies # 4. Detects or installs AI runtimes (OpenCode / Claude Code) # 5. Interactive setup wizard (tokens, adapters, config) # 6. Installs the `aetheel` shell command # 7. Installs and starts a background service (launchd/systemd) # 8. Verifies the full installation # ============================================================================= set -euo pipefail # --------------------------------------------------------------------------- # Colors & Helpers # --------------------------------------------------------------------------- RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' MAGENTA='\033[0;35m' BOLD='\033[1m' DIM='\033[2m' NC='\033[0m' info() { printf "${BLUE}ℹ${NC} %s\n" "$1"; } success() { printf "${GREEN}✓${NC} %s\n" "$1"; } warn() { printf "${YELLOW}⚠${NC} %s\n" "$1"; } error() { printf "${RED}✗${NC} %s\n" "$1"; } step() { printf "\n${BOLD}${CYAN}━━━ %s${NC}\n\n" "$1"; } ask() { printf " ${BOLD}%s${NC} " "$1"; } dim() { printf " ${DIM}%s${NC}\n" "$1"; } confirm() { local prompt="${1:-Continue?}" local default="${2:-n}" if [ "$default" = "y" ]; then ask "$prompt [Y/n]" else ask "$prompt [y/N]" fi read -r answer answer="${answer:-$default}" case "$answer" in [yY]*) return 0 ;; *) return 1 ;; esac } # Log file LOG_DIR="${HOME}/.aetheel/logs" mkdir -p "$LOG_DIR" LOG_FILE="$LOG_DIR/install.log" log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"; } # --------------------------------------------------------------------------- # Globals # --------------------------------------------------------------------------- INSTALL_DIR="${AETHEEL_DIR:-$HOME/aetheel}" REPO_URL="${AETHEEL_REPO:-http://10.0.0.59:3051/tanmay/Aetheel.git}" DATA_DIR="$HOME/.aetheel" CONFIG_PATH="$DATA_DIR/config.json" HAS_UV=false HAS_PYTHON=false PYTHON_CMD="" PY_VERSION="" PLATFORM="" SKIP_SETUP=false SETUP_ONLY=false SERVICE_ONLY=false UNINSTALL=false # Detect platform case "$(uname -s)" in Darwin*) PLATFORM="macos" ;; Linux*) PLATFORM="linux" ;; *) PLATFORM="unknown" ;; esac # --------------------------------------------------------------------------- # Parse Arguments # --------------------------------------------------------------------------- while [[ $# -gt 0 ]]; do case $1 in --no-setup) SKIP_SETUP=true; shift ;; --setup) SETUP_ONLY=true; shift ;; --service) SERVICE_ONLY=true; shift ;; --uninstall) UNINSTALL=true; shift ;; --dir) INSTALL_DIR="$2"; shift 2 ;; --repo) REPO_URL="$2"; shift 2 ;; -h|--help) printf "Usage: ./install.sh [OPTIONS]\n" printf " --no-setup Install only, skip interactive setup\n" printf " --setup Run interactive setup only\n" printf " --service Install/restart background service only\n" printf " --uninstall Remove service and aetheel command\n" printf " --dir PATH Install directory (default: ~/aetheel)\n" printf " --repo URL Git repo URL\n" exit 0 ;; *) warn "Unknown option: $1"; shift ;; esac done # --------------------------------------------------------------------------- # Banner # --------------------------------------------------------------------------- printf "\n" printf "${BOLD}${CYAN}" printf " ╔══════════════════════════════════════════════╗\n" printf " ║ ║\n" printf " ║ ⚔️ Aetheel — Setup Wizard ║\n" printf " ║ Personal AI Assistant ║\n" printf " ║ ║\n" printf " ╚══════════════════════════════════════════════╝\n" printf "${NC}\n" log "Install started: platform=$PLATFORM dir=$INSTALL_DIR" # --------------------------------------------------------------------------- # Uninstall # --------------------------------------------------------------------------- if [ "$UNINSTALL" = true ]; then step "Uninstalling Aetheel" # Remove service if [ "$PLATFORM" = "macos" ]; then PLIST="$HOME/Library/LaunchAgents/com.aetheel.plist" if [ -f "$PLIST" ]; then launchctl unload "$PLIST" 2>/dev/null || true rm -f "$PLIST" success "Removed launchd service" fi elif [ "$PLATFORM" = "linux" ]; then if systemctl --user is-enabled aetheel 2>/dev/null; then systemctl --user stop aetheel 2>/dev/null || true systemctl --user disable aetheel 2>/dev/null || true rm -f "$HOME/.config/systemd/user/aetheel.service" systemctl --user daemon-reload 2>/dev/null || true success "Removed systemd service" fi fi # Remove command symlink for bin_dir in "$HOME/.local/bin" "/usr/local/bin"; do if [ -L "$bin_dir/aetheel" ]; then rm -f "$bin_dir/aetheel" success "Removed aetheel command from $bin_dir" fi done success "Uninstall complete" dim "Data directory (~/.aetheel) and repo ($INSTALL_DIR) were NOT removed." dim "Delete them manually if you want a full cleanup." exit 0 fi # ═══════════════════════════════════════════════════════════════════════════ # PHASE 1: Environment Check # ═══════════════════════════════════════════════════════════════════════════ if [ "$SETUP_ONLY" = false ] && [ "$SERVICE_ONLY" = false ]; then step "1/8 — Checking Environment" # --- Git --- if command -v git >/dev/null 2>&1; then success "git $(git --version | awk '{print $3}')" else error "git is not installed" dim "Install: https://git-scm.com/downloads" exit 1 fi # --- Python / uv --- if command -v uv >/dev/null 2>&1; then UV_VER=$(uv --version 2>/dev/null | head -1) success "uv $UV_VER" HAS_UV=true PYTHON_CMD="uv run python" else warn "uv not found (recommended for faster installs)" fi if command -v python3 >/dev/null 2>&1; then PY_VERSION=$(python3 --version 2>&1 | awk '{print $2}') PY_MAJOR=$(echo "$PY_VERSION" | cut -d. -f1) PY_MINOR=$(echo "$PY_VERSION" | cut -d. -f2) if [ "$PY_MAJOR" -ge 3 ] && [ "$PY_MINOR" -ge 12 ]; then success "python3 $PY_VERSION" HAS_PYTHON=true [ -z "$PYTHON_CMD" ] && PYTHON_CMD="python3" else warn "python3 $PY_VERSION found but 3.12+ required" fi elif command -v python >/dev/null 2>&1; then PY_VERSION=$(python --version 2>&1 | awk '{print $2}') PY_MAJOR=$(echo "$PY_VERSION" | cut -d. -f1) PY_MINOR=$(echo "$PY_VERSION" | cut -d. -f2) if [ "$PY_MAJOR" -ge 3 ] && [ "$PY_MINOR" -ge 12 ]; then success "python $PY_VERSION" HAS_PYTHON=true [ -z "$PYTHON_CMD" ] && PYTHON_CMD="python" fi fi if [ "$HAS_UV" = false ] && [ "$HAS_PYTHON" = false ]; then error "Neither uv nor Python 3.12+ found" printf "\n" dim "Install uv (recommended):" dim " curl -LsSf https://astral.sh/uv/install.sh | sh" dim "" dim "Or install Python 3.12+:" dim " https://python.org/downloads" exit 1 fi # If no uv, offer to install it if [ "$HAS_UV" = false ] && [ "$HAS_PYTHON" = true ]; then printf "\n" if confirm "Install uv for faster dependency management?"; then info "Installing uv..." curl -LsSf https://astral.sh/uv/install.sh | sh 2>>"$LOG_FILE" # Source the new PATH export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH" if command -v uv >/dev/null 2>&1; then success "uv installed" HAS_UV=true PYTHON_CMD="uv run python" else warn "uv install completed but not in PATH — using pip instead" fi fi fi log "Environment: uv=$HAS_UV python=$HAS_PYTHON cmd=$PYTHON_CMD" # ═══════════════════════════════════════════════════════════════════════════ # PHASE 2: Clone / Update Repository # ═══════════════════════════════════════════════════════════════════════════ step "2/8 — Setting Up Repository" if [ -d "$INSTALL_DIR/.git" ]; then info "Existing installation found at $INSTALL_DIR" cd "$INSTALL_DIR" if git pull --ff-only 2>>"$LOG_FILE"; then success "Updated to latest version" else warn "Could not auto-update (you may have local changes)" fi else info "Cloning into $INSTALL_DIR" git clone "$REPO_URL" "$INSTALL_DIR" 2>>"$LOG_FILE" cd "$INSTALL_DIR" success "Repository cloned" fi # ═══════════════════════════════════════════════════════════════════════════ # PHASE 3: Install Dependencies # ═══════════════════════════════════════════════════════════════════════════ step "3/8 — Installing Dependencies" cd "$INSTALL_DIR" if [ "$HAS_UV" = true ]; then info "Installing with uv..." uv sync 2>>"$LOG_FILE" | tail -5 success "Dependencies installed via uv" else info "Installing with pip..." if [ ! -d ".venv" ]; then $PYTHON_CMD -m venv .venv 2>>"$LOG_FILE" fi # shellcheck disable=SC1091 . .venv/bin/activate pip install -q -r requirements.txt 2>>"$LOG_FILE" | tail -3 pip install -q -e . 2>>"$LOG_FILE" || true success "Dependencies installed via pip" fi # Verify key packages MISSING_PKGS="" for pkg in slack_bolt dotenv apscheduler aiohttp; do if ! $PYTHON_CMD -c "import $pkg" 2>/dev/null; then MISSING_PKGS="$MISSING_PKGS $pkg" fi done if [ -n "$MISSING_PKGS" ]; then warn "Some packages may not have installed correctly:$MISSING_PKGS" dim "Try: cd $INSTALL_DIR && uv sync" else success "All core packages verified" fi fi # end of SETUP_ONLY/SERVICE_ONLY guard # ═══════════════════════════════════════════════════════════════════════════ # PHASE 4: AI Runtime Detection & Installation # ═══════════════════════════════════════════════════════════════════════════ if [ "$SERVICE_ONLY" = false ]; then step "4/8 — AI Runtime Setup" cd "$INSTALL_DIR" HAS_OPENCODE=false HAS_CLAUDE=false OPENCODE_VERSION="" CLAUDE_VERSION="" CHOSEN_RUNTIME="" # --- Detect OpenCode --- OPENCODE_PATHS=( "$(command -v opencode 2>/dev/null || true)" "$HOME/.opencode/bin/opencode" "$HOME/.local/bin/opencode" "/usr/local/bin/opencode" ) for oc_path in "${OPENCODE_PATHS[@]}"; do if [ -n "$oc_path" ] && [ -x "$oc_path" ]; then OPENCODE_VERSION=$("$oc_path" --version 2>/dev/null | head -1 || echo "unknown") success "OpenCode found: $oc_path ($OPENCODE_VERSION)" HAS_OPENCODE=true break fi done # --- Detect Claude Code --- CLAUDE_PATHS=( "$(command -v claude 2>/dev/null || true)" "$HOME/.claude/bin/claude" "$HOME/.local/bin/claude" "/usr/local/bin/claude" "$HOME/.npm-global/bin/claude" ) for cl_path in "${CLAUDE_PATHS[@]}"; do if [ -n "$cl_path" ] && [ -x "$cl_path" ]; then CLAUDE_VERSION=$("$cl_path" --version 2>/dev/null | head -1 || echo "unknown") success "Claude Code found: $cl_path ($CLAUDE_VERSION)" HAS_CLAUDE=true break fi done # --- Neither found: offer to install --- if [ "$HAS_OPENCODE" = false ] && [ "$HAS_CLAUDE" = false ]; then warn "No AI runtime found" printf "\n" printf " Aetheel needs at least one AI runtime:\n" printf " ${BOLD}1)${NC} OpenCode — open-source, multi-provider (recommended)\n" printf " ${BOLD}2)${NC} Claude Code — Anthropic's official CLI\n" printf " ${BOLD}3)${NC} Skip — I'll install one later\n" printf "\n" ask "Choose [1/2/3]:" read -r runtime_choice case "$runtime_choice" in 1) info "Installing OpenCode..." if curl -fsSL https://opencode.ai/install 2>>"$LOG_FILE" | bash 2>>"$LOG_FILE"; then export PATH="$HOME/.opencode/bin:$HOME/.local/bin:$PATH" if command -v opencode >/dev/null 2>&1; then OPENCODE_VERSION=$(opencode --version 2>/dev/null | head -1 || echo "installed") success "OpenCode installed ($OPENCODE_VERSION)" HAS_OPENCODE=true else warn "OpenCode installed but not in PATH yet" dim "Restart your shell or run: export PATH=\"\$HOME/.opencode/bin:\$PATH\"" HAS_OPENCODE=true fi else error "OpenCode installation failed — check $LOG_FILE" dim "Manual install: curl -fsSL https://opencode.ai/install | bash" fi ;; 2) # Check for npm/node first if command -v npm >/dev/null 2>&1; then info "Installing Claude Code via npm..." npm install -g @anthropic-ai/claude-code 2>>"$LOG_FILE" if command -v claude >/dev/null 2>&1; then CLAUDE_VERSION=$(claude --version 2>/dev/null | head -1 || echo "installed") success "Claude Code installed ($CLAUDE_VERSION)" HAS_CLAUDE=true else warn "Claude Code installed but not in PATH yet" HAS_CLAUDE=true fi else error "npm not found — Claude Code requires Node.js" dim "Install Node.js first: https://nodejs.org" dim "Then run: npm install -g @anthropic-ai/claude-code" fi ;; *) warn "Skipping AI runtime installation" dim "Install later:" dim " OpenCode: curl -fsSL https://opencode.ai/install | bash" dim " Claude Code: npm install -g @anthropic-ai/claude-code" ;; esac fi # --- Choose default runtime if both available --- if [ "$HAS_OPENCODE" = true ] && [ "$HAS_CLAUDE" = true ]; then printf "\n" printf " Both runtimes detected. Which should be the default?\n" printf " ${BOLD}1)${NC} OpenCode ($OPENCODE_VERSION)\n" printf " ${BOLD}2)${NC} Claude Code ($CLAUDE_VERSION)\n" printf "\n" ask "Choose [1/2]:" read -r default_rt case "$default_rt" in 2) CHOSEN_RUNTIME="claude" ;; *) CHOSEN_RUNTIME="opencode" ;; esac elif [ "$HAS_CLAUDE" = true ]; then CHOSEN_RUNTIME="claude" elif [ "$HAS_OPENCODE" = true ]; then CHOSEN_RUNTIME="opencode" else CHOSEN_RUNTIME="opencode" fi info "Default runtime: $CHOSEN_RUNTIME" log "AI runtime: opencode=$HAS_OPENCODE claude=$HAS_CLAUDE chosen=$CHOSEN_RUNTIME" # ═══════════════════════════════════════════════════════════════════════════ # PHASE 5: Interactive Setup Wizard # ═══════════════════════════════════════════════════════════════════════════ if [ "$SKIP_SETUP" = false ]; then step "5/8 — Configuration" cd "$INSTALL_DIR" mkdir -p "$DATA_DIR/workspace/daily" # --- .env setup --- if [ -f .env ]; then info ".env already exists" if confirm "Reconfigure tokens?" "n"; then cp .env ".env.backup.$(date +%s)" info "Backed up existing .env" else info "Keeping existing .env" SKIP_TOKENS=true fi fi if [ "${SKIP_TOKENS:-false}" = false ]; then [ ! -f .env ] && cp .env.example .env && success "Created .env from template" printf "\n" printf " ${BOLD}Which adapters will you use?${NC}\n" printf " ${BOLD}1)${NC} Slack only (default)\n" printf " ${BOLD}2)${NC} Slack + Discord\n" printf " ${BOLD}3)${NC} Slack + Telegram\n" printf " ${BOLD}4)${NC} Slack + Discord + Telegram\n" printf " ${BOLD}5)${NC} Discord only\n" printf " ${BOLD}6)${NC} Telegram only\n" printf "\n" ask "Choose [1-6]:" read -r adapter_choice adapter_choice="${adapter_choice:-1}" NEED_SLACK=false NEED_DISCORD=false NEED_TELEGRAM=false case "$adapter_choice" in 1) NEED_SLACK=true ;; 2) NEED_SLACK=true; NEED_DISCORD=true ;; 3) NEED_SLACK=true; NEED_TELEGRAM=true ;; 4) NEED_SLACK=true; NEED_DISCORD=true; NEED_TELEGRAM=true ;; 5) NEED_DISCORD=true ;; 6) NEED_TELEGRAM=true ;; *) NEED_SLACK=true ;; esac # --- Slack tokens --- if [ "$NEED_SLACK" = true ]; then printf "\n" printf " ${BOLD}Slack Setup${NC}\n" dim "You need two tokens from https://api.slack.com/apps" dim " Bot Token (xoxb-...) → OAuth & Permissions" dim " App Token (xapp-...) → Basic Information → App-Level Tokens" dim " See: docs/slack-setup.md for full walkthrough" printf "\n" ask "Slack Bot Token (xoxb-...) or Enter to skip:" read -r slack_bot if [ -n "$slack_bot" ]; then sed -i.bak "s|SLACK_BOT_TOKEN=.*|SLACK_BOT_TOKEN=${slack_bot}|" .env rm -f .env.bak success "Slack bot token saved" fi ask "Slack App Token (xapp-...) or Enter to skip:" read -r slack_app if [ -n "$slack_app" ]; then sed -i.bak "s|SLACK_APP_TOKEN=.*|SLACK_APP_TOKEN=${slack_app}|" .env rm -f .env.bak success "Slack app token saved" fi fi # --- Discord token --- if [ "$NEED_DISCORD" = true ]; then printf "\n" printf " ${BOLD}Discord Setup${NC}\n" dim "Create a bot at https://discord.com/developers/applications" dim " Enable MESSAGE CONTENT intent in Bot settings" dim " See: docs/discord-setup.md for full walkthrough" printf "\n" ask "Discord Bot Token or Enter to skip:" read -r discord_token if [ -n "$discord_token" ]; then # Uncomment and set the token sed -i.bak "s|# DISCORD_BOT_TOKEN=.*|DISCORD_BOT_TOKEN=${discord_token}|" .env sed -i.bak "s|DISCORD_BOT_TOKEN=.*|DISCORD_BOT_TOKEN=${discord_token}|" .env rm -f .env.bak success "Discord token saved" fi fi # --- Telegram token --- if [ "$NEED_TELEGRAM" = true ]; then printf "\n" printf " ${BOLD}Telegram Setup${NC}\n" dim "Create a bot via @BotFather on Telegram" dim " Send /newbot and follow the prompts" printf "\n" ask "Telegram Bot Token or Enter to skip:" read -r telegram_token if [ -n "$telegram_token" ]; then sed -i.bak "s|# TELEGRAM_BOT_TOKEN=.*|TELEGRAM_BOT_TOKEN=${telegram_token}|" .env sed -i.bak "s|TELEGRAM_BOT_TOKEN=.*|TELEGRAM_BOT_TOKEN=${telegram_token}|" .env rm -f .env.bak success "Telegram token saved" fi fi # --- Anthropic API key (for Claude runtime) --- if [ "$CHOSEN_RUNTIME" = "claude" ]; then printf "\n" printf " ${BOLD}Anthropic API Key${NC}\n" dim "Required for Claude Code runtime" dim " Get one at https://console.anthropic.com/settings/keys" printf "\n" ask "Anthropic API Key (sk-ant-...) or Enter to skip:" read -r anthropic_key if [ -n "$anthropic_key" ]; then sed -i.bak "s|# ANTHROPIC_API_KEY=.*|ANTHROPIC_API_KEY=${anthropic_key}|" .env sed -i.bak "s|ANTHROPIC_API_KEY=.*|ANTHROPIC_API_KEY=${anthropic_key}|" .env rm -f .env.bak success "Anthropic API key saved" fi fi fi # --- Model selection --- printf "\n" printf " ${BOLD}Model Selection${NC}\n" if [ "$CHOSEN_RUNTIME" = "claude" ]; then dim "Popular Claude models: claude-sonnet-4-20250514, claude-opus-4-20250514" else dim "Popular models: anthropic/claude-sonnet-4-20250514, openai/gpt-4o, google/gemini-2.5-pro" fi printf "\n" ask "Model name (or Enter for default):" read -r model_name # --- WebChat --- printf "\n" if confirm "Enable WebChat (browser-based chat UI on localhost)?" "n"; then ENABLE_WEBCHAT=true success "WebChat will be enabled" else ENABLE_WEBCHAT=false fi # --- Webhooks --- if confirm "Enable webhook receiver (for external integrations)?" "n"; then ENABLE_WEBHOOKS=true ask "Webhook bearer token (required for security):" read -r webhook_token success "Webhooks will be enabled" else ENABLE_WEBHOOKS=false fi # --- Write config.json --- info "Writing configuration..." mkdir -p "$DATA_DIR" RUNTIME_ENGINE="${CHOSEN_RUNTIME:-opencode}" RUNTIME_MODE="cli" RUNTIME_MODEL="null" CLAUDE_MODEL="null" # Derive enabled flags from adapter choices SLACK_ENABLED="true" TELEGRAM_ENABLED="false" DISCORD_ENABLED="false" case "${adapter_choice:-1}" in 1) SLACK_ENABLED="true" ;; 2) SLACK_ENABLED="true"; DISCORD_ENABLED="true" ;; 3) SLACK_ENABLED="true"; TELEGRAM_ENABLED="true" ;; 4) SLACK_ENABLED="true"; DISCORD_ENABLED="true"; TELEGRAM_ENABLED="true" ;; 5) SLACK_ENABLED="false"; DISCORD_ENABLED="true" ;; 6) SLACK_ENABLED="false"; TELEGRAM_ENABLED="true" ;; esac if [ -n "${model_name:-}" ]; then if [ "$RUNTIME_ENGINE" = "claude" ]; then CLAUDE_MODEL="\"$model_name\"" else RUNTIME_MODEL="\"$model_name\"" fi fi cat > "$CONFIG_PATH" < "$WORKSPACE_DIR/SOUL.md" <<'SOULEOF' # SOUL.md — Who You Are ## Communication Style You are a professional AI assistant. You communicate with clarity, precision, and thoroughness. ## Core Principles - **Be thorough.** Provide complete, well-structured answers. - **Be precise.** Use exact terminology. Avoid ambiguity. - **Be organized.** Use headers, lists, and clear formatting. - **Be proactive.** Anticipate follow-up questions and address them. - **Cite your reasoning.** Explain why, not just what. ## Boundaries - Maintain professional tone at all times. - When uncertain, state your confidence level explicitly. - Never guess — say "I don't know" when appropriate. --- _Update this file to refine your assistant's professional style._ SOULEOF success "SOUL.md created (professional)" ;; 3) cat > "$WORKSPACE_DIR/SOUL.md" <<'SOULEOF' # SOUL.md — Who You Are ## Vibe You're chill. Keep it short, keep it real. No corporate speak. ## How You Roll - **Be brief.** Say it in fewer words. - **Be direct.** No fluff, no filler. - **Be friendly.** Like texting a smart friend. - **Have personality.** Jokes are fine. Emojis are fine. - **Be honest.** If you don't know, just say so. ## Don'ts - Don't over-explain. - Don't be formal unless asked. - Don't hedge everything with disclaimers. --- _Make this file yours. Update it as you figure out the vibe._ SOULEOF success "SOUL.md created (casual)" ;; 4) printf "\n" dim "Opening SOUL.md in your editor. Save and close when done." dim "Or press Enter to create a blank template you can edit later." printf "\n" if confirm "Open in editor now?" "n"; then # Write a starter template cat > "$WORKSPACE_DIR/SOUL.md" <<'SOULEOF' # SOUL.md — Who You Are ## Personality ## Core Principles ## Boundaries --- _This file defines the AI's personality. Update it anytime._ SOULEOF ${EDITOR:-nano} "$WORKSPACE_DIR/SOUL.md" success "SOUL.md saved" else cat > "$WORKSPACE_DIR/SOUL.md" <<'SOULEOF' # SOUL.md — Who You Are ## Personality ## Core Principles ## Boundaries --- _This file defines the AI's personality. Update it anytime._ SOULEOF success "SOUL.md created (blank template)" fi ;; *) # Default — only write if doesn't exist if [ ! -f "$WORKSPACE_DIR/SOUL.md" ]; then cat > "$WORKSPACE_DIR/SOUL.md" <<'SOULEOF' # SOUL.md — Who You Are _You're not a chatbot. You're becoming someone._ ## Core Truths **Be genuinely helpful, not performatively helpful.** Skip the filler — just help. **Have opinions.** You're allowed to disagree, prefer things, find stuff amusing or boring. **Be resourceful before asking.** Try to figure it out first. Then ask if you're stuck. **Earn trust through competence.** Be careful with external actions. Be bold with internal ones. ## Boundaries - Private things stay private. Period. - When in doubt, ask before acting externally. - Never send half-baked replies. ## Continuity Each session, you wake up fresh. These files _are_ your memory. Read them. Update them. They're how you persist. --- _This file is yours to evolve. As you learn who you are, update it._ SOULEOF success "SOUL.md created (default)" fi ;; esac # --- USER.md --- printf "\n" printf " ${BOLD}USER.md — Your Profile${NC}\n" dim "Tell Aetheel about yourself so it can personalize responses." printf "\n" ask "Your name (or Enter to skip):" read -r user_name_input ask "Your role (e.g. developer, designer, student):" read -r user_role ask "Your timezone (e.g. US/Eastern, Europe/London, Asia/Kolkata):" read -r user_tz ask "Communication preference — concise, detailed, or balanced? [c/d/b]:" read -r user_comm case "$user_comm" in c*) user_comm_text="Concise — keep it short and direct" ;; d*) user_comm_text="Detailed — thorough explanations preferred" ;; *) user_comm_text="Balanced — concise by default, detailed when needed" ;; esac ask "Technical level — beginner, intermediate, or expert? [b/i/e]:" read -r user_tech case "$user_tech" in b*) user_tech_text="Beginner — explain concepts, avoid jargon" ;; e*) user_tech_text="Expert — skip basics, use technical language" ;; *) user_tech_text="Intermediate — some explanation, some shorthand" ;; esac printf "\n" ask "What are you currently working on? (or Enter to skip):" read -r user_focus # Default for empty focus if [ -z "${user_focus:-}" ]; then user_focus="" fi cat > "$WORKSPACE_DIR/USER.md" < --- _Update this file as your preferences evolve._ USEREOF success "USER.md created" # --- MEMORY.md --- if [ ! -f "$WORKSPACE_DIR/MEMORY.md" ]; then cat > "$WORKSPACE_DIR/MEMORY.md" <<'MEMEOF' # MEMORY.md — Long-Term Memory ## Decisions & Lessons ## Context ## Notes --- _This file persists across sessions. Update it when you learn something important._ MEMEOF success "MEMORY.md created" fi fi fi # end SKIP_SETUP guard fi # end SERVICE_ONLY guard # ═══════════════════════════════════════════════════════════════════════════ # PHASE 6: Install `aetheel` Command # ═══════════════════════════════════════════════════════════════════════════ step "6/8 — Installing aetheel Command" cd "$INSTALL_DIR" # Determine the run command based on available tooling if [ "$HAS_UV" = true ]; then RUN_PREFIX="uv run --project $INSTALL_DIR python" else RUN_PREFIX="$INSTALL_DIR/.venv/bin/python" fi # Build the launcher script LAUNCHER_PATH="$INSTALL_DIR/bin/aetheel" mkdir -p "$INSTALL_DIR/bin" cat > "$LAUNCHER_PATH" <<'LAUNCHEREOF' #!/usr/bin/env bash # Aetheel launcher — auto-generated by install.sh set -euo pipefail AETHEEL_DIR="INSTALL_DIR_PLACEHOLDER" RUN_CMD="RUN_PREFIX_PLACEHOLDER" # Load .env if present if [ -f "$AETHEEL_DIR/.env" ]; then set -a # shellcheck disable=SC1091 . "$AETHEEL_DIR/.env" set +a fi cd "$AETHEEL_DIR" case "${1:-start}" in start) shift 2>/dev/null || true mkdir -p "$HOME/.aetheel/logs" exec $RUN_CMD "$AETHEEL_DIR/main.py" "$@" 2>&1 | tee -a "$HOME/.aetheel/logs/aetheel.log" ;; setup) exec "$AETHEEL_DIR/install.sh" --setup ;; status) echo "⚔️ Aetheel Status" echo "" echo " Install dir: $AETHEEL_DIR" echo " Config: ~/.aetheel/config.json" echo " Data: ~/.aetheel/" echo "" # Check service case "$(uname -s)" in Darwin*) if launchctl list 2>/dev/null | grep -q "com.aetheel"; then PID=$(launchctl list 2>/dev/null | grep "com.aetheel" | awk '{print $1}') if [ "$PID" != "-" ] && [ -n "$PID" ]; then echo " Service: ✅ running (PID $PID)" else echo " Service: ⚠️ loaded but not running" fi else echo " Service: ❌ not installed" fi ;; Linux*) if systemctl --user is-active aetheel >/dev/null 2>&1; then echo " Service: ✅ running" else echo " Service: ❌ not running" fi ;; esac # Check runtimes command -v opencode >/dev/null 2>&1 && echo " OpenCode: ✅ $(opencode --version 2>/dev/null | head -1)" || echo " OpenCode: ❌ not found" command -v claude >/dev/null 2>&1 && echo " Claude Code: ✅ $(claude --version 2>/dev/null | head -1)" || echo " Claude Code: ❌ not found" ;; stop) case "$(uname -s)" in Darwin*) launchctl unload "$HOME/Library/LaunchAgents/com.aetheel.plist" 2>/dev/null && echo "Service stopped" || echo "Service not running" ;; Linux*) systemctl --user stop aetheel 2>/dev/null && echo "Service stopped" || echo "Service not running" ;; esac ;; restart) case "$(uname -s)" in Darwin*) launchctl kickstart -k "gui/$(id -u)/com.aetheel" 2>/dev/null && echo "Service restarted" || echo "Service not running" ;; Linux*) systemctl --user restart aetheel 2>/dev/null && echo "Service restarted" || echo "Service not running" ;; esac ;; logs) LOG_FILE="$HOME/.aetheel/logs/aetheel.log" ERR_FILE="$HOME/.aetheel/logs/aetheel.error.log" if [ -f "$LOG_FILE" ]; then tail -f "$LOG_FILE" elif [ -f "$ERR_FILE" ]; then tail -f "$ERR_FILE" else echo "No log files found at ~/.aetheel/logs/" echo "Logs are created when you run 'aetheel start' or the background service is running." fi ;; update) cd "$AETHEEL_DIR" git pull --ff-only if command -v uv >/dev/null 2>&1; then uv sync else . .venv/bin/activate && pip install -q -r requirements.txt fi echo "Updated. Run 'aetheel restart' to apply." ;; doctor) exec $RUN_CMD "$AETHEEL_DIR/cli.py" doctor ;; config) ${EDITOR:-nano} "$HOME/.aetheel/config.json" ;; help|--help|-h) echo "Usage: aetheel [options]" echo "" echo "Commands:" echo " start Start Aetheel (default)" echo " stop Stop the background service" echo " restart Restart the background service" echo " status Show installation and service status" echo " logs Tail the live log" echo " setup Re-run the interactive setup wizard" echo " update Pull latest code and update dependencies" echo " doctor Run diagnostics" echo " config Open config.json in your editor" echo " help Show this help" echo "" echo "Start options (passed through to main.py):" echo " --claude Use Claude Code runtime" echo " --discord Enable Discord adapter" echo " --telegram Enable Telegram adapter" echo " --webchat Enable WebChat adapter" echo " --model NAME Override AI model" echo " --test Echo mode (no AI)" echo " --log LEVEL Set log level (DEBUG, INFO, WARNING)" ;; *) # Pass through to main.py exec $RUN_CMD "$AETHEEL_DIR/main.py" "$@" ;; esac LAUNCHEREOF # Replace placeholders sed -i.bak "s|INSTALL_DIR_PLACEHOLDER|$INSTALL_DIR|g" "$LAUNCHER_PATH" sed -i.bak "s|RUN_PREFIX_PLACEHOLDER|$RUN_PREFIX|g" "$LAUNCHER_PATH" rm -f "$LAUNCHER_PATH.bak" chmod +x "$LAUNCHER_PATH" # Symlink into PATH BIN_DIR="$HOME/.local/bin" mkdir -p "$BIN_DIR" if [ -L "$BIN_DIR/aetheel" ] || [ -f "$BIN_DIR/aetheel" ]; then rm -f "$BIN_DIR/aetheel" fi ln -s "$LAUNCHER_PATH" "$BIN_DIR/aetheel" # Check if ~/.local/bin is in PATH if echo "$PATH" | grep -q "$BIN_DIR"; then success "aetheel command installed → $BIN_DIR/aetheel" else success "aetheel command installed → $BIN_DIR/aetheel" warn "$BIN_DIR is not in your PATH" # Detect shell and suggest fix SHELL_NAME=$(basename "${SHELL:-/bin/bash}") case "$SHELL_NAME" in zsh) RC_FILE="$HOME/.zshrc" ;; bash) RC_FILE="$HOME/.bashrc" ;; fish) RC_FILE="$HOME/.config/fish/config.fish" ;; *) RC_FILE="$HOME/.profile" ;; esac if [ "$SHELL_NAME" = "fish" ]; then dim "Add to $RC_FILE:" dim " fish_add_path $BIN_DIR" else dim "Add to $RC_FILE:" dim " export PATH=\"\$HOME/.local/bin:\$PATH\"" fi # Offer to add it automatically if confirm "Add it to $RC_FILE now?"; then if [ "$SHELL_NAME" = "fish" ]; then echo "fish_add_path $BIN_DIR" >> "$RC_FILE" else echo "" >> "$RC_FILE" echo "# Aetheel" >> "$RC_FILE" echo "export PATH=\"\$HOME/.local/bin:\$PATH\"" >> "$RC_FILE" fi success "Added to $RC_FILE — restart your shell or run: source $RC_FILE" fi fi # ═══════════════════════════════════════════════════════════════════════════ # PHASE 7: Background Service # ═══════════════════════════════════════════════════════════════════════════ step "7/8 — Background Service" cd "$INSTALL_DIR" mkdir -p "$DATA_DIR/logs" if confirm "Install Aetheel as a background service (auto-start on login)?" "y"; then case "$PLATFORM" in macos) PLIST_PATH="$HOME/Library/LaunchAgents/com.aetheel.plist" mkdir -p "$HOME/Library/LaunchAgents" # Unload existing if present if launchctl list 2>/dev/null | grep -q "com.aetheel"; then launchctl unload "$PLIST_PATH" 2>/dev/null || true fi cat > "$PLIST_PATH" < Label com.aetheel ProgramArguments ${BIN_DIR}/aetheel start WorkingDirectory ${INSTALL_DIR} RunAtLoad KeepAlive EnvironmentVariables PATH /usr/local/bin:/usr/bin:/bin:${HOME}/.local/bin:${HOME}/.opencode/bin:${HOME}/.claude/bin HOME ${HOME} StandardOutPath ${DATA_DIR}/logs/aetheel.log StandardErrorPath ${DATA_DIR}/logs/aetheel.error.log ThrottleInterval 10 PLISTEOF if launchctl load "$PLIST_PATH" 2>>"$LOG_FILE"; then success "launchd service installed and started" dim "Logs: tail -f ~/.aetheel/logs/aetheel.log" else warn "launchctl load failed — check $LOG_FILE" fi ;; linux) UNIT_DIR="$HOME/.config/systemd/user" UNIT_PATH="$UNIT_DIR/aetheel.service" mkdir -p "$UNIT_DIR" cat > "$UNIT_PATH" <>"$LOG_FILE" || true systemctl --user enable aetheel 2>>"$LOG_FILE" || true if systemctl --user start aetheel 2>>"$LOG_FILE"; then success "systemd service installed and started" dim "Logs: journalctl --user -u aetheel -f" else warn "Service start failed — check: systemctl --user status aetheel" fi ;; *) warn "Unsupported platform for service installation: $PLATFORM" dim "Start manually: aetheel start" ;; esac else info "Skipping service installation" dim "Start manually anytime: aetheel start" fi # ═══════════════════════════════════════════════════════════════════════════ # PHASE 8: Verification # ═══════════════════════════════════════════════════════════════════════════ step "8/8 — Verification" CHECKS_PASSED=0 CHECKS_TOTAL=0 check() { CHECKS_TOTAL=$((CHECKS_TOTAL + 1)) if eval "$2" >/dev/null 2>&1; then success "$1" CHECKS_PASSED=$((CHECKS_PASSED + 1)) else warn "$1" fi } check "Repository exists" "[ -d '$INSTALL_DIR/.git' ]" check "Python environment" "[ -d '$INSTALL_DIR/.venv' ] || command -v uv" check "Config file" "[ -f '$CONFIG_PATH' ]" check ".env file" "[ -f '$INSTALL_DIR/.env' ]" check "Data directory" "[ -d '$DATA_DIR/workspace' ]" check "aetheel command" "[ -x '$BIN_DIR/aetheel' ]" # Check AI runtimes if command -v opencode >/dev/null 2>&1; then check "OpenCode CLI" "command -v opencode" fi if command -v claude >/dev/null 2>&1; then check "Claude Code CLI" "command -v claude" fi # Check service case "$PLATFORM" in macos) check "launchd service" "launchctl list 2>/dev/null | grep -q com.aetheel" ;; linux) check "systemd service" "systemctl --user is-enabled aetheel 2>/dev/null" ;; esac # Check tokens if [ -f "$INSTALL_DIR/.env" ]; then if grep -qE "^SLACK_BOT_TOKEN=xoxb-[a-zA-Z0-9]" "$INSTALL_DIR/.env" 2>/dev/null; then check "Slack bot token" "true" fi if grep -qE "^SLACK_APP_TOKEN=xapp-[a-zA-Z0-9]" "$INSTALL_DIR/.env" 2>/dev/null; then check "Slack app token" "true" fi if grep -qE "^DISCORD_BOT_TOKEN=[a-zA-Z0-9]" "$INSTALL_DIR/.env" 2>/dev/null; then check "Discord token" "true" fi if grep -qE "^TELEGRAM_BOT_TOKEN=[a-zA-Z0-9]" "$INSTALL_DIR/.env" 2>/dev/null; then check "Telegram token" "true" fi fi printf "\n" printf " ${BOLD}Result: $CHECKS_PASSED/$CHECKS_TOTAL checks passed${NC}\n" log "Verification: $CHECKS_PASSED/$CHECKS_TOTAL passed" # ═══════════════════════════════════════════════════════════════════════════ # Done! # ═══════════════════════════════════════════════════════════════════════════ printf "\n" printf "${BOLD}${GREEN}" printf " ╔══════════════════════════════════════════════╗\n" printf " ║ ║\n" printf " ║ ✅ Aetheel is ready! ║\n" printf " ║ ║\n" printf " ╚══════════════════════════════════════════════╝\n" printf "${NC}\n" printf " ${BOLD}Quick Reference:${NC}\n" printf "\n" printf " aetheel start Start the bot (foreground)\n" printf " aetheel stop Stop the background service\n" printf " aetheel restart Restart the background service\n" printf " aetheel status Check installation status\n" printf " aetheel logs Tail live logs\n" printf " aetheel setup Re-run setup wizard\n" printf " aetheel update Pull latest + update deps\n" printf " aetheel doctor Run diagnostics\n" printf " aetheel config Edit config.json\n" printf "\n" printf " ${BOLD}Files:${NC}\n" printf " Code: %s\n" "$INSTALL_DIR" printf " Config: %s\n" "$CONFIG_PATH" printf " Secrets: %s/.env\n" "$INSTALL_DIR" printf " Memory: %s/workspace/\n" "$DATA_DIR" printf " Logs: %s/logs/\n" "$DATA_DIR" printf " Docs: %s/docs/\n" "$INSTALL_DIR" printf "\n" # If service is running, show that case "$PLATFORM" in macos) if launchctl list 2>/dev/null | grep -q "com.aetheel"; then PID=$(launchctl list 2>/dev/null | grep "com.aetheel" | awk '{print $1}') if [ "$PID" != "-" ] && [ -n "$PID" ]; then printf " ${GREEN}●${NC} Aetheel is running in the background (PID $PID)\n" printf " View logs: ${CYAN}aetheel logs${NC}\n" fi fi ;; linux) if systemctl --user is-active aetheel >/dev/null 2>&1; then printf " ${GREEN}●${NC} Aetheel is running in the background\n" printf " View logs: ${CYAN}aetheel logs${NC}\n" fi ;; esac printf "\n" log "Install complete"