Files
Aetheel/install.sh

1451 lines
48 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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"