claude-os/setup-claude-os.sh
Bob Roberts 753d427896 fix: Configure MCP server per-project via claude mcp add
Claude Code stores MCP configs in ~/.claude.json per-project, NOT in
~/.claude/settings.json. The previous approach of writing to settings.json
didn't work - projects showed "No MCP servers configured".

Changes:
- Remove broken MCP setup from setup-claude-os.sh (was writing to wrong file)
- Add Step 2.5 to /claude-os-init that runs `claude mcp add` for each project
- Update final summary to remind users to restart Claude Code after init

Now the workflow is:
1. ./setup-claude-os.sh - Install Claude OS once
2. /claude-os-init - Run in each project (creates KBs + configures MCP)
3. Restart Claude Code - MCP connects
4. mcp__code-forge__* tools available!

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 18:16:19 -06:00

1213 lines
49 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.

#!/bin/bash
# ╔═══════════════════════════════════════════════════════════════════════════╗
# ║ Claude OS Installer ║
# ║ ║
# ║ Beautiful, unified setup for Claude's AI memory system ║
# ║ Works on macOS and Linux ║
# ╚═══════════════════════════════════════════════════════════════════════════╝
set -e
# ═══════════════════════════════════════════════════════════════════════════
# CONFIGURATION
# ═══════════════════════════════════════════════════════════════════════════
VERSION="2.2.0"
CLAUDE_OS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
USER_CLAUDE_DIR="${HOME}/.claude"
TEMPLATES_DIR="${CLAUDE_OS_DIR}/templates"
# Mode flags
DRY_RUN=false
DEMO_MODE=false
FORCE_MODE=false
# Default model choices
DEFAULT_LLM_MODEL="llama3.2:3b" # Lite model - faster download, works on most machines
DEFAULT_EMBED_MODEL="nomic-embed-text" # Best local embedding model
FULL_LLM_MODEL="llama3.1:8b" # Full model - better quality
# ═══════════════════════════════════════════════════════════════════════════
# COLORS & STYLING
# ═══════════════════════════════════════════════════════════════════════════
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
WHITE='\033[1;37m'
DIM='\033[2m'
BOLD='\033[1m'
NC='\033[0m'
# Box drawing characters
BOX_TL="╭"
BOX_TR="╮"
BOX_BL="╰"
BOX_BR="╯"
BOX_H="─"
BOX_V="│"
# ═══════════════════════════════════════════════════════════════════════════
# GUM DETECTION & SETUP
# ═══════════════════════════════════════════════════════════════════════════
# Gum (https://github.com/charmbracelet/gum) provides beautiful interactive
# prompts. If available, we use it. Otherwise, we fall back to bash.
HAS_GUM=false
if command -v gum &> /dev/null; then
HAS_GUM=true
fi
# Offer to install gum for enhanced experience
offer_gum_install() {
if [[ "$HAS_GUM" == "true" ]]; then
return
fi
echo ""
echo -e "${CYAN}┌────────────────────────────────────────────────────────────────┐${NC}"
echo -e "${CYAN}│${NC} ${WHITE}✨ Enhanced installer available!${NC} ${CYAN}│${NC}"
echo -e "${CYAN}│${NC} ${CYAN}│${NC}"
echo -e "${CYAN}│${NC} ${DIM}Install 'gum' for a more beautiful experience with${NC} ${CYAN}│${NC}"
echo -e "${CYAN}│${NC} ${DIM}interactive menus and smooth animations.${NC} ${CYAN}│${NC}"
echo -e "${CYAN}└────────────────────────────────────────────────────────────────┘${NC}"
echo ""
echo -ne " ${WHITE}Install gum? [y/N]:${NC} "
read -r install_gum
if [[ "$install_gum" =~ ^[Yy]$ ]]; then
echo ""
info "Installing gum..."
if [[ "$OS" == "macos" ]]; then
brew install gum &>/dev/null &
spinner $! "Installing gum via Homebrew..."
elif [[ "$OS" == "linux" ]]; then
# Try various package managers
if [[ "$PKG_MANAGER" == "apt" ]]; then
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://repo.charm.sh/apt/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg 2>/dev/null
echo "deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *" | sudo tee /etc/apt/sources.list.d/charm.list > /dev/null
sudo apt update -qq && sudo apt install -y -qq gum &>/dev/null &
spinner $! "Installing gum via apt..."
elif command -v go &> /dev/null; then
go install github.com/charmbracelet/gum@latest &>/dev/null &
spinner $! "Installing gum via go..."
else
warn "Could not auto-install gum. Install manually: https://github.com/charmbracelet/gum"
return
fi
fi
if command -v gum &> /dev/null; then
HAS_GUM=true
success "Gum installed! You'll get the premium experience ✨"
else
warn "Gum installation may have failed. Continuing with standard installer."
fi
fi
}
# ═══════════════════════════════════════════════════════════════════════════
# GUM WRAPPER FUNCTIONS (with bash fallback)
# ═══════════════════════════════════════════════════════════════════════════
# Choose from a list of options
# Usage: choice=$(gum_choose "Option 1" "Option 2" "Option 3")
gum_choose() {
if [[ "$HAS_GUM" == "true" ]]; then
gum choose --cursor.foreground="#00FFFF" --selected.foreground="#00FF00" "$@"
else
# Bash fallback
local options=("$@")
local i=1
for opt in "${options[@]}"; do
echo -e " ${CYAN}[$i]${NC} $opt"
((i++))
done
echo ""
while true; do
echo -ne " ${WHITE}Enter choice [1-${#options[@]}]:${NC} "
read -r choice
if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#options[@]} )); then
echo "${options[$((choice-1))]}"
return
fi
echo -e " ${RED}Please enter a number between 1 and ${#options[@]}${NC}"
done
fi
}
# Confirm yes/no
# Usage: if gum_confirm "Are you sure?"; then ...
gum_confirm() {
local prompt="$1"
if [[ "$HAS_GUM" == "true" ]]; then
gum confirm --prompt.foreground="#FFFFFF" "$prompt"
else
echo -ne " ${WHITE}$prompt [y/N]:${NC} "
read -r response
[[ "$response" =~ ^[Yy]$ ]]
fi
}
# Get text input
# Usage: value=$(gum_input "Enter value" "placeholder")
gum_input() {
local prompt="$1"
local placeholder="$2"
if [[ "$HAS_GUM" == "true" ]]; then
gum input --placeholder "$placeholder" --prompt "$prompt: " --prompt.foreground="#00FFFF"
else
echo -ne " ${WHITE}$prompt:${NC} "
read -r value
echo "$value"
fi
}
# Get password input (hidden)
# Usage: value=$(gum_password "Enter password")
gum_password() {
local prompt="$1"
if [[ "$HAS_GUM" == "true" ]]; then
gum input --password --placeholder "••••••••••••••••" --prompt "$prompt: " --prompt.foreground="#00FFFF"
else
echo -ne " ${WHITE}$prompt:${NC} "
read -rs value
echo ""
echo "$value"
fi
}
# Show a spinner while a command runs
# Usage: gum_spin "Message" command arg1 arg2
gum_spin() {
local message="$1"
shift
if [[ "$HAS_GUM" == "true" ]]; then
gum spin --spinner dot --title "$message" -- "$@"
else
"$@" &
spinner $! "$message"
fi
}
# Style text in a box
gum_style_box() {
local text="$1"
if [[ "$HAS_GUM" == "true" ]]; then
gum style --border rounded --padding "1 2" --border-foreground "#00FFFF" "$text"
else
print_box "$text"
fi
}
# ═══════════════════════════════════════════════════════════════════════════
# ARGUMENT PARSING & MODES
# ═══════════════════════════════════════════════════════════════════════════
show_help() {
echo ""
echo -e "${CYAN}Claude OS Installer v${VERSION}${NC}"
echo ""
echo -e "${WHITE}Usage:${NC} ./setup-claude-os.sh [OPTIONS]"
echo ""
echo -e "${WHITE}Options:${NC}"
echo -e " ${CYAN}--help, -h${NC} Show this help message"
echo -e " ${CYAN}--demo${NC} Run interactive demo (no changes made)"
echo -e " ${CYAN}--dry-run${NC} Show what would be done without doing it"
echo -e " ${CYAN}--force, -f${NC} Skip confirmation prompts"
echo -e " ${CYAN}--version, -v${NC} Show version number"
echo ""
echo -e "${WHITE}Examples:${NC}"
echo -e " ${DIM}./setup-claude-os.sh${NC} # Normal installation"
echo -e " ${DIM}./setup-claude-os.sh --demo${NC} # See the beautiful UI"
echo -e " ${DIM}./setup-claude-os.sh --dry-run${NC} # Preview changes"
echo ""
}
parse_args() {
while [[ $# -gt 0 ]]; do
case $1 in
--help|-h)
show_help
exit 0
;;
--demo)
DEMO_MODE=true
shift
;;
--dry-run)
DRY_RUN=true
shift
;;
--force|-f)
FORCE_MODE=true
shift
;;
--version|-v)
echo "Claude OS Installer v${VERSION}"
exit 0
;;
*)
echo -e "${RED}Unknown option: $1${NC}"
echo "Use --help for usage information"
exit 1
;;
esac
done
}
# Run the interactive demo without making any changes
run_demo() {
clear
echo ""
echo -e "${CYAN}"
cat << 'EOF'
██████╗██╗ █████╗ ██╗ ██╗██████╗ ███████╗ ██████╗ ███████╗
██╔════╝██║ ██╔══██╗██║ ██║██╔══██╗██╔════╝ ██╔═══██╗██╔════╝
██║ ██║ ███████║██║ ██║██║ ██║█████╗ ██║ ██║███████╗
██║ ██║ ██╔══██║██║ ██║██║ ██║██╔══╝ ██║ ██║╚════██║
╚██████╗███████╗██║ ██║╚██████╔╝██████╔╝███████╗ ╚██████╔╝███████║
╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚══════╝
EOF
echo -e "${NC}"
echo -e "${DIM} DEMO MODE - No changes will be made${NC}"
echo ""
if [[ "$HAS_GUM" != "true" ]]; then
echo -e "${YELLOW} ⚠ Gum not installed - showing bash fallback version${NC}"
echo -e "${DIM} Install gum for the full experience: brew install gum${NC}"
echo ""
fi
sleep 1
# Demo: Styled Box
echo -e "${CYAN}━━━ Demo 1: Styled Boxes ━━━${NC}"
if [[ "$HAS_GUM" == "true" ]]; then
gum style --border rounded --padding "1 3" --border-foreground "#00FFFF" \
"$(gum style --foreground "#FFFFFF" --bold "Welcome to Claude OS!")" \
"" \
"$(gum style --foreground "#888888" "Your AI Memory System")"
else
print_box "Welcome to Claude OS!"
fi
echo ""
sleep 1
# Demo: Choose
echo -e "${CYAN}━━━ Demo 2: Interactive Selection ━━━${NC}"
echo ""
local choice
if [[ "$HAS_GUM" == "true" ]]; then
choice=$(gum choose --cursor.foreground="#00FFFF" --selected.foreground="#00FF00" \
"🏠 Local (Ollama) - Free, private, runs on your machine" \
"☁️ Cloud (OpenAI) - Fast, no local resources needed" \
"🔧 Custom - I'll configure it myself")
else
echo -e " ${CYAN}[1]${NC} 🏠 Local (Ollama) - Free, private"
echo -e " ${CYAN}[2]${NC} ☁️ Cloud (OpenAI) - Fast, no local resources"
echo -e " ${CYAN}[3]${NC} 🔧 Custom - Configure manually"
echo ""
echo -ne " ${WHITE}Enter choice [1-3]:${NC} "
read -r num
case $num in
1) choice="🏠 Local" ;;
2) choice="☁️ Cloud" ;;
*) choice="🔧 Custom" ;;
esac
fi
echo ""
echo -e "${GREEN}✓${NC} You selected: $choice"
echo ""
sleep 1
# Demo: Confirm
echo -e "${CYAN}━━━ Demo 3: Confirmation ━━━${NC}"
echo ""
if [[ "$HAS_GUM" == "true" ]]; then
if gum confirm --prompt.foreground="#FFFFFF" "Do you like this experience?"; then
echo -e "${GREEN}✓${NC} Awesome!"
else
echo -e "${WHITE}The bash version is nice too!${NC}"
fi
else
echo -ne " ${WHITE}Do you like this? [y/N]:${NC} "
read -r response
if [[ "$response" =~ ^[Yy]$ ]]; then
echo -e "${GREEN}✓${NC} Awesome!"
else
echo -e "${WHITE}The bash version is nice too!${NC}"
fi
fi
echo ""
sleep 1
# Demo: Spinner
echo -e "${CYAN}━━━ Demo 4: Progress Spinner ━━━${NC}"
echo ""
if [[ "$HAS_GUM" == "true" ]]; then
gum spin --spinner dot --title "Installing imaginary things..." -- sleep 2
else
sleep 2 &
spinner $! "Installing imaginary things..."
fi
echo -e "${GREEN}✓${NC} Done!"
echo ""
# Completion
echo ""
if [[ "$HAS_GUM" == "true" ]]; then
gum style --border double --padding "1 4" --border-foreground "#00FF00" --foreground "#00FF00" \
"✨ Demo Complete! ✨"
else
echo -e "${GREEN}╔═══════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ ✨ Demo Complete! ✨ ║${NC}"
echo -e "${GREEN}╚═══════════════════════════════════════╝${NC}"
fi
echo ""
echo -e "${DIM}Run without --demo to actually install Claude OS${NC}"
echo ""
exit 0
}
# Show what would be done without doing it
dry_run_msg() {
echo -e "${YELLOW} [DRY-RUN]${NC} $1"
}
# Backup a file before overwriting
backup_file() {
local file="$1"
if [[ -f "$file" ]]; then
local backup="${file}.backup.$(date +%Y%m%d_%H%M%S)"
if [[ "$DRY_RUN" == "true" ]]; then
dry_run_msg "Would backup $file → $backup"
else
cp "$file" "$backup"
success "Backed up $file → $backup"
fi
fi
}
# ═══════════════════════════════════════════════════════════════════════════
# HELPER FUNCTIONS
# ═══════════════════════════════════════════════════════════════════════════
# Print a styled box
print_box() {
local title="$1"
local width=60
local padding=$(( (width - ${#title} - 2) / 2 ))
echo ""
echo -e "${CYAN}${BOX_TL}$(printf '%*s' "$width" | tr ' ' "$BOX_H")${BOX_TR}${NC}"
echo -e "${CYAN}${BOX_V}${NC}$(printf '%*s' "$padding")${BOLD}${WHITE}$title${NC}$(printf '%*s' "$((width - padding - ${#title}))") ${CYAN}${BOX_V}${NC}"
echo -e "${CYAN}${BOX_BL}$(printf '%*s' "$width" | tr ' ' "$BOX_H")${BOX_BR}${NC}"
echo ""
}
# Print a section header
print_section() {
echo ""
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${BLUE} $1${NC}"
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
}
# Animated spinner
spinner() {
local pid=$1
local message=$2
local spin='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
local i=0
while kill -0 $pid 2>/dev/null; do
i=$(( (i + 1) % 10 ))
printf "\r${CYAN} ${spin:$i:1}${NC} ${message}"
sleep 0.1
done
printf "\r"
}
# Success message
success() {
echo -e "${GREEN} ✓${NC} $1"
}
# Warning message
warn() {
echo -e "${YELLOW} ⚠${NC} $1"
}
# Error message
error() {
echo -e "${RED} ✗${NC} $1"
}
# Info message
info() {
echo -e "${CYAN} ℹ${NC} $1"
}
# Progress bar
progress_bar() {
local current=$1
local total=$2
local width=40
local percentage=$((current * 100 / total))
local filled=$((current * width / total))
local empty=$((width - filled))
printf "\r ${CYAN}["
printf "%${filled}s" | tr ' ' '█'
printf "%${empty}s" | tr ' ' '░'
printf "]${NC} ${percentage}%%"
}
# Detect OS
detect_os() {
if [[ "$OSTYPE" == "darwin"* ]]; then
OS="macos"
PKG_MANAGER="brew"
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
OS="linux"
if command -v apt-get &> /dev/null; then
PKG_MANAGER="apt"
elif command -v dnf &> /dev/null; then
PKG_MANAGER="dnf"
elif command -v yum &> /dev/null; then
PKG_MANAGER="yum"
elif command -v pacman &> /dev/null; then
PKG_MANAGER="pacman"
else
PKG_MANAGER="unknown"
fi
else
OS="unknown"
PKG_MANAGER="unknown"
fi
}
# ═══════════════════════════════════════════════════════════════════════════
# BANNER
# ═══════════════════════════════════════════════════════════════════════════
show_banner() {
clear
echo ""
echo -e "${CYAN}"
cat << 'EOF'
██████╗██╗ █████╗ ██╗ ██╗██████╗ ███████╗ ██████╗ ███████╗
██╔════╝██║ ██╔══██╗██║ ██║██╔══██╗██╔════╝ ██╔═══██╗██╔════╝
██║ ██║ ███████║██║ ██║██║ ██║█████╗ ██║ ██║███████╗
██║ ██║ ██╔══██║██║ ██║██║ ██║██╔══╝ ██║ ██║╚════██║
╚██████╗███████╗██║ ██║╚██████╔╝██████╔╝███████╗ ╚██████╔╝███████║
╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚══════╝
EOF
echo -e "${NC}"
echo -e "${DIM} Your AI Memory System • v${VERSION}${NC}"
echo ""
echo -e "${WHITE} Claude CLI + Claude OS = Invincible! 🚀${NC}"
echo ""
}
# ═══════════════════════════════════════════════════════════════════════════
# PROVIDER SELECTION
# ═══════════════════════════════════════════════════════════════════════════
select_provider() {
if [[ "$HAS_GUM" == "true" ]]; then
# Beautiful gum version
echo ""
gum style --border rounded --padding "1 2" --border-foreground "#00FFFF" \
"$(gum style --foreground "#FFFFFF" --bold "How would you like to power Claude OS?")"
echo ""
local choice=$(gum choose --cursor.foreground="#00FFFF" --selected.foreground="#00FF00" \
"🏠 Local (Ollama) - Free, private, runs on your machine" \
"☁️ Cloud (OpenAI) - Fast, no local resources needed" \
"🔧 Custom - I'll configure it myself")
case "$choice" in
*"Local"*) PROVIDER="local" ;;
*"Cloud"*) PROVIDER="openai" ;;
*"Custom"*) PROVIDER="custom" ;;
esac
else
# Bash fallback
print_box "Choose Your Setup"
echo -e " ${WHITE}How would you like to power Claude OS?${NC}"
echo ""
echo -e " ${CYAN}[1]${NC} ${GREEN}🏠 Local (Ollama)${NC}"
echo -e " ${DIM}Free, private, runs on your machine${NC}"
echo -e " ${DIM}Best for: Privacy-focused users, offline use${NC}"
echo ""
echo -e " ${CYAN}[2]${NC} ${BLUE}☁️ Cloud (OpenAI)${NC}"
echo -e " ${DIM}Fast, no local resources needed${NC}"
echo -e " ${DIM}Best for: Quick setup, Linux servers${NC}"
echo ""
echo -e " ${CYAN}[3]${NC} ${MAGENTA}🔧 Custom${NC}"
echo -e " ${DIM}I'll configure it myself${NC}"
echo ""
while true; do
echo -ne " ${WHITE}Enter choice [1-3]:${NC} "
read -r choice
case $choice in
1) PROVIDER="local"; break ;;
2) PROVIDER="openai"; break ;;
3) PROVIDER="custom"; break ;;
*) echo -e " ${RED}Please enter 1, 2, or 3${NC}" ;;
esac
done
fi
# Show selection confirmation
echo ""
case "$PROVIDER" in
local) success "Selected: Local (Ollama)" ;;
openai) success "Selected: Cloud (OpenAI)" ;;
custom) success "Selected: Custom configuration" ;;
esac
}
# ═══════════════════════════════════════════════════════════════════════════
# MODEL SIZE SELECTION (for local installs)
# ═══════════════════════════════════════════════════════════════════════════
select_model_size() {
if [[ "$HAS_GUM" == "true" ]]; then
# Beautiful gum version
echo ""
gum style --border rounded --padding "1 2" --border-foreground "#00FFFF" \
"$(gum style --foreground "#FFFFFF" --bold "Choose your model size")"
echo ""
local choice=$(gum choose --cursor.foreground="#00FFFF" --selected.foreground="#00FF00" \
"💨 Lite (Recommended) - llama3.2:3b - 2GB download, ~4GB RAM" \
"🚀 Full - llama3.1:8b - 4.7GB download, ~8GB RAM")
case "$choice" in
*"Lite"*) LLM_MODEL="$DEFAULT_LLM_MODEL" ;;
*"Full"*) LLM_MODEL="$FULL_LLM_MODEL" ;;
esac
else
# Bash fallback
print_box "Choose Model Size"
echo -e " ${WHITE}Select your local model:${NC}"
echo ""
echo -e " ${CYAN}[1]${NC} ${GREEN}💨 Lite${NC} ${DIM}(Recommended)${NC}"
echo -e " ${WHITE}llama3.2:3b${NC} - 2GB download, ~4GB RAM"
echo -e " ${DIM}Fast download, works on most machines${NC}"
echo ""
echo -e " ${CYAN}[2]${NC} ${BLUE}🚀 Full${NC}"
echo -e " ${WHITE}llama3.1:8b${NC} - 4.7GB download, ~8GB RAM"
echo -e " ${DIM}Better quality, needs more resources${NC}"
echo ""
while true; do
echo -ne " ${WHITE}Enter choice [1-2]:${NC} "
read -r choice
case $choice in
1) LLM_MODEL="$DEFAULT_LLM_MODEL"; break ;;
2) LLM_MODEL="$FULL_LLM_MODEL"; break ;;
*) echo -e " ${RED}Please enter 1 or 2${NC}" ;;
esac
done
fi
EMBED_MODEL="$DEFAULT_EMBED_MODEL"
# Show selection confirmation
echo ""
success "Selected: $LLM_MODEL"
}
# ═══════════════════════════════════════════════════════════════════════════
# OPENAI CONFIGURATION
# ═══════════════════════════════════════════════════════════════════════════
configure_openai() {
if [[ "$HAS_GUM" == "true" ]]; then
# Beautiful gum version
echo ""
gum style --border rounded --padding "1 2" --border-foreground "#00FFFF" \
"$(gum style --foreground "#FFFFFF" --bold "OpenAI Configuration")"
echo ""
echo -e " ${DIM}Get your API key at https://platform.openai.com/api-keys${NC}"
echo ""
OPENAI_API_KEY=$(gum input --password --placeholder "sk-..." --prompt "API Key: " --prompt.foreground="#00FFFF")
else
# Bash fallback
print_box "OpenAI Configuration"
echo -e " ${WHITE}Enter your OpenAI API key:${NC}"
echo -e " ${DIM}(Get one at https://platform.openai.com/api-keys)${NC}"
echo ""
echo -ne " ${WHITE}API Key:${NC} "
read -rs OPENAI_API_KEY
echo ""
fi
if [[ -z "$OPENAI_API_KEY" ]]; then
error "API key cannot be empty"
exit 1
fi
# Validate key format
if [[ ! "$OPENAI_API_KEY" =~ ^sk- ]]; then
warn "API key doesn't start with 'sk-' - are you sure it's correct?"
fi
echo ""
success "API key saved"
}
# ═══════════════════════════════════════════════════════════════════════════
# PYTHON SETUP
# ═══════════════════════════════════════════════════════════════════════════
setup_python() {
print_section "Setting Up Python Environment"
# Find compatible Python
PYTHON_CMD=""
if command -v python3.12 &> /dev/null; then
PYTHON_CMD="python3.12"
elif command -v python3.11 &> /dev/null; then
PYTHON_CMD="python3.11"
elif command -v python3 &> /dev/null; then
local ver=$(python3 --version 2>&1 | cut -d' ' -f2 | cut -d'.' -f1,2)
if [[ "$ver" == "3.11" || "$ver" == "3.12" ]]; then
PYTHON_CMD="python3"
fi
fi
if [[ -z "$PYTHON_CMD" ]]; then
warn "Python 3.11 or 3.12 not found"
echo ""
# Offer to install Python automatically
local install_python=""
if [[ "$HAS_GUM" == "true" ]]; then
install_python=$(gum choose "Yes, install Python 3.12 for me" "No, I'll install it myself" --header "Install Python 3.12 automatically?")
[[ "$install_python" == "Yes"* ]] && install_python="y"
else
echo -ne " ${WHITE}Install Python 3.12 automatically? [Y/n]:${NC} "
read -r install_python
install_python=${install_python:-y}
fi
if [[ "$install_python" =~ ^[Yy]$ ]]; then
info "Installing Python 3.12..."
if [[ "$OS" == "macos" ]]; then
if command -v brew &> /dev/null; then
brew install python@3.12 &>/dev/null &
spinner $! "Installing Python 3.12 via Homebrew..."
else
error "Homebrew not found. Install from https://brew.sh first."
exit 1
fi
elif [[ "$OS" == "linux" ]]; then
case "$PKG_MANAGER" in
apt)
info "Adding deadsnakes PPA for Python 3.12 (this may take a minute)..."
sudo apt-get install -y software-properties-common
sudo add-apt-repository -y ppa:deadsnakes/ppa
info "Updating package lists..."
sudo apt-get update
info "Installing Python 3.12..."
sudo apt-get install -y python3.12 python3.12-venv python3.12-dev
;;
dnf)
sudo dnf install -y -q python3.12 python3.12-devel &>/dev/null &
spinner $! "Installing Python 3.12 via dnf..."
;;
yum)
sudo yum install -y -q python3.12 python3.12-devel &>/dev/null &
spinner $! "Installing Python 3.12 via yum..."
;;
pacman)
sudo pacman -S --noconfirm python &>/dev/null &
spinner $! "Installing Python via pacman..."
;;
*)
error "Unknown package manager. Please install Python 3.12 manually."
exit 1
;;
esac
fi
# Re-check for Python after install
if command -v python3.12 &> /dev/null; then
PYTHON_CMD="python3.12"
success "Python 3.12 installed successfully"
else
error "Python installation failed. Please install manually."
exit 1
fi
else
echo ""
echo -e " ${WHITE}Install Python manually:${NC}"
echo -e " macOS: ${CYAN}brew install python@3.12${NC}"
echo -e " Ubuntu: ${CYAN}sudo add-apt-repository ppa:deadsnakes/ppa && sudo apt install python3.12 python3.12-venv${NC}"
echo -e " Fedora: ${CYAN}sudo dnf install python3.12${NC}"
exit 1
fi
fi
local py_version=$($PYTHON_CMD --version | cut -d' ' -f2)
success "Found Python $py_version"
# Create virtual environment
if [[ ! -d "${CLAUDE_OS_DIR}/venv" ]]; then
info "Creating virtual environment..."
$PYTHON_CMD -m venv "${CLAUDE_OS_DIR}/venv" &
spinner $! "Creating virtual environment..."
success "Virtual environment created"
else
success "Virtual environment exists"
fi
# Activate and install dependencies
source "${CLAUDE_OS_DIR}/venv/bin/activate"
info "Installing dependencies..."
pip install --quiet --upgrade pip setuptools wheel 2>/dev/null &
spinner $! "Upgrading pip..."
pip install --quiet -r "${CLAUDE_OS_DIR}/requirements.txt" 2>/dev/null &
spinner $! "Installing dependencies (this may take a minute)..."
success "Dependencies installed"
}
# ═══════════════════════════════════════════════════════════════════════════
# OLLAMA SETUP
# ═══════════════════════════════════════════════════════════════════════════
setup_ollama() {
print_section "Setting Up Ollama"
# Check if Ollama is installed
if command -v ollama &> /dev/null; then
success "Ollama already installed"
else
info "Installing Ollama..."
if [[ "$OS" == "macos" ]]; then
if command -v brew &> /dev/null; then
brew install ollama &>/dev/null &
spinner $! "Installing via Homebrew..."
else
error "Homebrew required. Install from https://brew.sh"
exit 1
fi
elif [[ "$OS" == "linux" ]]; then
curl -fsSL https://ollama.ai/install.sh | sh &>/dev/null &
spinner $! "Installing Ollama..."
fi
success "Ollama installed"
fi
# Start Ollama if not running
if ! curl -s http://localhost:11434/api/tags &>/dev/null; then
info "Starting Ollama..."
if [[ "$OS" == "macos" ]]; then
brew services start ollama &>/dev/null || ollama serve &>/dev/null &
else
ollama serve &>/dev/null &
fi
sleep 3
if curl -s http://localhost:11434/api/tags &>/dev/null; then
success "Ollama started"
else
warn "Ollama may need manual start: ollama serve"
fi
else
success "Ollama is running"
fi
# Download models
echo ""
info "Downloading AI models..."
echo ""
# Download LLM model
if curl -s http://localhost:11434/api/tags | grep -q "\"name\":\"${LLM_MODEL}\""; then
success "Model ${LLM_MODEL} ready"
else
echo -e " ${CYAN}Downloading ${LLM_MODEL}...${NC}"
echo -e " ${DIM}(This may take a few minutes on first install)${NC}"
ollama pull "$LLM_MODEL" 2>&1 | while read line; do
if [[ "$line" =~ ([0-9]+)% ]]; then
progress_bar "${BASH_REMATCH[1]}" 100
fi
done
echo ""
success "Model ${LLM_MODEL} downloaded"
fi
# Download embedding model
if curl -s http://localhost:11434/api/tags | grep -q "\"name\":\"${EMBED_MODEL}\""; then
success "Model ${EMBED_MODEL} ready"
else
echo -e " ${CYAN}Downloading ${EMBED_MODEL}...${NC}"
ollama pull "$EMBED_MODEL" 2>&1 | while read line; do
if [[ "$line" =~ ([0-9]+)% ]]; then
progress_bar "${BASH_REMATCH[1]}" 100
fi
done
echo ""
success "Model ${EMBED_MODEL} downloaded"
fi
}
# ═══════════════════════════════════════════════════════════════════════════
# REDIS SETUP
# ═══════════════════════════════════════════════════════════════════════════
setup_redis() {
print_section "Setting Up Redis"
if command -v redis-cli &> /dev/null; then
success "Redis already installed"
else
info "Installing Redis..."
if [[ "$OS" == "macos" ]]; then
brew install redis &>/dev/null &
spinner $! "Installing via Homebrew..."
elif [[ "$OS" == "linux" ]]; then
case "$PKG_MANAGER" in
apt) sudo apt-get update -qq && sudo apt-get install -y -qq redis-server &>/dev/null & ;;
dnf) sudo dnf install -y -q redis &>/dev/null & ;;
yum) sudo yum install -y -q redis &>/dev/null & ;;
pacman) sudo pacman -S --noconfirm --quiet redis &>/dev/null & ;;
esac
spinner $! "Installing Redis..."
fi
success "Redis installed"
fi
# Start Redis if not running
if redis-cli ping &>/dev/null; then
success "Redis is running"
else
info "Starting Redis..."
if [[ "$OS" == "macos" ]]; then
brew services start redis &>/dev/null || true
else
redis-server --daemonize yes &>/dev/null || true
fi
sleep 1
if redis-cli ping &>/dev/null; then
success "Redis started"
else
warn "Redis may need manual start"
fi
fi
}
# ═══════════════════════════════════════════════════════════════════════════
# CLAUDE CODE INTEGRATION
# ═══════════════════════════════════════════════════════════════════════════
setup_claude_integration() {
print_section "Integrating with Claude Code"
# Create directories
mkdir -p "${USER_CLAUDE_DIR}/commands"
mkdir -p "${USER_CLAUDE_DIR}/skills"
mkdir -p "${USER_CLAUDE_DIR}/mcp-servers"
mkdir -p "${CLAUDE_OS_DIR}/data"
mkdir -p "${CLAUDE_OS_DIR}/logs"
success "Created directories"
# Symlink commands
local cmd_count=0
for cmd_file in "${TEMPLATES_DIR}"/commands/*.md; do
if [[ -f "$cmd_file" ]]; then
local cmd_name=$(basename "$cmd_file")
local dest="${USER_CLAUDE_DIR}/commands/${cmd_name}"
rm -f "$dest" 2>/dev/null
ln -s "$cmd_file" "$dest"
cmd_count=$((cmd_count + 1))
fi
done
success "Linked ${cmd_count} commands"
# Symlink skills
local skill_count=0
for skill_dir in "${TEMPLATES_DIR}"/skills/*/; do
if [[ -d "$skill_dir" ]]; then
local skill_name=$(basename "$skill_dir")
local dest="${USER_CLAUDE_DIR}/skills/${skill_name}"
rm -rf "$dest" 2>/dev/null
ln -s "$skill_dir" "$dest"
skill_count=$((skill_count + 1))
fi
done
success "Linked ${skill_count} skills"
# NOTE: MCP server is configured per-project when running /claude-os-init
# Claude Code stores MCP configs in ~/.claude.json per-project, not in settings.json
# The /claude-os-init command runs: claude mcp add --transport stdio code-forge ...
info "MCP server will be configured per-project via /claude-os-init"
}
# ═══════════════════════════════════════════════════════════════════════════
# CREATE CONFIGURATION FILE
# ═══════════════════════════════════════════════════════════════════════════
create_config() {
print_section "Creating Configuration"
local config_file="${CLAUDE_OS_DIR}/.env"
local json_config="${CLAUDE_OS_DIR}/claude-os-config.json"
# Backup existing configs before overwriting
backup_file "$config_file"
backup_file "$json_config"
if [[ "$DRY_RUN" == "true" ]]; then
dry_run_msg "Would create $config_file with:"
dry_run_msg " CLAUDE_OS_PROVIDER=${PROVIDER}"
dry_run_msg " LLM_MODEL=${LLM_MODEL:-gpt-4o-mini}"
dry_run_msg " EMBEDDING_MODEL=${EMBED_MODEL:-text-embedding-3-small}"
dry_run_msg "Would create $json_config"
return
fi
cat > "$config_file" << EOF
# Claude OS Configuration
# Generated by setup-claude-os.sh on $(date)
# Provider: local or openai
CLAUDE_OS_PROVIDER=${PROVIDER}
# LLM Settings
LLM_MODEL=${LLM_MODEL:-gpt-4o-mini}
EMBEDDING_MODEL=${EMBED_MODEL:-text-embedding-3-small}
# Ollama Settings (for local provider)
OLLAMA_HOST=http://localhost:11434
# OpenAI Settings (for cloud provider)
OPENAI_API_KEY=${OPENAI_API_KEY:-}
# Server Settings
CLAUDE_OS_HOST=0.0.0.0
CLAUDE_OS_PORT=8051
# Database
CLAUDE_OS_DB_PATH=./data/claude-os.db
EOF
success "Created configuration file"
# Also create the JSON config for backwards compatibility
cat > "${CLAUDE_OS_DIR}/claude-os-config.json" << EOF
{
"provider": "${PROVIDER}",
"llm_model": "${LLM_MODEL:-gpt-4o-mini}",
"embed_model": "${EMBED_MODEL:-text-embedding-3-small}",
"version": "${VERSION}",
"installed_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
}
EOF
}
# ═══════════════════════════════════════════════════════════════════════════
# COMPLETION
# ═══════════════════════════════════════════════════════════════════════════
show_completion() {
echo ""
if [[ "$HAS_GUM" == "true" ]]; then
# Beautiful gum completion
gum style --border double --padding "1 4" --border-foreground "#00FF00" --foreground "#00FF00" \
"✨ Claude OS is ready! ✨"
else
echo -e "${GREEN}"
cat << 'EOF'
╔═══════════════════════════════════════════════════════════════════╗
║ ║
║ ✨ Claude OS is ready! ✨ ║
║ ║
╚═══════════════════════════════════════════════════════════════════╝
EOF
echo -e "${NC}"
fi
echo ""
echo -e " ${WHITE}What was set up:${NC}"
echo ""
[[ -d "${CLAUDE_OS_DIR}/venv" ]] && echo -e " ${GREEN}✓${NC} Python environment"
[[ "$PROVIDER" == "local" ]] && echo -e " ${GREEN}✓${NC} Ollama with ${LLM_MODEL}"
[[ "$PROVIDER" == "local" ]] && echo -e " ${GREEN}✓${NC} Embedding model (${EMBED_MODEL})"
[[ "$PROVIDER" == "openai" ]] && echo -e " ${GREEN}✓${NC} OpenAI API configured"
command -v redis-cli &>/dev/null && echo -e " ${GREEN}✓${NC} Redis cache"
[[ -d "${USER_CLAUDE_DIR}/commands" ]] && echo -e " ${GREEN}✓${NC} Claude Code commands"
[[ -d "${USER_CLAUDE_DIR}/skills" ]] && echo -e " ${GREEN}✓${NC} Claude Code skills"
[[ -f "${USER_CLAUDE_DIR}/settings.json" ]] && echo -e " ${GREEN}✓${NC} MCP server configured"
echo ""
echo -e " ${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
echo -e " ${WHITE}Next Steps:${NC}"
echo ""
echo -e " ${CYAN}1.${NC} Start Claude OS:"
echo -e " ${DIM}./start_all_services.sh${NC}"
echo ""
echo -e " ${CYAN}2.${NC} In your project, initialize Claude OS:"
echo -e " ${DIM}/claude-os-init${NC}"
echo ""
echo -e " ${CYAN}3.${NC} Start a session:"
echo -e " ${DIM}/claude-os-session start \"working on feature X\"${NC}"
echo ""
echo -e " ${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
echo -e " ${DIM}Documentation: README.md${NC}"
echo -e " ${DIM}Issues: https://github.com/brobertsaz/claude-os/issues${NC}"
echo ""
echo -e " ${WHITE}Happy coding! 🚀${NC}"
echo ""
}
# ═══════════════════════════════════════════════════════════════════════════
# MAIN
# ═══════════════════════════════════════════════════════════════════════════
main() {
# Parse command line arguments first
parse_args "$@"
# Detect OS (needed for gum install and demo)
detect_os
# Handle demo mode - runs demo and exits
if [[ "$DEMO_MODE" == "true" ]]; then
run_demo
exit 0
fi
# Show banner
show_banner
# Show dry-run notice if applicable
if [[ "$DRY_RUN" == "true" ]]; then
echo ""
echo -e "${YELLOW}┌────────────────────────────────────────────────────────────────┐${NC}"
echo -e "${YELLOW}│${NC} ${WHITE}🔍 DRY-RUN MODE${NC} ${YELLOW}│${NC}"
echo -e "${YELLOW}│${NC} ${YELLOW}│${NC}"
echo -e "${YELLOW}│${NC} ${DIM}No changes will be made. This shows what would happen.${NC} ${YELLOW}│${NC}"
echo -e "${YELLOW}└────────────────────────────────────────────────────────────────┘${NC}"
echo ""
sleep 1
fi
# Offer to install gum for enhanced experience (skip in dry-run)
if [[ "$DRY_RUN" != "true" ]]; then
offer_gum_install
fi
# Provider selection
select_provider
# Provider-specific setup
if [[ "$PROVIDER" == "local" ]]; then
select_model_size
elif [[ "$PROVIDER" == "openai" ]]; then
configure_openai
LLM_MODEL="gpt-4o-mini"
EMBED_MODEL="text-embedding-3-small"
elif [[ "$PROVIDER" == "custom" ]]; then
info "Custom setup - you'll need to configure .env manually"
LLM_MODEL=""
EMBED_MODEL=""
fi
# Common setup
if [[ "$DRY_RUN" == "true" ]]; then
print_section "Python Environment"
dry_run_msg "Would create/use virtual environment at ${CLAUDE_OS_DIR}/venv"
dry_run_msg "Would install dependencies from requirements.txt"
else
setup_python
fi
# Provider-specific dependencies
if [[ "$PROVIDER" == "local" ]]; then
if [[ "$DRY_RUN" == "true" ]]; then
print_section "Ollama Setup"
dry_run_msg "Would install Ollama if not present"
dry_run_msg "Would download model: ${LLM_MODEL}"
dry_run_msg "Would download model: ${EMBED_MODEL}"
else
setup_ollama
fi
fi
# Optional: Redis (useful for caching)
if [[ "$DRY_RUN" == "true" ]]; then
print_section "Redis Setup"
dry_run_msg "Would install Redis if not present"
dry_run_msg "Would start Redis service"
else
setup_redis
fi
# Claude Code integration
if [[ "$DRY_RUN" == "true" ]]; then
print_section "Claude Code Integration"
dry_run_msg "Would create directories: ~/.claude/commands, ~/.claude/skills"
dry_run_msg "Would symlink commands from ${TEMPLATES_DIR}/commands/"
dry_run_msg "Would symlink skills from ${TEMPLATES_DIR}/skills/"
dry_run_msg "Would configure MCP server in ~/.claude/settings.json"
else
setup_claude_integration
fi
# Create configuration
create_config
# Show completion
if [[ "$DRY_RUN" == "true" ]]; then
echo ""
echo -e "${YELLOW}┌────────────────────────────────────────────────────────────────┐${NC}"
echo -e "${YELLOW}│${NC} ${WHITE}✓ Dry-run complete!${NC} ${YELLOW}│${NC}"
echo -e "${YELLOW}│${NC} ${YELLOW}│${NC}"
echo -e "${YELLOW}│${NC} ${DIM}Run without --dry-run to perform the actual installation.${NC} ${YELLOW}│${NC}"
echo -e "${YELLOW}└────────────────────────────────────────────────────────────────┘${NC}"
echo ""
else
show_completion
fi
}
# Run main
main "$@"