install.sh•69.8 kB
#!/bin/bash
# VoiceMode Universal Installer
# Usage: curl -O https://getvoicemode.com/install.sh && bash install.sh
# Parse command line arguments
NON_INTERACTIVE=false
CI_MODE=false
show_help() {
cat <<EOF
VoiceMode Universal Installer
Usage: $0 [OPTIONS]
Options:
-h, --help Show this help message
-d, --debug Enable debug output
-n, --non-interactive Run without prompts (assumes yes to all)
--ci CI mode (non-interactive + skip audio/API checks)
Environment variables:
VOICEMODE_INSTALL_DEBUG=true Enable debug output
DEBUG=true Enable debug output
CI=true Enables CI mode automatically
Examples:
# Normal installation
curl -O https://getvoicemode.com/install.sh && bash install.sh
# Debug mode
VOICEMODE_INSTALL_DEBUG=true ./install.sh
./install.sh --debug
# Non-interactive mode
./install.sh --non-interactive
# CI mode
./install.sh --ci
EOF
exit 0
}
# Process arguments
while [[ $# -gt 0 ]]; do
case $1 in
-h | --help)
show_help
;;
-d | --debug)
export VOICEMODE_INSTALL_DEBUG=true
shift
;;
-n | --non-interactive)
NON_INTERACTIVE=true
shift
;;
--ci)
CI_MODE=true
NON_INTERACTIVE=true
shift
;;
*)
echo "Unknown option: $1"
echo "Use --help for usage information"
exit 1
;;
esac
done
# Support DEBUG=true as well as VOICEMODE_INSTALL_DEBUG=true
if [[ "${DEBUG:-}" == "true" ]]; then
export VOICEMODE_INSTALL_DEBUG=true
fi
# Detect CI environment
if [[ "${CI:-}" == "true" ]] || [[ "${GITHUB_ACTIONS:-}" == "true" ]]; then
CI_MODE=true
NON_INTERACTIVE=true
fi
# Reattach stdin to terminal for interactive prompts when run via curl | bash
# Only attempt this if /dev/tty exists and is accessible (skip in CI mode)
if [ ! -t 0 ] && [ -e /dev/tty ] && [ -r /dev/tty ] && [ "$CI_MODE" != "true" ]; then
exec </dev/tty 2>/dev/null || true
fi
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
ORANGE='\033[38;5;208m' # Claude Code orange
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m' # No Color
# Global variables
OS=""
ARCH=""
HOMEBREW_INSTALLED=false
XCODE_TOOLS_INSTALLED=false
IS_WSL=false
print_step() {
echo -e "${BLUE}🔧 $1${NC}"
}
print_success() {
echo -e "${GREEN}✅ $1${NC}"
}
print_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
print_error() {
echo -e "${RED}❌ $1${NC}"
exit 1
}
detect_os() {
print_step "Detecting operating system..."
# Check if running in WSL
# More robust WSL detection to avoid false positives
# Note: WSLInterop-late is used on newer systems with systemd
if [[ -n "$WSL_DISTRO_NAME" ]] || [[ -n "$WSL_INTEROP" ]] || [[ -f "/proc/sys/fs/binfmt_misc/WSLInterop" ]] || [[ -f "/proc/sys/fs/binfmt_misc/WSLInterop-late" ]]; then
IS_WSL=true
print_warning "Running in WSL2 - additional audio setup may be required"
elif grep -qi "microsoft.*WSL" /proc/version 2>/dev/null; then
# Only match if both "microsoft" AND "WSL" are present (case-insensitive)
IS_WSL=true
print_warning "Running in WSL2 - additional audio setup may be required"
fi
if [[ "$OSTYPE" == "darwin"* ]]; then
OS="macos"
ARCH=$(uname -m)
local macos_version=$(sw_vers -productVersion)
print_success "Detected macOS $macos_version on $ARCH"
elif [[ -f /etc/fedora-release ]]; then
OS="fedora"
ARCH=$(uname -m)
local fedora_version=$(cat /etc/fedora-release | grep -oP '\d+' | head -1)
print_success "Detected Fedora $fedora_version on $ARCH$([[ "$IS_WSL" == "true" ]] && echo " (WSL2)" || echo "")"
elif [[ -f /etc/os-release ]]; then
source /etc/os-release
if [[ "$ID" == "ubuntu" ]] || [[ "$ID_LIKE" == *"ubuntu"* ]] || [[ "$ID" == "debian" ]] || [[ "$ID_LIKE" == *"debian"* ]]; then
OS="debian" # Use debian for all Debian-based distros (Ubuntu, Debian, etc - they use apt)
ARCH=$(uname -m)
local distro_name="$NAME"
print_success "Detected $distro_name $VERSION_ID on $ARCH$([[ "$IS_WSL" == "true" ]] && echo " (WSL2)" || echo "")"
elif [[ "$ID" == "fedora" ]]; then
OS="fedora"
ARCH=$(uname -m)
print_success "Detected Fedora $VERSION_ID on $ARCH$([[ "$IS_WSL" == "true" ]] && echo " (WSL2)" || echo "")"
else
print_error "Unsupported Linux distribution: $ID. Currently only Ubuntu, Debian and Fedora are supported."
fi
else
print_error "Unsupported operating system: $OSTYPE"
fi
}
check_xcode_tools() {
print_step "Checking for Xcode Command Line Tools..."
if xcode-select -p >/dev/null 2>&1; then
XCODE_TOOLS_INSTALLED=true
print_success "Xcode Command Line Tools are already installed"
else
print_warning "Xcode Command Line Tools not found"
fi
}
install_xcode_tools() {
if [ "$XCODE_TOOLS_INSTALLED" = false ]; then
print_step "Installing Xcode Command Line Tools..."
echo "This will open a dialog to install Xcode Command Line Tools."
echo "Please follow the prompts and re-run this installer after installation completes."
xcode-select --install
print_warning "Please complete the Xcode Command Line Tools installation and re-run this installer."
exit 0
fi
}
check_homebrew() {
print_step "Checking for Homebrew..."
if command -v brew >/dev/null 2>&1; then
HOMEBREW_INSTALLED=true
print_success "Homebrew is already installed"
else
print_warning "Homebrew not found"
fi
}
collect_system_info() {
print_step "Collecting system information..."
# Initialize global variables for system info
SYSTEM_INFO_OS=""
SYSTEM_INFO_OS_VERSION=""
SYSTEM_INFO_ARCH=""
SYSTEM_INFO_SHELL=""
SYSTEM_INFO_SHELL_VERSION=""
SYSTEM_INFO_SHELL_RC_FILE=""
SYSTEM_INFO_NODE_VERSION=""
SYSTEM_INFO_NPM_VERSION=""
SYSTEM_INFO_PYTHON_VERSION=""
SYSTEM_INFO_PIP_VERSION=""
SYSTEM_INFO_CURL_VERSION=""
SYSTEM_INFO_GIT_VERSION=""
SYSTEM_INFO_FFMPEG_INSTALLED=""
SYSTEM_INFO_DOCKER_INSTALLED=""
SYSTEM_INFO_HOMEBREW_VERSION=""
SYSTEM_INFO_APT_INSTALLED=""
SYSTEM_INFO_TERMINAL_APP=""
SYSTEM_INFO_USER_HOME="$HOME"
SYSTEM_INFO_CURRENT_DIR="$(pwd)"
SYSTEM_INFO_DISK_AVAILABLE=""
# OS and Architecture (already detected)
SYSTEM_INFO_OS="$OS"
SYSTEM_INFO_ARCH="$ARCH"
# OS Version details
if [[ "$OS" == "macos" ]]; then
SYSTEM_INFO_OS_VERSION="$(sw_vers -productVersion 2>/dev/null || echo 'unknown')"
elif [[ "$OS" == "linux" ]]; then
if [ -f /etc/os-release ]; then
. /etc/os-release
SYSTEM_INFO_OS_VERSION="${PRETTY_NAME:-${NAME:-unknown}}"
else
SYSTEM_INFO_OS_VERSION="$(uname -v 2>/dev/null || echo 'unknown')"
fi
else
SYSTEM_INFO_OS_VERSION="$(uname -v 2>/dev/null || echo 'unknown')"
fi
# Default shell and version
SYSTEM_INFO_SHELL="${SHELL:-/bin/bash}"
if command -v "$SYSTEM_INFO_SHELL" >/dev/null 2>&1; then
if [[ "$SYSTEM_INFO_SHELL" == *"zsh"* ]]; then
SYSTEM_INFO_SHELL_VERSION="$($SYSTEM_INFO_SHELL --version 2>/dev/null | head -n1 || echo 'unknown')"
elif [[ "$SYSTEM_INFO_SHELL" == *"bash"* ]]; then
SYSTEM_INFO_SHELL_VERSION="$($SYSTEM_INFO_SHELL --version 2>/dev/null | head -n1 || echo 'unknown')"
elif [[ "$SYSTEM_INFO_SHELL" == *"fish"* ]]; then
SYSTEM_INFO_SHELL_VERSION="$($SYSTEM_INFO_SHELL --version 2>/dev/null || echo 'unknown')"
else
SYSTEM_INFO_SHELL_VERSION="unknown"
fi
fi
# Determine shell RC file
if [[ "$SYSTEM_INFO_SHELL" == *"zsh"* ]]; then
SYSTEM_INFO_SHELL_RC_FILE="$HOME/.zshrc"
elif [[ "$SYSTEM_INFO_SHELL" == *"bash"* ]]; then
if [[ "$OS" == "macos" ]]; then
# macOS uses .bash_profile for login shells
SYSTEM_INFO_SHELL_RC_FILE="$HOME/.bash_profile"
else
SYSTEM_INFO_SHELL_RC_FILE="$HOME/.bashrc"
fi
elif [[ "$SYSTEM_INFO_SHELL" == *"fish"* ]]; then
SYSTEM_INFO_SHELL_RC_FILE="$HOME/.config/fish/config.fish"
else
# Fallback to .profile
SYSTEM_INFO_SHELL_RC_FILE="$HOME/.profile"
fi
# Check if RC file exists, if not try alternatives
if [[ ! -f "$SYSTEM_INFO_SHELL_RC_FILE" ]]; then
if [[ "$SYSTEM_INFO_SHELL" == *"bash"* ]]; then
# Try alternative bash files
for rcfile in "$HOME/.bash_profile" "$HOME/.bashrc" "$HOME/.profile"; do
if [[ -f "$rcfile" ]]; then
SYSTEM_INFO_SHELL_RC_FILE="$rcfile"
break
fi
done
fi
fi
# Node.js and npm versions
if command -v node >/dev/null 2>&1; then
SYSTEM_INFO_NODE_VERSION="$(node --version 2>/dev/null || echo 'not installed')"
else
SYSTEM_INFO_NODE_VERSION="not installed"
fi
if command -v npm >/dev/null 2>&1; then
SYSTEM_INFO_NPM_VERSION="$(npm --version 2>/dev/null || echo 'not installed')"
else
SYSTEM_INFO_NPM_VERSION="not installed"
fi
# Python and pip versions
if command -v python3 >/dev/null 2>&1; then
SYSTEM_INFO_PYTHON_VERSION="$(python3 --version 2>&1 | cut -d' ' -f2 || echo 'not installed')"
elif command -v python >/dev/null 2>&1; then
SYSTEM_INFO_PYTHON_VERSION="$(python --version 2>&1 | cut -d' ' -f2 || echo 'not installed')"
else
SYSTEM_INFO_PYTHON_VERSION="not installed"
fi
if command -v pip3 >/dev/null 2>&1; then
SYSTEM_INFO_PIP_VERSION="$(pip3 --version 2>/dev/null | cut -d' ' -f2 || echo 'not installed')"
elif command -v pip >/dev/null 2>&1; then
SYSTEM_INFO_PIP_VERSION="$(pip --version 2>/dev/null | cut -d' ' -f2 || echo 'not installed')"
else
SYSTEM_INFO_PIP_VERSION="not installed"
fi
# Curl version
if command -v curl >/dev/null 2>&1; then
SYSTEM_INFO_CURL_VERSION="$(curl --version 2>/dev/null | head -n1 | cut -d' ' -f2 || echo 'not installed')"
else
SYSTEM_INFO_CURL_VERSION="not installed"
fi
# Git version
if command -v git >/dev/null 2>&1; then
SYSTEM_INFO_GIT_VERSION="$(git --version 2>/dev/null | cut -d' ' -f3 || echo 'not installed')"
else
SYSTEM_INFO_GIT_VERSION="not installed"
fi
# FFmpeg installed
if command -v ffmpeg >/dev/null 2>&1; then
SYSTEM_INFO_FFMPEG_INSTALLED="yes"
else
SYSTEM_INFO_FFMPEG_INSTALLED="no"
fi
# Docker installed
if command -v docker >/dev/null 2>&1; then
SYSTEM_INFO_DOCKER_INSTALLED="yes"
else
SYSTEM_INFO_DOCKER_INSTALLED="no"
fi
# Package managers
if [[ "$OS" == "macos" ]] && command -v brew >/dev/null 2>&1; then
SYSTEM_INFO_HOMEBREW_VERSION="$(brew --version 2>/dev/null | head -n1 | cut -d' ' -f2 || echo 'not installed')"
else
SYSTEM_INFO_HOMEBREW_VERSION="not installed"
fi
if command -v apt >/dev/null 2>&1; then
SYSTEM_INFO_APT_INSTALLED="yes"
else
SYSTEM_INFO_APT_INSTALLED="no"
fi
# Terminal application (if detectable)
SYSTEM_INFO_TERMINAL_APP="${TERM_PROGRAM:-${TERMINAL_EMULATOR:-unknown}}"
# Available disk space (in GB) on home partition
if [[ "$OS" == "macos" ]]; then
SYSTEM_INFO_DISK_AVAILABLE="$(df -h "$HOME" 2>/dev/null | awk 'NR==2 {print $4}' || echo 'unknown')"
else
SYSTEM_INFO_DISK_AVAILABLE="$(df -h "$HOME" 2>/dev/null | awk 'NR==2 {print $4}' || echo 'unknown')"
fi
if [[ "${VOICEMODE_INSTALL_DEBUG:-}" == "true" ]]; then
echo "System Information:"
echo " OS: $SYSTEM_INFO_OS"
echo " OS Version: $SYSTEM_INFO_OS_VERSION"
echo " Architecture: $SYSTEM_INFO_ARCH"
echo " Shell: $SYSTEM_INFO_SHELL"
echo " Shell Version: $SYSTEM_INFO_SHELL_VERSION"
echo " Shell RC File: $SYSTEM_INFO_SHELL_RC_FILE"
echo " Node Version: $SYSTEM_INFO_NODE_VERSION"
echo " NPM Version: $SYSTEM_INFO_NPM_VERSION"
echo " Python Version: $SYSTEM_INFO_PYTHON_VERSION"
echo " FFmpeg: $SYSTEM_INFO_FFMPEG_INSTALLED"
echo " Terminal: $SYSTEM_INFO_TERMINAL_APP"
echo " Disk Available: $SYSTEM_INFO_DISK_AVAILABLE"
fi
print_success "System information collected"
}
display_dependency_status() {
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " 📋 System Dependency Status"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# Required dependencies
echo -e "${BOLD}Required Dependencies:${NC}"
echo ""
# Node.js
if [[ "$SYSTEM_INFO_NODE_VERSION" != "not installed" ]]; then
echo -e " ${GREEN}✅${NC} Node.js ${DIM}$SYSTEM_INFO_NODE_VERSION${NC}"
else
echo -e " ${RED}❌${NC} Node.js ${DIM}(required for Claude Code)${NC}"
fi
# npm
if [[ "$SYSTEM_INFO_NPM_VERSION" != "not installed" ]]; then
echo -e " ${GREEN}✅${NC} npm ${DIM}v$SYSTEM_INFO_NPM_VERSION${NC}"
else
echo -e " ${RED}❌${NC} npm ${DIM}(required for package management)${NC}"
fi
# Python
if [[ "$SYSTEM_INFO_PYTHON_VERSION" != "not installed" ]]; then
echo -e " ${GREEN}✅${NC} Python ${DIM}$SYSTEM_INFO_PYTHON_VERSION${NC}"
else
echo -e " ${RED}❌${NC} Python ${DIM}(required for VoiceMode)${NC}"
fi
# FFmpeg
if [[ "$SYSTEM_INFO_FFMPEG_INSTALLED" == "yes" ]]; then
echo -e " ${GREEN}✅${NC} FFmpeg ${DIM}(audio processing)${NC}"
else
echo -e " ${RED}❌${NC} FFmpeg ${DIM}(required for audio processing)${NC}"
fi
# Git
if [[ "$SYSTEM_INFO_GIT_VERSION" != "not installed" ]]; then
echo -e " ${GREEN}✅${NC} Git ${DIM}v$SYSTEM_INFO_GIT_VERSION${NC}"
else
echo -e " ${YELLOW}⚠️ ${NC} Git ${DIM}(recommended for development)${NC}"
fi
echo ""
echo -e "${BOLD}Package Managers:${NC}"
echo ""
# Platform-specific package managers
if [[ "$OS" == "macos" ]]; then
if [[ "$SYSTEM_INFO_HOMEBREW_VERSION" != "not installed" ]]; then
echo -e " ${GREEN}✅${NC} Homebrew ${DIM}v$SYSTEM_INFO_HOMEBREW_VERSION${NC}"
else
echo -e " ${RED}❌${NC} Homebrew ${DIM}(required for macOS dependencies)${NC}"
fi
elif [[ "$OS" == "linux" ]]; then
if [[ "$SYSTEM_INFO_APT_INSTALLED" == "yes" ]]; then
echo -e " ${GREEN}✅${NC} APT ${DIM}(system package manager)${NC}"
else
echo -e " ${YELLOW}⚠️ ${NC} APT ${DIM}(package manager not detected)${NC}"
fi
fi
echo ""
echo -e "${BOLD}System Information:${NC}"
echo ""
echo " • OS: $SYSTEM_INFO_OS_VERSION"
echo " • Architecture: $SYSTEM_INFO_ARCH"
echo -e " • Shell: ${SYSTEM_INFO_SHELL##*/} ${DIM}(${SYSTEM_INFO_SHELL_RC_FILE})${NC}"
echo " • Terminal: $SYSTEM_INFO_TERMINAL_APP"
echo " • Disk Space: $SYSTEM_INFO_DISK_AVAILABLE available"
if [[ "$IS_WSL" == true ]]; then
echo ""
echo -e " ${YELLOW}⚠️ WSL2 Detected${NC} - Additional audio setup may be required"
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
}
check_missing_dependencies() {
# Returns 0 if all required dependencies are met, 1 if any are missing
local missing=0
if [[ "$SYSTEM_INFO_NODE_VERSION" == "not installed" ]]; then
missing=1
fi
if [[ "$SYSTEM_INFO_NPM_VERSION" == "not installed" ]]; then
missing=1
fi
if [[ "$SYSTEM_INFO_PYTHON_VERSION" == "not installed" ]]; then
missing=1
fi
if [[ "$SYSTEM_INFO_FFMPEG_INSTALLED" != "yes" ]]; then
missing=1
fi
# Platform-specific requirements
if [[ "$OS" == "macos" ]] && [[ "$SYSTEM_INFO_HOMEBREW_VERSION" == "not installed" ]]; then
missing=1
fi
return $missing
}
confirm_action() {
local action="$1"
local default_yes="${2:-true}" # Default to yes unless specified
# In non-interactive mode, always return success
if [[ "$NON_INTERACTIVE" == "true" ]]; then
echo ""
if [[ "$action" == *"?"* ]]; then
echo "$action"
else
echo "About to: $action"
fi
echo "→ Auto-accepting (non-interactive mode)"
return 0
fi
echo ""
# Check if action is already a question (contains "?")
if [[ "$action" == *"?"* ]]; then
echo "$action"
else
echo "About to: $action"
fi
if [[ "$default_yes" == "true" ]]; then
read -p "Continue? [Y/n]: " choice
# Empty response defaults to yes
if [[ -z "$choice" ]]; then
return 0
fi
else
read -p "Continue? [y/N]: " choice
# Empty response defaults to no
if [[ -z "$choice" ]]; then
echo "Skipping: $action"
return 1
fi
fi
case $choice in
[Yy]*) return 0 ;;
*)
echo "Skipping..."
return 1
;;
esac
}
install_homebrew() {
if [ "$HOMEBREW_INSTALLED" = false ]; then
if confirm_action "Install Homebrew (this will also install Xcode Command Line Tools)"; then
print_step "Installing Homebrew..."
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Add Homebrew to PATH for current session
if [[ "$ARCH" == "arm64" ]]; then
eval "$(/opt/homebrew/bin/brew shellenv)"
else
eval "$(/usr/local/bin/brew shellenv)"
fi
print_success "Homebrew installed successfully"
# Update the status variables since Homebrew installs Xcode tools
HOMEBREW_INSTALLED=true
XCODE_TOOLS_INSTALLED=true
else
print_error "Homebrew is required for VoiceMode dependencies. Installation aborted."
fi
fi
}
check_system_dependencies() {
print_step "Checking system dependencies..."
if [[ "$OS" == "macos" ]]; then
local packages=("node" "portaudio" "ffmpeg")
local missing_packages=()
for package in "${packages[@]}"; do
if brew list "$package" >/dev/null 2>&1; then
print_success "$package is already installed"
else
missing_packages+=("$package")
fi
done
if [ ${#missing_packages[@]} -eq 0 ]; then
print_success "All system dependencies are already installed"
return 0
else
echo "Missing packages: ${missing_packages[*]}"
return 1
fi
elif [[ "$OS" == "fedora" ]]; then
local packages=("nodejs" "portaudio-devel" "ffmpeg" "cmake" "python3-devel" "alsa-lib-devel" "gcc")
local missing_packages=()
for package in "${packages[@]}"; do
# Special handling for ffmpeg which might be installed from RPM Fusion
if [[ "$package" == "ffmpeg" ]]; then
if command -v ffmpeg >/dev/null 2>&1; then
print_success "$package is already installed"
else
missing_packages+=("$package")
fi
elif rpm -q "$package" >/dev/null 2>&1; then
print_success "$package is already installed"
else
missing_packages+=("$package")
fi
done
if [ ${#missing_packages[@]} -eq 0 ]; then
print_success "All system dependencies are already installed"
return 0
else
echo "Missing packages: ${missing_packages[*]}"
return 1
fi
elif [[ "$OS" == "debian" ]]; then
local packages=("nodejs" "npm" "portaudio19-dev" "ffmpeg" "cmake" "python3-dev" "libasound2-dev" "libasound2-plugins")
local missing_packages=()
for package in "${packages[@]}"; do
if dpkg -l "$package" 2>/dev/null | grep -q '^ii'; then
print_success "$package is already installed"
else
missing_packages+=("$package")
fi
done
if [ ${#missing_packages[@]} -eq 0 ]; then
print_success "All system dependencies are already installed"
return 0
else
echo "Missing packages: ${missing_packages[*]}"
return 1
fi
fi
}
install_system_dependencies() {
if ! check_system_dependencies; then
if [[ "$OS" == "macos" ]]; then
# Build list of what needs to be installed
local needs_homebrew=false
local needs_deps=false
local install_message=""
if [ "$HOMEBREW_INSTALLED" = false ]; then
needs_homebrew=true
install_message="• Homebrew (package manager)\n • Xcode Command Line Tools (automatically installed)\n "
fi
# Check for missing audio dependencies
local missing_packages=()
for package in ffmpeg portaudio; do
if ! brew list $package &>/dev/null; then
missing_packages+=($package)
needs_deps=true
fi
done
if [ "$needs_homebrew" = true ] || [ "$needs_deps" = true ]; then
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " 📦 System Dependencies Required"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "VoiceMode needs to install the following:"
if [ "$needs_homebrew" = true ]; then
echo -e "$install_message"
fi
if [ "$needs_deps" = true ]; then
echo "• Audio dependencies: ${missing_packages[*]}"
fi
echo ""
if confirm_action "Install all required dependencies?"; then
# Install Homebrew if needed
if [ "$needs_homebrew" = true ]; then
install_homebrew
if [ "$HOMEBREW_INSTALLED" = false ]; then
print_warning "Failed to install Homebrew. Cannot proceed."
return 1
fi
fi
# Install missing packages
if [ "$needs_deps" = true ]; then
print_step "Installing audio dependencies..."
brew update
for package in "${missing_packages[@]}"; do
print_step "Installing $package..."
brew install "$package"
print_success "$package installed"
done
fi
else
print_warning "Skipping system dependencies. VoiceMode may not work properly without them."
return 1
fi
fi
elif [[ "$OS" == "fedora" ]]; then
if confirm_action "Install missing system dependencies via DNF"; then
print_step "Installing system dependencies..."
# Update package lists (ignore exit code as dnf check-update returns 100 when updates are available)
sudo dnf check-update || true
# Install required packages
local packages=("nodejs" "portaudio-devel" "ffmpeg" "cmake" "python3-devel" "alsa-lib-devel" "gcc")
print_step "Installing packages: ${packages[*]}"
sudo dnf install -y "${packages[@]}"
print_success "System dependencies installed"
else
print_warning "Skipping system dependencies. VoiceMode may not work properly without them."
fi
elif [[ "$OS" == "debian" ]]; then
if confirm_action "Install missing system dependencies via APT"; then
print_step "Installing system dependencies..."
# Update package lists
sudo apt update
# Install required packages
local packages=("nodejs" "npm" "portaudio19-dev" "ffmpeg" "cmake" "python3-dev" "libasound2-dev" "libasound2-plugins" "pulseaudio" "pulseaudio-utils")
# Add WSL-specific packages if detected
if [[ "$IS_WSL" == true ]]; then
print_warning "WSL2 detected - installing additional audio packages"
packages+=("libasound2-plugins" "pulseaudio")
fi
print_step "Installing packages: ${packages[*]}"
sudo apt install -y "${packages[@]}"
print_success "System dependencies installed"
# WSL-specific audio setup
if [[ "$IS_WSL" == true ]]; then
print_step "Setting up WSL2 audio support..."
# Start PulseAudio if not running
if ! pgrep -x "pulseaudio" >/dev/null; then
pulseaudio --start 2>/dev/null || true
print_success "Started PulseAudio service"
fi
# Check audio devices
if command -v pactl >/dev/null 2>&1; then
if pactl list sources short 2>/dev/null | grep -q .; then
print_success "Audio devices detected in WSL2"
else
print_warning "No audio devices detected. WSL2 audio setup may require:"
echo " 1. Enable Windows microphone permissions for your terminal"
echo " 2. Ensure WSL version is 2.3.26.0 or higher (run 'wsl --version')"
echo " 3. See: https://github.com/mbailey/voicemode/blob/main/docs/troubleshooting/wsl2-microphone-access.md"
fi
fi
fi
else
print_warning "Skipping system dependencies. VoiceMode may not work properly without them."
fi
fi
fi
}
check_python() {
print_step "Checking Python installation..."
if command -v python3 >/dev/null 2>&1; then
local python_version=$(python3 --version | cut -d' ' -f2)
print_success "Python 3 found: $python_version"
else
print_warning "Python 3 not found in PATH"
echo " UV will manage Python installation automatically"
fi
}
install_uv() {
if ! command -v uv >/dev/null 2>&1; then
if confirm_action "Install UV (required for VoiceMode)"; then
print_step "Installing UV..."
# Install UV using the official installer
curl -LsSf https://astral.sh/uv/install.sh | sh
# Add UV to PATH for current session
export PATH="$HOME/.local/bin:$PATH"
# Verify installation immediately
if ! command -v uv >/dev/null 2>&1; then
print_error "UV installation failed - command not found after installation"
return 1
fi
# Test uv actually works
if ! uv --version >/dev/null 2>&1; then
print_error "UV installation failed - command not working"
return 1
fi
# Add to shell profile if not already there
local shell_profile="${SYSTEM_INFO_SHELL_RC_FILE:-$HOME/.bashrc}"
if [ -n "$shell_profile" ] && [ -f "$shell_profile" ]; then
if ! grep -q "\.local/bin" "$shell_profile"; then
echo 'export PATH="$HOME/.local/bin:$PATH"' >>"$shell_profile"
print_success "Added UV to PATH in $shell_profile"
# Source the profile to make UV immediately available
source "$shell_profile"
print_success "Loaded $shell_profile to update PATH"
fi
fi
print_success "UV installed and verified successfully"
else
print_error "UV is required for VoiceMode. Installation aborted."
return 1
fi
else
print_success "UV is already installed"
# Even if already installed, verify it works
if ! uv --version >/dev/null 2>&1; then
print_error "UV is installed but not working properly"
return 1
fi
fi
}
install_voicemode() {
print_step "Installing VoiceMode..."
# Install voice-mode package with uv tool install
if uv tool install --upgrade --force voice-mode; then
print_success "VoiceMode installed successfully"
# Update shell to ensure PATH includes UV tools
print_step "Updating shell PATH configuration..."
if uv tool update-shell; then
print_success "Shell PATH updated"
# Export PATH for current session
export PATH="$HOME/.local/bin:$PATH"
# Source shell profile for immediate availability
if [[ -n "${SYSTEM_INFO_SHELL_RC_FILE}" ]] && [[ -f "${SYSTEM_INFO_SHELL_RC_FILE}" ]]; then
source "${SYSTEM_INFO_SHELL_RC_FILE}" 2>/dev/null || true
fi
else
print_warning "Could not update shell PATH automatically"
fi
# Verify the voicemode command is available
if command -v voicemode >/dev/null 2>&1; then
print_success "VoiceMode command verified: voicemode"
return 0
else
print_warning "VoiceMode installed but command not immediately available"
if [[ -n "${SYSTEM_INFO_SHELL_RC_FILE}" ]]; then
echo " You may need to restart your shell or run: source ${SYSTEM_INFO_SHELL_RC_FILE}"
else
echo " You may need to restart your shell"
fi
return 0
fi
else
print_error "Failed to install VoiceMode"
return 1
fi
}
setup_local_npm() {
print_step "Setting up local npm configuration..."
# Set up npm to use local directory (no sudo required)
mkdir -p "$HOME/.npm-global"
npm config set prefix "$HOME/.npm-global"
# Add to PATH for current session
export PATH="$HOME/.npm-global/bin:$PATH"
# Add to shell profile if not already there
local shell_profile="${SYSTEM_INFO_SHELL_RC_FILE:-$HOME/.bashrc}"
if [ -n "$shell_profile" ] && [ -f "$shell_profile" ]; then
if ! grep -q "\.npm-global/bin" "$shell_profile"; then
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >>"$shell_profile"
print_success "Added npm global bin to PATH in $shell_profile"
# Source the profile to make npm immediately available
source "$shell_profile"
print_success "Loaded $shell_profile to update PATH"
fi
fi
print_success "Local npm configuration complete"
}
setup_shell_completion() {
# Safe shell completion setup that checks command availability at runtime
# This prevents shell startup errors by only enabling completions when the command exists
# Detect current shell
local shell_type=""
local shell_rc=""
if [[ -n "${BASH_VERSION:-}" ]]; then
shell_type="bash"
shell_rc="$HOME/.bashrc"
elif [[ -n "${ZSH_VERSION:-}" ]]; then
shell_type="zsh"
shell_rc="$HOME/.zshrc"
else
# Try to detect from SHELL environment variable
case "${SHELL:-}" in
*/bash)
shell_type="bash"
shell_rc="$HOME/.bashrc"
;;
*/zsh)
shell_type="zsh"
shell_rc="$HOME/.zshrc"
;;
*)
print_warning "Unsupported shell for completion: ${SHELL:-unknown}"
echo " VoiceMode completion supports bash and zsh only"
return 1
;;
esac
fi
if [[ -z "$shell_type" ]]; then
print_debug "Could not detect shell type for completion setup"
return 1
fi
print_step "Setting up shell completion for $shell_type..."
# Set up completion based on shell type
if [[ "$shell_type" == "bash" ]]; then
# Check for existing bash-completion support
local has_bash_completion=false
local user_completions_dir="${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion/completions"
# Check if bash-completion is installed
if [[ -f "/usr/share/bash-completion/bash_completion" ]] ||
[[ -f "/etc/bash_completion" ]] ||
(command -v brew >/dev/null 2>&1 && brew list bash-completion@2 >/dev/null 2>&1); then
has_bash_completion=true
fi
if [[ "$has_bash_completion" == true ]]; then
# Install completion file to user directory
mkdir -p "$user_completions_dir"
# Generate and save completion file with bash version compatibility
if command -v voicemode >/dev/null 2>&1; then
# Check bash version - nosort option is only available in bash 4.4+
local bash_version="${BASH_VERSION%%.*}"
local bash_minor="${BASH_VERSION#*.}"
bash_minor="${bash_minor%%.*}"
if [[ $bash_version -gt 4 ]] || [[ $bash_version -eq 4 && $bash_minor -ge 4 ]]; then
# Bash 4.4+ supports nosort
_VOICEMODE_COMPLETE=bash_source voicemode >"$user_completions_dir/voicemode" 2>/dev/null
else
# Older bash - filter out nosort option to prevent errors
_VOICEMODE_COMPLETE=bash_source voicemode 2>/dev/null | sed 's/complete -o nosort/complete/g' >"$user_completions_dir/voicemode"
fi
if [[ -s "$user_completions_dir/voicemode" ]]; then
print_success "Installed bash completion to $user_completions_dir/"
echo " Tab completion will be available in new bash sessions"
else
rm -f "$user_completions_dir/voicemode"
# Fallback to shell RC method
has_bash_completion=false
fi
fi
fi
if [[ "$has_bash_completion" == false ]]; then
# Fallback: Add to shell RC file with bash version check
local completion_line='# VoiceMode shell completion
if command -v voicemode >/dev/null 2>&1; then
# Check bash version for nosort compatibility
bash_version="${BASH_VERSION%%.*}"
bash_minor="${BASH_VERSION#*.}"
bash_minor="${bash_minor%%.*}"
if [[ $bash_version -gt 4 ]] || [[ $bash_version -eq 4 && $bash_minor -ge 4 ]]; then
eval "$(_VOICEMODE_COMPLETE=bash_source voicemode)"
else
# Filter out nosort for older bash versions
eval "$(_VOICEMODE_COMPLETE=bash_source voicemode | sed '\''s/complete -o nosort/complete/g'\'')"
fi
fi'
if [[ -f "$shell_rc" ]] && grep -q "_VOICEMODE_COMPLETE\|_VOICE_MODE_COMPLETE" "$shell_rc" 2>/dev/null; then
print_success "Shell completion already configured in $shell_rc"
else
echo "" >>"$shell_rc"
echo "$completion_line" >>"$shell_rc"
print_success "Added shell completion to $shell_rc"
echo " Tab completion will be available in new shell sessions"
fi
fi
elif [[ "$shell_type" == "zsh" ]]; then
# Detect best location for zsh completions
local completion_dir=""
local needs_fpath_update=false
# Check for Homebrew on macOS
if [[ "$OS" == "macos" ]] && command -v brew >/dev/null 2>&1; then
local brew_prefix=$(brew --prefix)
if [[ -d "$brew_prefix/share/zsh/site-functions" ]]; then
completion_dir="$brew_prefix/share/zsh/site-functions"
print_debug "Using Homebrew zsh completion directory"
fi
fi
# Check standard locations if not found
if [[ -z "$completion_dir" ]]; then
for dir in \
"/usr/local/share/zsh/site-functions" \
"/usr/share/zsh/site-functions" \
"$HOME/.zsh/completions"; do
if [[ -d "$dir" ]] || [[ "$dir" == "$HOME/.zsh/completions" ]]; then
completion_dir="$dir"
if [[ "$dir" == "$HOME/.zsh/completions" ]]; then
needs_fpath_update=true
fi
break
fi
done
fi
# Create directory if needed
if [[ ! -d "$completion_dir" ]]; then
mkdir -p "$completion_dir"
fi
# Generate and install completion file (with underscore prefix)
if command -v voicemode >/dev/null 2>&1; then
_VOICEMODE_COMPLETE=zsh_source voicemode >"$completion_dir/_voicemode" 2>/dev/null
if [[ -s "$completion_dir/_voicemode" ]]; then
print_success "Installed zsh completion to $completion_dir/"
# Update fpath if using custom directory
if [[ "$needs_fpath_update" == true ]]; then
local fpath_line="fpath=($completion_dir \$fpath)"
if [[ -f "$shell_rc" ]] && ! grep -q "$completion_dir" "$shell_rc" 2>/dev/null; then
echo "" >>"$shell_rc"
echo "# Add VoiceMode completions to fpath" >>"$shell_rc"
echo "$fpath_line" >>"$shell_rc"
print_success "Added $completion_dir to fpath in $shell_rc"
fi
fi
echo " Tab completion will be available in new zsh sessions"
else
# Fallback to shell RC method
rm -f "$completion_dir/_voicemode"
local completion_line='# VoiceMode shell completion
if command -v voicemode >/dev/null 2>&1; then
eval "$(_VOICEMODE_COMPLETE=zsh_source voicemode)"
fi'
if [[ -f "$shell_rc" ]] && grep -q "_VOICEMODE_COMPLETE\|_VOICE_MODE_COMPLETE" "$shell_rc" 2>/dev/null; then
print_success "Shell completion already configured in $shell_rc"
else
echo "" >>"$shell_rc"
echo "$completion_line" >>"$shell_rc"
print_success "Added shell completion to $shell_rc"
echo " Tab completion will be available in new shell sessions"
fi
fi
fi
fi
return 0
}
configure_api_key() {
# Skip API key configuration in CI mode
if [[ "$CI_MODE" == "true" ]]; then
print_step "Skipping OpenAI API key configuration (CI mode)"
echo "→ API key can be configured later if needed"
export VOICEMODE_API_KEY_CONFIGURED=false
return 0
fi
print_step "Checking OpenAI API key configuration..."
# Track if we have an API key configured
local api_key_configured=false
# Check if OPENAI_API_KEY is already set
if [ -n "${OPENAI_API_KEY:-}" ]; then
print_success "OPENAI_API_KEY is already set in environment"
echo ""
echo "Your OpenAI API key is configured. VoiceMode will use OpenAI for speech"
echo "recognition and text-to-speech, with automatic fallback support."
api_key_configured=true
# Export a flag for later use
export VOICEMODE_API_KEY_CONFIGURED=true
return 0
fi
# Check if it's in shell config files
for file in ~/.bashrc ~/.zshrc ~/.bash_profile; do
if [ -f "$file" ] && grep -q "export OPENAI_API_KEY" "$file" 2>/dev/null; then
print_success "OPENAI_API_KEY found in $file"
echo ""
echo "Your OpenAI API key is configured. VoiceMode will use OpenAI for speech"
echo "recognition and text-to-speech, with automatic fallback support."
api_key_configured=true
# Export a flag for later use
export VOICEMODE_API_KEY_CONFIGURED=true
return 0
fi
done
# API key not found - recommend setting it up
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " 🎯 Quick Start with OpenAI (Recommended)"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "For the best first experience, we recommend using OpenAI's API:"
echo " ✓ Works immediately - no complex setup required"
echo " ✓ High-quality speech recognition and natural voices"
echo " ✓ Automatic fallback when local services are unavailable"
echo ""
echo "Local services (Whisper/Kokoro) can be added later for:"
echo " • Reduced costs"
echo " • Enhanced privacy"
echo " • Offline capability"
echo " (Claude can help you set these up when you're ready)"
echo ""
if confirm_action "Would you like to set up your OpenAI API key now? (recommended)"; then
echo ""
echo "To get an API key:"
echo " 1. Visit: https://platform.openai.com"
echo " 2. Sign in or create an account"
echo " 3. Click 'Create new secret key'"
echo " 4. Copy the key (it starts with 'sk-')"
echo ""
# Offer to open the page
if command -v open >/dev/null 2>&1; then
if confirm_action "Would you like to open the OpenAI API keys page in your browser?"; then
open "https://platform.openai.com/api-keys"
echo ""
echo "Waiting for you to create and copy your API key..."
sleep 2
fi
fi
# Platform-specific paste instructions
echo ""
if [[ "$OS" == "macos" ]]; then
echo "Please paste your OpenAI API key (press Cmd+V to paste, or press Enter to skip):"
elif [[ "$IS_WSL" == true ]]; then
echo "Please paste your OpenAI API key (right-click to paste, or press Enter to skip):"
else
echo "Please paste your OpenAI API key (Ctrl+Shift+V or right-click to paste, or press Enter to skip):"
fi
echo "(The key will be hidden as you type)"
# Allow up to 3 attempts for API key entry
local attempts=0
local max_attempts=3
local api_key=""
while [[ $attempts -lt $max_attempts ]]; do
read -s api_key
echo ""
if [ -z "$api_key" ]; then
# User pressed Enter to skip
break
fi
# Validate that it looks like an API key
if [[ ! "$api_key" =~ ^sk- ]]; then
((attempts++))
if [[ $attempts -lt $max_attempts ]]; then
print_warning "That doesn't look like an OpenAI API key (should start with 'sk-')"
echo "Please try again (attempt $((attempts + 1)) of $max_attempts):"
echo "(Make sure to copy the entire key, it should start with 'sk-')"
else
print_warning "Invalid API key format after $max_attempts attempts"
echo "You can add it manually later to your shell configuration"
return 1
fi
else
# Validate the API key by making a test request
print_step "Validating API key with OpenAI..."
local http_code=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $api_key" \
-H "Content-Type: application/json" \
"https://api.openai.com/v1/models" 2>/dev/null)
if [[ "$http_code" == "200" ]]; then
print_success "API key validated successfully!"
break
elif [[ "$http_code" == "401" ]]; then
((attempts++))
if [[ $attempts -lt $max_attempts ]]; then
print_warning "Invalid API key - authentication failed"
echo "Please check your key and try again (attempt $((attempts + 1)) of $max_attempts):"
else
print_error "Invalid API key after $max_attempts attempts"
echo "You can add a valid key later to your shell configuration"
return 1
fi
else
# Network error or other issue - proceed anyway
print_warning "Could not validate API key (network issue?)"
echo "Proceeding with configuration anyway..."
break
fi
fi
done
if [ -n "$api_key" ] && [[ "$api_key" =~ ^sk- ]]; then
# Add to shell configuration
local shell_profile="${SYSTEM_INFO_SHELL_RC_FILE:-$HOME/.bashrc}"
if [ -n "$shell_profile" ]; then
echo "" >>"$shell_profile"
echo "# OpenAI API Key for VoiceMode" >>"$shell_profile"
echo "export OPENAI_API_KEY='$api_key'" >>"$shell_profile"
print_success "Added OPENAI_API_KEY to $shell_profile"
# Export for current session
export OPENAI_API_KEY="$api_key"
export VOICEMODE_API_KEY_CONFIGURED=true
echo ""
print_success "OpenAI API key configured successfully!"
return 0
fi
else
print_warning "Skipping OpenAI API key configuration"
fi
else
print_warning "Skipping OpenAI API key configuration"
echo ""
echo "You can add it later to your shell configuration:"
echo " export OPENAI_API_KEY='your-api-key-here'"
echo ""
echo "Without an API key, only local services will work (after installation)."
fi
return 1
}
test_voice_setup() {
print_step "Testing voice setup..."
# Check if voicemode command is available
if ! command -v voicemode >/dev/null 2>&1; then
print_warning "voicemode command not found in PATH"
return 1
fi
# Check for audio devices first
print_step "Checking audio devices..."
local device_output=$(voicemode diag devices 2>&1)
if echo "$device_output" | grep -q "Input Devices:"; then
# Extract only the input devices section (between "Input Devices:" and next empty line or "Output")
local input_section=$(echo "$device_output" | sed -n '/Input Devices:/,/^$/p' | sed -n '/Input Devices:/,/Output Devices:/p')
local input_count=$(echo "$input_section" | grep -c "^\s*\[[0-9]\].*channels)")
if [ "$input_count" -gt 0 ]; then
print_success "Found $input_count microphone(s)"
# Show the default input device
echo "$device_output" | grep "Default Input:" | sed 's/^/ /'
else
print_warning "No microphones detected"
echo " Voice features require a microphone to be connected"
return 1
fi
else
print_warning "Could not detect audio devices"
fi
# Check if we have an API key or local services
local has_tts=false
if [ -n "${OPENAI_API_KEY:-}" ]; then
has_tts=true
echo " OpenAI API key is configured ✓"
fi
# Quick check for local services
if voicemode whisper status 2>/dev/null | grep -q "running"; then
has_tts=true
echo " Whisper (STT) is running ✓"
fi
if voicemode kokoro status 2>/dev/null | grep -q "available"; then
has_tts=true
echo " Kokoro (TTS) is running ✓"
fi
if [ "$has_tts" = false ]; then
print_warning "No TTS/STT services available yet"
echo " You'll need either an OpenAI API key or local services to use voice features"
return 1
fi
echo ""
if confirm_action "Would you like to test voice mode now?"; then
echo ""
echo "Starting voice test..."
echo " • Say 'Hello' when you hear the chime"
echo " • The test will confirm your microphone and API are working"
echo " • Press Ctrl+C to exit the test"
echo ""
# Run a voice test
echo ""
echo "Running voice test..."
echo ""
# Run a single interaction test
# This will speak, then listen for a response (default 120 seconds)
voicemode converse --message "Hello! Testing voice mode. Say something to test your microphone." 2>&1 || true
echo ""
print_success "Voice test complete!"
fi
return 0
}
configure_claude_voicemode() {
if command -v claude >/dev/null 2>&1; then
# Check if voicemode is already configured
if claude mcp list 2>/dev/null | grep -q -e "voicemode" -e "voice-mode"; then
print_success "VoiceMode is already configured in Claude Code"
setup_shell_completion
return 0
else
if confirm_action "Add VoiceMode to Claude Code (adds MCP server)"; then
print_step "Adding VoiceMode to Claude Code..."
# Try with --scope flag first (newer versions)
if claude mcp add --scope user voicemode -- uvx --refresh voice-mode 2>/dev/null; then
print_success "VoiceMode added to Claude Code"
setup_shell_completion
return 0
else
print_error "Failed to add VoiceMode to Claude Code"
return 1
fi
else
print_step "Skipping VoiceMode configuration"
echo "You can configure it later with:"
echo " claude mcp add --scope user voicemode -- uvx --refresh voice-mode"
return 1
fi
fi
else
print_warning "Claude Code not found. Please install it first to use VoiceMode."
return 1
fi
}
check_and_suggest_working_directory() {
# Check if user is in home directory
local current_dir=$(pwd)
local home_dir="$HOME"
if [[ "$current_dir" == "$home_dir" ]]; then
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " 📁 Working Directory Setup"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "You're currently in your home directory."
echo "Claude Code works best when launched from a project directory."
echo ""
echo "Where would you like to start Claude from?"
echo " • Press Enter for ~/claude (recommended sandbox)"
echo " • Type a path like ~/projects or ~/code"
echo " • Type 'here' to use your home directory"
echo ""
read -p "Directory [~/claude]: " chosen_dir
# Handle the user's choice
if [[ -z "$chosen_dir" ]]; then
# Default to ~/claude
chosen_dir="$HOME/claude"
elif [[ "$chosen_dir" == "here" ]] || [[ "$chosen_dir" == "." ]]; then
# Use current directory (home)
chosen_dir="$HOME"
else
# Expand tilde if present
chosen_dir="${chosen_dir/#\~/$HOME}"
fi
# Create directory if it doesn't exist (unless it's home)
if [[ "$chosen_dir" != "$HOME" ]]; then
if [[ ! -d "$chosen_dir" ]]; then
print_step "Creating directory: $chosen_dir"
mkdir -p "$chosen_dir"
print_success "Created $chosen_dir"
else
print_success "Using existing directory: $chosen_dir"
fi
# Save for later reference
export CLAUDE_SUGGESTED_DIR="$chosen_dir"
echo ""
echo "After installation completes, you can:"
echo " 1. cd $chosen_dir"
echo " 2. claude converse"
echo ""
else
print_success "Will use home directory"
echo ""
echo "You can start Claude from your home directory after installation."
echo ""
fi
fi
}
install_claude_if_needed() {
if ! command -v claude >/dev/null 2>&1; then
if confirm_action "Install Claude Code (required for VoiceMode)"; then
print_step "Installing Claude Code..."
# Check for npm and install Node.js if missing
if ! command -v npm >/dev/null 2>&1; then
print_step "npm not found. Installing Node.js..."
if [[ "$OS" == "macos" ]]; then
# Install Node.js via Homebrew on macOS
if command -v brew >/dev/null 2>&1; then
brew install node
print_success "Node.js installed via Homebrew"
else
print_error "Homebrew not found. Please install Homebrew first."
return 1
fi
elif [[ "$OS" == "linux" ]]; then
# Install Node.js on Linux
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update && sudo apt-get install -y nodejs npm
print_success "Node.js installed via apt"
elif command -v dnf >/dev/null 2>&1; then
sudo dnf install -y nodejs npm
print_success "Node.js installed via dnf"
else
print_error "Unable to install Node.js. Please install it manually."
return 1
fi
fi
# Set up local npm configuration to avoid sudo for global installs
setup_local_npm
fi
# Now install Claude Code
if command -v npm >/dev/null 2>&1; then
npm install -g @anthropic-ai/claude-code
print_success "Claude Code installed"
else
print_error "Failed to install npm. Please install Node.js manually."
return 1
fi
else
print_warning "Claude Code is required for VoiceMode. Skipping configuration."
return 1
fi
fi
return 0
}
# Service installation functions
check_voice_mode_cli() {
# Check for locally installed voicemode command
# This should be available after uv tool install
if [[ "${VOICEMODE_INSTALL_DEBUG:-}" == "true" ]]; then
echo "[DEBUG] Checking for voicemode CLI availability..." >&2
fi
# Check if voicemode command is available
if command -v voicemode >/dev/null 2>&1; then
print_success "VoiceMode CLI is available" >&2
echo "voicemode"
return 0
else
print_warning "VoiceMode CLI not found" >&2
echo " Please ensure VoiceMode was installed correctly with 'uv tool install --upgrade --force voice-mode'" >&2
echo " You may need to restart your shell or run: source ~/.bashrc" >&2
return 1
fi
}
install_service() {
local service_name="$1"
local voice_mode_cmd="$2"
local description="$3"
print_step "Installing $description..."
# Debug mode check
if [[ "${VOICEMODE_INSTALL_DEBUG:-}" == "true" ]]; then
echo "[DEBUG] Checking service command: $voice_mode_cmd $service_name --help"
fi
# Check if the service subcommand exists first
# Redirect stderr to stdout and check for the actual help output
# This handles cases where Python warnings are printed to stderr
local help_output
local help_exit_code
help_output=$(timeout 30 $voice_mode_cmd $service_name --help 2>&1 || true)
help_exit_code=$?
if [[ "${VOICEMODE_INSTALL_DEBUG:-}" == "true" ]]; then
echo "[DEBUG] Help command exit code: $help_exit_code"
echo "[DEBUG] Help output length: ${#help_output} bytes"
echo "[DEBUG] First 500 chars of help output:"
echo "$help_output" | head -c 500
echo ""
echo "[DEBUG] Checking for 'Commands:' in output..."
fi
if ! echo "$help_output" | grep -q "Commands:"; then
print_warning "$description service command not available"
if [[ "${VOICEMODE_INSTALL_DEBUG:-}" == "true" ]]; then
echo "[DEBUG] 'Commands:' not found in help output"
echo "[DEBUG] Full help output:"
echo "$help_output"
fi
return 1
fi
if [[ "${VOICEMODE_INSTALL_DEBUG:-}" == "true" ]]; then
echo "[DEBUG] Service command check passed"
fi
# Install with timeout and capture output
local temp_log=$(mktemp)
local install_success=false
print_step "Running: $voice_mode_cmd $service_name install --auto-enable"
if timeout 600 $voice_mode_cmd $service_name install --auto-enable 2>&1 | tee "$temp_log"; then
install_success=true
fi
print_step "Running: $voice_mode_cmd $service_name enable"
if timeout 600 $voice_mode_cmd $service_name enable 2>&1 | tee "$temp_log"; then
install_success=true
fi
# Check for specific success/failure indicators
if [[ "$install_success" == true ]] && ! grep -qi "error\|failed\|traceback" "$temp_log"; then
print_success "$description installed successfully"
rm -f "$temp_log"
return 0
else
print_warning "$description installation may have failed"
echo "Last few lines of output:"
tail -10 "$temp_log" | sed 's/^/ /'
rm -f "$temp_log"
return 1
fi
}
install_all_services() {
local voice_mode_cmd="$1"
local success_count=0
local total_count=3
print_step "Installing all VoiceMode services..."
# Install each service independently
if install_service "whisper" "$voice_mode_cmd" "Whisper (Speech-to-Text)"; then
((success_count++))
# CoreML acceleration info for Apple Silicon Macs (now automatic!)
setup_coreml_acceleration
fi
if install_service "kokoro" "$voice_mode_cmd" "Kokoro (Text-to-Speech)"; then
((success_count++))
fi
if install_service "livekit" "$voice_mode_cmd" "LiveKit (Real-time Communication)"; then
((success_count++))
fi
# Report results
echo ""
if [[ $success_count -eq $total_count ]]; then
print_success "All voice services installed successfully!"
elif [[ $success_count -gt 0 ]]; then
print_warning "$success_count of $total_count services installed successfully"
echo " Check error messages above for failed installations"
else
print_error "No services were installed successfully"
fi
}
install_services_selective() {
local voice_mode_cmd="$1"
if confirm_action "Would you like to install Whisper (Speech-to-Text)?" false; then
install_service "whisper" "$voice_mode_cmd" "Whisper"
# CoreML acceleration info for Apple Silicon Macs (now automatic!)
setup_coreml_acceleration
fi
if confirm_action "Would you like to install Kokoro (Text-to-Speech)?" false; then
install_service "kokoro" "$voice_mode_cmd" "Kokoro"
fi
if confirm_action "Would you like to install LiveKit (Real-time Communication)?" false; then
install_service "livekit" "$voice_mode_cmd" "LiveKit"
fi
}
verify_voice_mode_after_mcp() {
print_step "Verifying VoiceMode CLI availability after MCP configuration..."
# Give a moment for any caching to settle
sleep 2
# Check voice-mode CLI availability
local voice_mode_cmd
if ! voice_mode_cmd=$(check_voice_mode_cli); then
print_warning "VoiceMode CLI not available yet. This could be due to:"
echo " • PATH not updated in current shell"
echo " • VoiceMode not installed yet"
echo " • Need to restart shell for PATH updates"
echo ""
echo "You can install services manually later with:"
echo " voicemode whisper install"
echo " voicemode kokoro install"
echo " voicemode livekit install"
return 1
fi
print_success "VoiceMode CLI verified: $voice_mode_cmd"
return 0
}
install_voice_services() {
# Verify voice-mode CLI availability first
if ! verify_voice_mode_after_mcp; then
return 1
fi
# Get the verified command
local voice_mode_cmd
voice_mode_cmd=$(check_voice_mode_cli)
echo ""
echo -e "${BLUE}🎤 VoiceMode Services${NC}"
echo ""
echo "VoiceMode can install local services for the best experience:"
echo " • Whisper - Fast local speech-to-text (no cloud required)"
echo " • Kokoro - Natural text-to-speech with multiple voices"
echo " • LiveKit - Real-time voice communication server"
echo ""
echo "Benefits:"
echo " • Privacy - All processing happens locally"
echo " • Speed - No network latency"
echo " • Reliability - Works offline"
echo ""
echo "Note: Service installation may take several minutes and requires internet access."
echo ""
# Quick mode or selective
read -p "Install all recommended services? [Y/n/s]: " choice
case $choice in
[Ss]*)
# Selective mode
install_services_selective "$voice_mode_cmd"
;;
[Nn]*)
print_warning "Skipping service installation. VoiceMode will use cloud services."
;;
*)
# Default: install all
install_all_services "$voice_mode_cmd"
;;
esac
}
show_banner() {
# Clear screen for clean presentation
clear
# Display VoiceMode ASCII art
echo ""
echo -e "${ORANGE}${BOLD}"
cat <<'EOF'
╔════════════════════════════════════════════╗
║ ║
║ ██╗ ██╗ ██████╗ ██╗ ██████╗███████╗ ║
║ ██║ ██║██╔═══██╗██║██╔════╝██╔════╝ ║
║ ██║ ██║██║ ██║██║██║ █████╗ ║
║ ╚██╗ ██╔╝██║ ██║██║██║ ██╔══╝ ║
║ ╚████╔╝ ╚██████╔╝██║╚██████╗███████╗ ║
║ ╚═══╝ ╚═════╝ ╚═╝ ╚═════╝╚══════╝ ║
║ ║
║ ███╗ ███╗ ██████╗ ██████╗ ███████╗ ║
║ ████╗ ████║██╔═══██╗██╔══██╗██╔════╝ ║
║ ██╔████╔██║██║ ██║██║ ██║█████╗ ║
║ ██║╚██╔╝██║██║ ██║██║ ██║██╔══╝ ║
║ ██║ ╚═╝ ██║╚██████╔╝██████╔╝███████╗ ║
║ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝ ║
║ ║
║ 🎙️ VoiceMode for Claude Code 🤖 ║
║ ║
╚════════════════════════════════════════════╝
EOF
echo -e "${NC}"
echo -e "${BOLD}Talk to Claude like a colleague, not a chatbot.${NC}"
echo ""
echo -e "${DIM}Transform your AI coding experience with natural voice conversations.${NC}"
echo ""
}
setup_coreml_acceleration() {
# Check if we're on Apple Silicon Mac
if [[ "$OS" == "macos" ]] && [[ "$ARCH" == "arm64" ]]; then
echo ""
echo -e "${CYAN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
echo -e "${BLUE}${BOLD}🚀 CoreML Acceleration Included!${NC}"
echo ""
echo "Your Mac supports CoreML acceleration for Whisper!"
echo ""
echo -e "${GREEN}Great News:${NC}"
echo " • Pre-built Core ML models download automatically"
echo " • NO Python dependencies (PyTorch) required"
echo " • NO Xcode installation needed"
echo " • NO manual setup or configuration"
echo ""
echo -e "${GREEN}Benefits:${NC}"
echo " • 2-3x faster transcription than Metal-only"
echo " • Lower CPU usage during speech recognition"
echo " • Better battery life on MacBooks"
echo " • Instant setup - just downloads and works!"
echo ""
echo -e "${DIM}Core ML models are downloaded automatically when you install${NC}"
echo -e "${DIM}Whisper models. No additional steps required!${NC}"
echo ""
echo -e "${CYAN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
fi
}
main() {
# Show banner first
show_banner
# Check for debug mode
if [[ "${VOICEMODE_INSTALL_DEBUG:-}" == "true" ]]; then
echo -e "${YELLOW}[DEBUG MODE ENABLED]${NC}"
echo ""
fi
# Anonymous analytics beacon (privacy-respecting)
# Only tracks: install event, OS type, timestamp
# No personal data collected
{
if command -v curl >/dev/null 2>&1; then
# Simple beacon to track installs
# Uses Goatcounter's pixel tracking endpoint
curl -s "https://getvoicemode.goatcounter.com/count?p=/install&t=Install%20Script" \
-H "User-Agent: VoiceMode-Installer/${OS:-unknown}" \
-H "Referer: https://getvoicemode.com/install.sh" \
>/dev/null 2>&1 || true
fi
} &
# Pre-flight checks
detect_os
# Collect comprehensive system information
collect_system_info
# Display dependency status
display_dependency_status
# Check if we have missing dependencies
if check_missing_dependencies; then
echo "All required dependencies are installed!"
else
print_warning "Some dependencies are missing and will be installed"
fi
# Early sudo caching for service installation (Linux only)
if [[ "$OS" == "linux" ]] && command -v sudo >/dev/null 2>&1; then
print_step "Requesting administrator access for system configuration..."
if ! sudo -v; then
print_warning "Administrator access declined. Some features may not be available."
fi
fi
# OS-specific setup
if [[ "$OS" == "macos" ]]; then
check_homebrew
# Don't install Homebrew here - let install_system_dependencies handle it
fi
check_python
install_uv
# Install dependencies (handles Homebrew installation if needed)
install_system_dependencies
# Install VoiceMode package locally
install_voicemode
# Setup npm for non-macOS systems
if [[ "$OS" != "macos" ]]; then
setup_local_npm
fi
# Configure OpenAI API key for quick start
configure_api_key
# Check if user is in home directory and suggest creating a workspace
check_and_suggest_working_directory
# Install Claude Code if needed, then configure VoiceMode
# Skip Claude Code installation in CI mode
if [[ "$CI_MODE" == "true" ]]; then
print_step "Skipping Claude Code installation (CI mode)"
echo ""
echo "✅ VoiceMode installation completed successfully!"
echo ""
echo "VoiceMode has been installed to: $(which voicemode)"
echo "Version: $(voicemode --version 2>/dev/null || echo 'unknown')"
elif install_claude_if_needed; then
if configure_claude_voicemode; then
# VoiceMode configured successfully
echo ""
echo "🎉 VoiceMode is ready!"
echo ""
# Test voice setup if API key is configured (from environment or config file)
if [ -n "${OPENAI_API_KEY:-}" ] || [ "${VOICEMODE_API_KEY_CONFIGURED:-}" = "true" ]; then
test_voice_setup
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " ✨ VoiceMode Installation Complete!"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# Advanced options as a side note
echo "📝 Advanced Options (not required):"
echo " Local voice services can reduce latency/costs and improve privacy:"
echo " • voicemode whisper install # Local speech-to-text"
echo " • voicemode kokoro install # Local text-to-speech"
echo ""
# Main instructions
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "🎯 How to use VoiceMode with Claude:"
echo ""
echo "1. From terminal: claude converse"
echo "2. From Claude Code: Type 'converse' or '/voicemode:converse'"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# Important note about shell restart
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "⚠️ IMPORTANT: Terminal Restart Required"
echo ""
echo "The 'claude' command won't work until you:"
echo " Option 1: Close and reopen your terminal (recommended)"
if [[ -n "${SYSTEM_INFO_SHELL_RC_FILE}" ]]; then
echo " Option 2: Run: source ${SYSTEM_INFO_SHELL_RC_FILE}"
else
echo " Option 2: Run: source your shell configuration file"
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "But we can start a conversation right now without restarting!"
echo ""
# Offer to start Claude immediately
if confirm_action "Would you like to start a voice conversation with Claude now? [Y/n]"; then
echo ""
print_step "Starting Claude with voice mode..."
echo " • Say something when you hear the chime"
echo " • Press Ctrl+C to exit when done"
echo ""
# Use the full path to claude since PATH may not be updated yet
if [ -f "$HOME/.npm-global/bin/claude" ]; then
"$HOME/.npm-global/bin/claude" converse
elif command -v claude >/dev/null 2>&1; then
claude converse
else
print_warning "Claude command not found in expected location."
echo "After restarting your terminal, run: claude converse"
fi
else
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "🚀 To start using VoiceMode:"
echo ""
if [[ -n "${SYSTEM_INFO_SHELL_RC_FILE}" ]]; then
echo " 1. Restart your terminal (or run: source ${SYSTEM_INFO_SHELL_RC_FILE})"
else
echo " 1. Restart your terminal"
fi
if [[ -n "${CLAUDE_SUGGESTED_DIR:-}" ]]; then
echo " 2. cd ${CLAUDE_SUGGESTED_DIR}"
echo " 3. claude converse"
else
echo " 2. claude converse (from your current directory)"
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
fi
else
print_warning "VoiceMode configuration was skipped or failed."
echo ""
echo "You can manually configure VoiceMode later with:"
echo " claude mcp add --scope user voicemode -- uvx --refresh voice-mode"
echo ""
echo "Then install services with:"
echo " voicemode whisper install"
echo " voicemode kokoro install"
echo " voicemode livekit install"
fi
fi
# WSL-specific instructions
if [[ "$IS_WSL" == true ]]; then
echo ""
echo -e "${YELLOW}WSL2 Audio Setup:${NC}"
echo "VoiceMode requires microphone access in WSL2. If you encounter audio issues:"
echo " 1. Enable Windows microphone permissions for your terminal app"
echo " 2. Ensure PulseAudio is running: pulseaudio --start"
echo " 3. Test audio devices: python3 -m sounddevice"
echo " 4. See troubleshooting guide: https://github.com/mbailey/voicemode/blob/main/docs/troubleshooting/wsl2-microphone-access.md"
fi
echo ""
echo "For more information, visit: https://github.com/mbailey/voicemode"
}
# Run main function
main "$@"