1451 lines
48 KiB
Bash
Executable File
1451 lines
48 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# =============================================================================
|
||
# Aetheel — Installer & Setup Wizard
|
||
# =============================================================================
|
||
# Usage:
|
||
# curl -fsSL <repo-url>/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" <<CONFIGEOF
|
||
{
|
||
"\$schema": "Aetheel configuration — edit this file, keep secrets in .env",
|
||
"log_level": "INFO",
|
||
"runtime": {
|
||
"engine": "$RUNTIME_ENGINE",
|
||
"mode": "$RUNTIME_MODE",
|
||
"model": $RUNTIME_MODEL,
|
||
"timeout_seconds": 120,
|
||
"server_url": "http://localhost:4096",
|
||
"format": "json"
|
||
},
|
||
"claude": {
|
||
"model": $CLAUDE_MODEL,
|
||
"timeout_seconds": 120,
|
||
"max_turns": 3,
|
||
"no_tools": false
|
||
},
|
||
"slack": {
|
||
"enabled": $SLACK_ENABLED
|
||
},
|
||
"telegram": {
|
||
"enabled": $TELEGRAM_ENABLED
|
||
},
|
||
"discord": {
|
||
"enabled": $DISCORD_ENABLED,
|
||
"listen_channels": []
|
||
},
|
||
"memory": {
|
||
"workspace": "~/.aetheel/workspace",
|
||
"db_path": "~/.aetheel/memory.db"
|
||
},
|
||
"scheduler": {
|
||
"db_path": "~/.aetheel/scheduler.db"
|
||
},
|
||
"heartbeat": {
|
||
"enabled": true,
|
||
"default_channel": "slack",
|
||
"default_channel_id": "",
|
||
"silent": false
|
||
},
|
||
"webchat": {
|
||
"enabled": ${ENABLE_WEBCHAT:-false},
|
||
"port": 8080,
|
||
"host": "127.0.0.1"
|
||
},
|
||
"mcp": {
|
||
"servers": {}
|
||
},
|
||
"hooks": {
|
||
"enabled": true
|
||
},
|
||
"webhooks": {
|
||
"enabled": ${ENABLE_WEBHOOKS:-false},
|
||
"port": 8090,
|
||
"host": "127.0.0.1",
|
||
"token": "${webhook_token:-}"
|
||
}
|
||
}
|
||
CONFIGEOF
|
||
|
||
success "Config written to $CONFIG_PATH"
|
||
|
||
# --- Identity Files (SOUL.md, USER.md, MEMORY.md) ---
|
||
|
||
WORKSPACE_DIR="$DATA_DIR/workspace"
|
||
mkdir -p "$WORKSPACE_DIR/daily"
|
||
|
||
printf "\n"
|
||
printf " ${BOLD}Identity Files${NC}\n"
|
||
dim "Aetheel uses three markdown files to define its personality and your context."
|
||
dim "All agents (main + subagents) share the same identity files."
|
||
dim " SOUL.md — The AI's personality, values, and communication style"
|
||
dim " USER.md — Your profile, preferences, and current focus"
|
||
dim " MEMORY.md — Long-term notes that persist across sessions"
|
||
dim ""
|
||
dim "Location: $WORKSPACE_DIR/"
|
||
printf "\n"
|
||
|
||
if [ -f "$WORKSPACE_DIR/SOUL.md" ] && [ -f "$WORKSPACE_DIR/USER.md" ]; then
|
||
info "Identity files already exist"
|
||
if confirm "Reconfigure identity files?" "n"; then
|
||
SETUP_IDENTITY=true
|
||
else
|
||
SETUP_IDENTITY=false
|
||
fi
|
||
else
|
||
SETUP_IDENTITY=true
|
||
fi
|
||
|
||
if [ "$SETUP_IDENTITY" = true ]; then
|
||
|
||
# --- SOUL.md ---
|
||
printf "\n"
|
||
printf " ${BOLD}SOUL.md — AI Personality${NC}\n"
|
||
dim "How should Aetheel communicate?"
|
||
printf "\n"
|
||
printf " ${BOLD}1)${NC} Default — Helpful, opinionated, resourceful, concise\n"
|
||
printf " ${BOLD}2)${NC} Professional — Formal, thorough, structured\n"
|
||
printf " ${BOLD}3)${NC} Casual — Relaxed, friendly, brief\n"
|
||
printf " ${BOLD}4)${NC} Custom — I'll write my own\n"
|
||
printf "\n"
|
||
ask "Choose [1-4]:"
|
||
read -r soul_choice
|
||
soul_choice="${soul_choice:-1}"
|
||
|
||
case "$soul_choice" in
|
||
2)
|
||
cat > "$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
|
||
|
||
<!-- Describe how you want the AI to communicate -->
|
||
|
||
## Core Principles
|
||
|
||
<!-- What values should guide its behavior? -->
|
||
|
||
## Boundaries
|
||
|
||
<!-- What should it never do? -->
|
||
|
||
---
|
||
|
||
_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
|
||
|
||
<!-- Describe how you want the AI to communicate -->
|
||
|
||
## Core Principles
|
||
|
||
<!-- What values should guide its behavior? -->
|
||
|
||
## Boundaries
|
||
|
||
<!-- What should it never do? -->
|
||
|
||
---
|
||
|
||
_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="<!-- What you're working on -->"
|
||
fi
|
||
|
||
cat > "$WORKSPACE_DIR/USER.md" <<USEREOF
|
||
# USER.md — Who I Am
|
||
|
||
## About Me
|
||
|
||
- **Name:** ${user_name_input}
|
||
- **Role:** ${user_role}
|
||
- **Timezone:** ${user_tz}
|
||
|
||
## Preferences
|
||
|
||
- **Communication style:** ${user_comm_text}
|
||
- **Technical level:** ${user_tech_text}
|
||
|
||
## Current Focus
|
||
|
||
${user_focus}
|
||
|
||
## Tools & Services
|
||
|
||
<!-- Services you use regularly -->
|
||
|
||
---
|
||
|
||
_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
|
||
|
||
<!-- Record important decisions and lessons learned -->
|
||
|
||
## Context
|
||
|
||
<!-- Persistent context that should carry across sessions -->
|
||
|
||
## Notes
|
||
|
||
<!-- Anything worth remembering -->
|
||
|
||
---
|
||
|
||
_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 <command> [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" <<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.aetheel</string>
|
||
<key>ProgramArguments</key>
|
||
<array>
|
||
<string>${BIN_DIR}/aetheel</string>
|
||
<string>start</string>
|
||
</array>
|
||
<key>WorkingDirectory</key>
|
||
<string>${INSTALL_DIR}</string>
|
||
<key>RunAtLoad</key>
|
||
<true/>
|
||
<key>KeepAlive</key>
|
||
<true/>
|
||
<key>EnvironmentVariables</key>
|
||
<dict>
|
||
<key>PATH</key>
|
||
<string>/usr/local/bin:/usr/bin:/bin:${HOME}/.local/bin:${HOME}/.opencode/bin:${HOME}/.claude/bin</string>
|
||
<key>HOME</key>
|
||
<string>${HOME}</string>
|
||
</dict>
|
||
<key>StandardOutPath</key>
|
||
<string>${DATA_DIR}/logs/aetheel.log</string>
|
||
<key>StandardErrorPath</key>
|
||
<string>${DATA_DIR}/logs/aetheel.error.log</string>
|
||
<key>ThrottleInterval</key>
|
||
<integer>10</integer>
|
||
</dict>
|
||
</plist>
|
||
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" <<UNITEOF
|
||
[Unit]
|
||
Description=Aetheel Personal AI Assistant
|
||
After=network.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
ExecStart=${BIN_DIR}/aetheel start
|
||
WorkingDirectory=${INSTALL_DIR}
|
||
Restart=always
|
||
RestartSec=10
|
||
Environment=HOME=${HOME}
|
||
Environment=PATH=/usr/local/bin:/usr/bin:/bin:${HOME}/.local/bin:${HOME}/.opencode/bin:${HOME}/.claude/bin
|
||
StandardOutput=append:${DATA_DIR}/logs/aetheel.log
|
||
StandardError=append:${DATA_DIR}/logs/aetheel.error.log
|
||
|
||
[Install]
|
||
WantedBy=default.target
|
||
UNITEOF
|
||
|
||
systemctl --user daemon-reload 2>>"$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"
|