| #!/bin/bash |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| set -e |
|
|
| |
| RED='\033[0;31m' |
| GREEN='\033[0;32m' |
| YELLOW='\033[0;33m' |
| BLUE='\033[0;34m' |
| MAGENTA='\033[0;35m' |
| CYAN='\033[0;36m' |
| NC='\033[0m' |
| BOLD='\033[1m' |
|
|
| |
| REPO_URL_SSH="git@github.com:NousResearch/hermes-agent.git" |
| REPO_URL_HTTPS="https://github.com/NousResearch/hermes-agent.git" |
| HERMES_HOME="$HOME/.hermes" |
| INSTALL_DIR="${HERMES_INSTALL_DIR:-$HERMES_HOME/hermes-agent}" |
| PYTHON_VERSION="3.11" |
| NODE_VERSION="22" |
|
|
| |
| USE_VENV=true |
| RUN_SETUP=true |
| BRANCH="main" |
|
|
| |
| |
| |
| if [ -t 0 ]; then |
| IS_INTERACTIVE=true |
| else |
| IS_INTERACTIVE=false |
| fi |
|
|
| |
| while [[ $# -gt 0 ]]; do |
| case $1 in |
| --no-venv) |
| USE_VENV=false |
| shift |
| ;; |
| --skip-setup) |
| RUN_SETUP=false |
| shift |
| ;; |
| --branch) |
| BRANCH="$2" |
| shift 2 |
| ;; |
| --dir) |
| INSTALL_DIR="$2" |
| shift 2 |
| ;; |
| -h|--help) |
| echo "Hermes Agent Installer" |
| echo "" |
| echo "Usage: install.sh [OPTIONS]" |
| echo "" |
| echo "Options:" |
| echo " --no-venv Don't create virtual environment" |
| echo " --skip-setup Skip interactive setup wizard" |
| echo " --branch NAME Git branch to install (default: main)" |
| echo " --dir PATH Installation directory (default: ~/.hermes/hermes-agent)" |
| echo " -h, --help Show this help" |
| exit 0 |
| ;; |
| *) |
| echo "Unknown option: $1" |
| exit 1 |
| ;; |
| esac |
| done |
|
|
| |
| |
| |
|
|
| print_banner() { |
| echo "" |
| echo -e "${MAGENTA}${BOLD}" |
| echo "βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ" |
| echo "β β Hermes Agent Installer β" |
| echo "βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€" |
| echo "β An open source AI agent by Nous Research. β" |
| echo "βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ" |
| echo -e "${NC}" |
| } |
|
|
| log_info() { |
| echo -e "${CYAN}β${NC} $1" |
| } |
|
|
| log_success() { |
| echo -e "${GREEN}β${NC} $1" |
| } |
|
|
| log_warn() { |
| echo -e "${YELLOW}β ${NC} $1" |
| } |
|
|
| log_error() { |
| echo -e "${RED}β${NC} $1" |
| } |
|
|
| |
| |
| |
|
|
| detect_os() { |
| case "$(uname -s)" in |
| Linux*) |
| OS="linux" |
| if [ -f /etc/os-release ]; then |
| . /etc/os-release |
| DISTRO="$ID" |
| else |
| DISTRO="unknown" |
| fi |
| ;; |
| Darwin*) |
| OS="macos" |
| DISTRO="macos" |
| ;; |
| CYGWIN*|MINGW*|MSYS*) |
| OS="windows" |
| DISTRO="windows" |
| log_error "Windows detected. Please use the PowerShell installer:" |
| log_info " irm https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1 | iex" |
| exit 1 |
| ;; |
| *) |
| OS="unknown" |
| DISTRO="unknown" |
| log_warn "Unknown operating system" |
| ;; |
| esac |
|
|
| log_success "Detected: $OS ($DISTRO)" |
| } |
|
|
| |
| |
| |
|
|
| install_uv() { |
| log_info "Checking for uv package manager..." |
|
|
| |
| if command -v uv &> /dev/null; then |
| UV_CMD="uv" |
| UV_VERSION=$($UV_CMD --version 2>/dev/null) |
| log_success "uv found ($UV_VERSION)" |
| return 0 |
| fi |
|
|
| |
| if [ -x "$HOME/.local/bin/uv" ]; then |
| UV_CMD="$HOME/.local/bin/uv" |
| UV_VERSION=$($UV_CMD --version 2>/dev/null) |
| log_success "uv found at ~/.local/bin ($UV_VERSION)" |
| return 0 |
| fi |
|
|
| |
| if [ -x "$HOME/.cargo/bin/uv" ]; then |
| UV_CMD="$HOME/.cargo/bin/uv" |
| UV_VERSION=$($UV_CMD --version 2>/dev/null) |
| log_success "uv found at ~/.cargo/bin ($UV_VERSION)" |
| return 0 |
| fi |
|
|
| |
| log_info "Installing uv (fast Python package manager)..." |
| if curl -LsSf https://astral.sh/uv/install.sh | sh 2>/dev/null; then |
| |
| if [ -x "$HOME/.local/bin/uv" ]; then |
| UV_CMD="$HOME/.local/bin/uv" |
| elif [ -x "$HOME/.cargo/bin/uv" ]; then |
| UV_CMD="$HOME/.cargo/bin/uv" |
| elif command -v uv &> /dev/null; then |
| UV_CMD="uv" |
| else |
| log_error "uv installed but not found on PATH" |
| log_info "Try adding ~/.local/bin to your PATH and re-running" |
| exit 1 |
| fi |
| UV_VERSION=$($UV_CMD --version 2>/dev/null) |
| log_success "uv installed ($UV_VERSION)" |
| else |
| log_error "Failed to install uv" |
| log_info "Install manually: https://docs.astral.sh/uv/getting-started/installation/" |
| exit 1 |
| fi |
| } |
|
|
| check_python() { |
| log_info "Checking Python $PYTHON_VERSION..." |
|
|
| |
| |
| if $UV_CMD python find "$PYTHON_VERSION" &> /dev/null; then |
| PYTHON_PATH=$($UV_CMD python find "$PYTHON_VERSION") |
| PYTHON_FOUND_VERSION=$($PYTHON_PATH --version 2>/dev/null) |
| log_success "Python found: $PYTHON_FOUND_VERSION" |
| return 0 |
| fi |
|
|
| |
| log_info "Python $PYTHON_VERSION not found, installing via uv..." |
| if $UV_CMD python install "$PYTHON_VERSION"; then |
| PYTHON_PATH=$($UV_CMD python find "$PYTHON_VERSION") |
| PYTHON_FOUND_VERSION=$($PYTHON_PATH --version 2>/dev/null) |
| log_success "Python installed: $PYTHON_FOUND_VERSION" |
| else |
| log_error "Failed to install Python $PYTHON_VERSION" |
| log_info "Install Python $PYTHON_VERSION manually, then re-run this script" |
| exit 1 |
| fi |
| } |
|
|
| check_git() { |
| log_info "Checking Git..." |
|
|
| if command -v git &> /dev/null; then |
| GIT_VERSION=$(git --version | awk '{print $3}') |
| log_success "Git $GIT_VERSION found" |
| return 0 |
| fi |
|
|
| log_error "Git not found" |
| log_info "Please install Git:" |
|
|
| case "$OS" in |
| linux) |
| case "$DISTRO" in |
| ubuntu|debian) |
| log_info " sudo apt update && sudo apt install git" |
| ;; |
| fedora) |
| log_info " sudo dnf install git" |
| ;; |
| arch) |
| log_info " sudo pacman -S git" |
| ;; |
| *) |
| log_info " Use your package manager to install git" |
| ;; |
| esac |
| ;; |
| macos) |
| log_info " xcode-select --install" |
| log_info " Or: brew install git" |
| ;; |
| esac |
|
|
| exit 1 |
| } |
|
|
| check_node() { |
| log_info "Checking Node.js (for browser tools)..." |
|
|
| if command -v node &> /dev/null; then |
| local found_ver=$(node --version) |
| log_success "Node.js $found_ver found" |
| HAS_NODE=true |
| return 0 |
| fi |
|
|
| |
| if [ -x "$HERMES_HOME/node/bin/node" ]; then |
| export PATH="$HERMES_HOME/node/bin:$PATH" |
| local found_ver=$("$HERMES_HOME/node/bin/node" --version) |
| log_success "Node.js $found_ver found (Hermes-managed)" |
| HAS_NODE=true |
| return 0 |
| fi |
|
|
| log_info "Node.js not found β installing Node.js $NODE_VERSION LTS..." |
| install_node |
| } |
|
|
| install_node() { |
| local arch=$(uname -m) |
| local node_arch |
| case "$arch" in |
| x86_64) node_arch="x64" ;; |
| aarch64|arm64) node_arch="arm64" ;; |
| armv7l) node_arch="armv7l" ;; |
| *) |
| log_warn "Unsupported architecture ($arch) for Node.js auto-install" |
| log_info "Install manually: https://nodejs.org/en/download/" |
| HAS_NODE=false |
| return 0 |
| ;; |
| esac |
|
|
| local node_os |
| case "$OS" in |
| linux) node_os="linux" ;; |
| macos) node_os="darwin" ;; |
| *) |
| log_warn "Unsupported OS for Node.js auto-install" |
| HAS_NODE=false |
| return 0 |
| ;; |
| esac |
|
|
| |
| local index_url="https://nodejs.org/dist/latest-v${NODE_VERSION}.x/" |
| local tarball_name |
| tarball_name=$(curl -fsSL "$index_url" \ |
| | grep -oE "node-v${NODE_VERSION}\.[0-9]+\.[0-9]+-${node_os}-${node_arch}\.tar\.xz" \ |
| | head -1) |
|
|
| |
| if [ -z "$tarball_name" ]; then |
| tarball_name=$(curl -fsSL "$index_url" \ |
| | grep -oE "node-v${NODE_VERSION}\.[0-9]+\.[0-9]+-${node_os}-${node_arch}\.tar\.gz" \ |
| | head -1) |
| fi |
|
|
| if [ -z "$tarball_name" ]; then |
| log_warn "Could not find Node.js $NODE_VERSION binary for $node_os-$node_arch" |
| log_info "Install manually: https://nodejs.org/en/download/" |
| HAS_NODE=false |
| return 0 |
| fi |
|
|
| local download_url="${index_url}${tarball_name}" |
| local tmp_dir |
| tmp_dir=$(mktemp -d) |
|
|
| log_info "Downloading $tarball_name..." |
| if ! curl -fsSL "$download_url" -o "$tmp_dir/$tarball_name"; then |
| log_warn "Download failed" |
| rm -rf "$tmp_dir" |
| HAS_NODE=false |
| return 0 |
| fi |
|
|
| log_info "Extracting to ~/.hermes/node/..." |
| if [[ "$tarball_name" == *.tar.xz ]]; then |
| tar xf "$tmp_dir/$tarball_name" -C "$tmp_dir" |
| else |
| tar xzf "$tmp_dir/$tarball_name" -C "$tmp_dir" |
| fi |
|
|
| local extracted_dir |
| extracted_dir=$(ls -d "$tmp_dir"/node-v* 2>/dev/null | head -1) |
|
|
| if [ ! -d "$extracted_dir" ]; then |
| log_warn "Extraction failed" |
| rm -rf "$tmp_dir" |
| HAS_NODE=false |
| return 0 |
| fi |
|
|
| |
| rm -rf "$HERMES_HOME/node" |
| mkdir -p "$HERMES_HOME" |
| mv "$extracted_dir" "$HERMES_HOME/node" |
| rm -rf "$tmp_dir" |
|
|
| mkdir -p "$HOME/.local/bin" |
| ln -sf "$HERMES_HOME/node/bin/node" "$HOME/.local/bin/node" |
| ln -sf "$HERMES_HOME/node/bin/npm" "$HOME/.local/bin/npm" |
| ln -sf "$HERMES_HOME/node/bin/npx" "$HOME/.local/bin/npx" |
|
|
| export PATH="$HERMES_HOME/node/bin:$PATH" |
|
|
| local installed_ver |
| installed_ver=$("$HERMES_HOME/node/bin/node" --version 2>/dev/null) |
| log_success "Node.js $installed_ver installed to ~/.hermes/node/" |
| HAS_NODE=true |
| } |
|
|
| install_system_packages() { |
| |
| HAS_RIPGREP=false |
| HAS_FFMPEG=false |
| local need_ripgrep=false |
| local need_ffmpeg=false |
|
|
| log_info "Checking ripgrep (fast file search)..." |
| if command -v rg &> /dev/null; then |
| log_success "$(rg --version | head -1) found" |
| HAS_RIPGREP=true |
| else |
| need_ripgrep=true |
| fi |
|
|
| log_info "Checking ffmpeg (TTS voice messages)..." |
| if command -v ffmpeg &> /dev/null; then |
| local ffmpeg_ver=$(ffmpeg -version 2>/dev/null | head -1 | awk '{print $3}') |
| log_success "ffmpeg $ffmpeg_ver found" |
| HAS_FFMPEG=true |
| else |
| need_ffmpeg=true |
| fi |
|
|
| |
| if [ "$need_ripgrep" = false ] && [ "$need_ffmpeg" = false ]; then |
| return 0 |
| fi |
|
|
| |
| local desc_parts=() |
| local pkgs=() |
| if [ "$need_ripgrep" = true ]; then |
| desc_parts+=("ripgrep for faster file search") |
| pkgs+=("ripgrep") |
| fi |
| if [ "$need_ffmpeg" = true ]; then |
| desc_parts+=("ffmpeg for TTS voice messages") |
| pkgs+=("ffmpeg") |
| fi |
| local description |
| description=$(IFS=" and "; echo "${desc_parts[*]}") |
|
|
| |
| if [ "$OS" = "macos" ]; then |
| if command -v brew &> /dev/null; then |
| log_info "Installing ${pkgs[*]} via Homebrew..." |
| if brew install "${pkgs[@]}"; then |
| [ "$need_ripgrep" = true ] && HAS_RIPGREP=true && log_success "ripgrep installed" |
| [ "$need_ffmpeg" = true ] && HAS_FFMPEG=true && log_success "ffmpeg installed" |
| return 0 |
| fi |
| fi |
| log_warn "Could not auto-install (brew not found or install failed)" |
| log_info "Install manually: brew install ${pkgs[*]}" |
| return 0 |
| fi |
|
|
| |
| local pkg_install="" |
| case "$DISTRO" in |
| ubuntu|debian) pkg_install="apt install -y" ;; |
| fedora) pkg_install="dnf install -y" ;; |
| arch) pkg_install="pacman -S --noconfirm" ;; |
| esac |
|
|
| if [ -n "$pkg_install" ]; then |
| local install_cmd="$pkg_install ${pkgs[*]}" |
|
|
| |
| case "$DISTRO" in |
| ubuntu|debian) export DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a ;; |
| esac |
|
|
| |
| if [ "$(id -u)" -eq 0 ]; then |
| log_info "Installing ${pkgs[*]}..." |
| if $install_cmd; then |
| [ "$need_ripgrep" = true ] && HAS_RIPGREP=true && log_success "ripgrep installed" |
| [ "$need_ffmpeg" = true ] && HAS_FFMPEG=true && log_success "ffmpeg installed" |
| return 0 |
| fi |
| |
| elif command -v sudo &> /dev/null && sudo -n true 2>/dev/null; then |
| log_info "Installing ${pkgs[*]}..." |
| if sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a $install_cmd; then |
| [ "$need_ripgrep" = true ] && HAS_RIPGREP=true && log_success "ripgrep installed" |
| [ "$need_ffmpeg" = true ] && HAS_FFMPEG=true && log_success "ffmpeg installed" |
| return 0 |
| fi |
| |
| elif command -v sudo &> /dev/null; then |
| if [ "$IS_INTERACTIVE" = true ]; then |
| echo "" |
| log_info "sudo is needed ONLY to install optional system packages (${pkgs[*]}) via your package manager." |
| log_info "Hermes Agent itself does not require or retain root access." |
| read -p "Install ${description}? (requires sudo) [y/N] " -n 1 -r |
| echo |
| if [[ $REPLY =~ ^[Yy]$ ]]; then |
| if sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a $install_cmd; then |
| [ "$need_ripgrep" = true ] && HAS_RIPGREP=true && log_success "ripgrep installed" |
| [ "$need_ffmpeg" = true ] && HAS_FFMPEG=true && log_success "ffmpeg installed" |
| return 0 |
| fi |
| fi |
| elif [ -e /dev/tty ]; then |
| |
| |
| echo "" |
| log_info "sudo is needed ONLY to install optional system packages (${pkgs[*]}) via your package manager." |
| log_info "Hermes Agent itself does not require or retain root access." |
| read -p "Install ${description}? [Y/n] " -n 1 -r < /dev/tty |
| echo |
| if [[ $REPLY =~ ^[Yy]$ ]] || [[ -z $REPLY ]]; then |
| if sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a $install_cmd < /dev/tty; then |
| [ "$need_ripgrep" = true ] && HAS_RIPGREP=true && log_success "ripgrep installed" |
| [ "$need_ffmpeg" = true ] && HAS_FFMPEG=true && log_success "ffmpeg installed" |
| return 0 |
| fi |
| fi |
| else |
| log_warn "Non-interactive mode and no terminal available β cannot install system packages" |
| log_info "Install manually after setup completes: sudo $install_cmd" |
| fi |
| fi |
| fi |
|
|
| |
| if [ "$need_ripgrep" = true ] && [ "$HAS_RIPGREP" = false ]; then |
| if command -v cargo &> /dev/null; then |
| log_info "Trying cargo install ripgrep (no sudo needed)..." |
| if cargo install ripgrep; then |
| log_success "ripgrep installed via cargo" |
| HAS_RIPGREP=true |
| fi |
| fi |
| fi |
|
|
| |
| if [ "$HAS_RIPGREP" = false ] && [ "$need_ripgrep" = true ]; then |
| log_warn "ripgrep not installed (file search will use grep fallback)" |
| show_manual_install_hint "ripgrep" |
| fi |
| if [ "$HAS_FFMPEG" = false ] && [ "$need_ffmpeg" = true ]; then |
| log_warn "ffmpeg not installed (TTS voice messages will be limited)" |
| show_manual_install_hint "ffmpeg" |
| fi |
| } |
|
|
| show_manual_install_hint() { |
| local pkg="$1" |
| log_info "To install $pkg manually:" |
| case "$OS" in |
| linux) |
| case "$DISTRO" in |
| ubuntu|debian) log_info " sudo apt install $pkg" ;; |
| fedora) log_info " sudo dnf install $pkg" ;; |
| arch) log_info " sudo pacman -S $pkg" ;; |
| *) log_info " Use your package manager or visit the project homepage" ;; |
| esac |
| ;; |
| macos) log_info " brew install $pkg" ;; |
| esac |
| } |
|
|
| |
| |
| |
|
|
| clone_repo() { |
| log_info "Installing to $INSTALL_DIR..." |
|
|
| if [ -d "$INSTALL_DIR" ]; then |
| if [ -d "$INSTALL_DIR/.git" ]; then |
| log_info "Existing installation found, updating..." |
| cd "$INSTALL_DIR" |
|
|
| local autostash_ref="" |
| if [ -n "$(git status --porcelain)" ]; then |
| local stash_name |
| stash_name="hermes-install-autostash-$(date -u +%Y%m%d-%H%M%S)" |
| log_info "Local changes detected, stashing before update..." |
| git stash push --include-untracked -m "$stash_name" |
| autostash_ref="$(git rev-parse --verify refs/stash)" |
| fi |
|
|
| git fetch origin |
| git checkout "$BRANCH" |
| git pull --ff-only origin "$BRANCH" |
|
|
| if [ -n "$autostash_ref" ]; then |
| local restore_now="yes" |
| if [ -t 0 ] && [ -t 1 ]; then |
| echo |
| log_warn "Local changes were stashed before updating." |
| log_warn "Restoring them may reapply local customizations onto the updated codebase." |
| printf "Restore local changes now? [Y/n] " |
| read -r restore_answer |
| case "$restore_answer" in |
| ""|y|Y|yes|YES|Yes) restore_now="yes" ;; |
| *) restore_now="no" ;; |
| esac |
| fi |
|
|
| if [ "$restore_now" = "yes" ]; then |
| log_info "Restoring local changes..." |
| if git stash apply "$autostash_ref"; then |
| git stash drop "$autostash_ref" >/dev/null |
| log_warn "Local changes were restored on top of the updated codebase." |
| log_warn "Review git diff / git status if Hermes behaves unexpectedly." |
| else |
| log_error "Update succeeded, but restoring local changes failed. Your changes are still preserved in git stash." |
| log_info "Resolve manually with: git stash apply $autostash_ref" |
| exit 1 |
| fi |
| else |
| log_info "Skipped restoring local changes." |
| log_info "Your changes are still preserved in git stash." |
| log_info "Restore manually with: git stash apply $autostash_ref" |
| fi |
| fi |
| else |
| log_error "Directory exists but is not a git repository: $INSTALL_DIR" |
| log_info "Remove it or choose a different directory with --dir" |
| exit 1 |
| fi |
| else |
| |
| |
| |
| log_info "Trying SSH clone..." |
| if GIT_SSH_COMMAND="ssh -o BatchMode=yes -o ConnectTimeout=5" \ |
| git clone --branch "$BRANCH" "$REPO_URL_SSH" "$INSTALL_DIR" 2>/dev/null; then |
| log_success "Cloned via SSH" |
| else |
| rm -rf "$INSTALL_DIR" 2>/dev/null |
| log_info "SSH failed, trying HTTPS..." |
| if git clone --branch "$BRANCH" "$REPO_URL_HTTPS" "$INSTALL_DIR"; then |
| log_success "Cloned via HTTPS" |
| else |
| log_error "Failed to clone repository" |
| exit 1 |
| fi |
| fi |
| fi |
|
|
| cd "$INSTALL_DIR" |
|
|
| log_success "Repository ready" |
| } |
|
|
| setup_venv() { |
| if [ "$USE_VENV" = false ]; then |
| log_info "Skipping virtual environment (--no-venv)" |
| return 0 |
| fi |
|
|
| log_info "Creating virtual environment with Python $PYTHON_VERSION..." |
|
|
| if [ -d "venv" ]; then |
| log_info "Virtual environment already exists, recreating..." |
| rm -rf venv |
| fi |
|
|
| |
| $UV_CMD venv venv --python "$PYTHON_VERSION" |
|
|
| log_success "Virtual environment ready (Python $PYTHON_VERSION)" |
| } |
|
|
| install_deps() { |
| log_info "Installing dependencies..." |
|
|
| if [ "$USE_VENV" = true ]; then |
| |
| export VIRTUAL_ENV="$INSTALL_DIR/venv" |
| fi |
|
|
| |
| |
| if [ "$DISTRO" = "ubuntu" ] || [ "$DISTRO" = "debian" ]; then |
| local need_build_tools=false |
| for pkg in gcc python3-dev libffi-dev; do |
| if ! dpkg -s "$pkg" &>/dev/null; then |
| need_build_tools=true |
| break |
| fi |
| done |
| if [ "$need_build_tools" = true ]; then |
| log_info "Some build tools may be needed for Python packages..." |
| if command -v sudo &> /dev/null; then |
| if sudo -n true 2>/dev/null; then |
| sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y -qq build-essential python3-dev libffi-dev >/dev/null 2>&1 || true |
| log_success "Build tools installed" |
| else |
| log_info "sudo is needed ONLY to install build tools (build-essential, python3-dev, libffi-dev) via apt." |
| log_info "Hermes Agent itself does not require or retain root access." |
| read -p "Install build tools? [Y/n] " -n 1 -r < /dev/tty |
| echo |
| if [[ $REPLY =~ ^[Yy]$ ]] || [[ -z $REPLY ]]; then |
| sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y -qq build-essential python3-dev libffi-dev >/dev/null 2>&1 || true |
| log_success "Build tools installed" |
| fi |
| fi |
| fi |
| fi |
| fi |
|
|
| |
| |
| if ! $UV_CMD pip install -e ".[all]" 2>/dev/null; then |
| log_warn "Full install (.[all]) failed, trying base install..." |
| if ! $UV_CMD pip install -e "."; then |
| log_error "Package installation failed." |
| log_info "Check that build tools are installed: sudo apt install build-essential python3-dev" |
| log_info "Then re-run: cd $INSTALL_DIR && uv pip install -e '.[all]'" |
| exit 1 |
| fi |
| fi |
|
|
| log_success "Main package installed" |
|
|
| |
| |
| if [ -d "tinker-atropos" ] && [ -f "tinker-atropos/pyproject.toml" ]; then |
| log_info "tinker-atropos submodule found β skipping install (optional, for RL training)" |
| log_info " To install: $UV_CMD pip install -e \"./tinker-atropos\"" |
| fi |
|
|
| log_success "All dependencies installed" |
| } |
|
|
| setup_path() { |
| log_info "Setting up hermes command..." |
|
|
| if [ "$USE_VENV" = true ]; then |
| HERMES_BIN="$INSTALL_DIR/venv/bin/hermes" |
| else |
| HERMES_BIN="$(which hermes 2>/dev/null || echo "")" |
| if [ -z "$HERMES_BIN" ]; then |
| log_warn "hermes not found on PATH after install" |
| return 0 |
| fi |
| fi |
|
|
| |
| if [ ! -x "$HERMES_BIN" ]; then |
| log_warn "hermes entry point not found at $HERMES_BIN" |
| log_info "This usually means the pip install didn't complete successfully." |
| log_info "Try: cd $INSTALL_DIR && uv pip install -e '.[all]'" |
| return 0 |
| fi |
|
|
| |
| mkdir -p "$HOME/.local/bin" |
| ln -sf "$HERMES_BIN" "$HOME/.local/bin/hermes" |
| log_success "Symlinked hermes β ~/.local/bin/hermes" |
|
|
| |
| |
| |
| if ! echo "$PATH" | tr ':' '\n' | grep -q "^$HOME/.local/bin$"; then |
| SHELL_CONFIGS=() |
| LOGIN_SHELL="$(basename "${SHELL:-/bin/bash}")" |
| case "$LOGIN_SHELL" in |
| zsh) |
| [ -f "$HOME/.zshrc" ] && SHELL_CONFIGS+=("$HOME/.zshrc") |
| [ -f "$HOME/.zprofile" ] && SHELL_CONFIGS+=("$HOME/.zprofile") |
| |
| if [ ${#SHELL_CONFIGS[@]} -eq 0 ]; then |
| touch "$HOME/.zshrc" |
| SHELL_CONFIGS+=("$HOME/.zshrc") |
| fi |
| ;; |
| bash) |
| [ -f "$HOME/.bashrc" ] && SHELL_CONFIGS+=("$HOME/.bashrc") |
| [ -f "$HOME/.bash_profile" ] && SHELL_CONFIGS+=("$HOME/.bash_profile") |
| ;; |
| *) |
| [ -f "$HOME/.bashrc" ] && SHELL_CONFIGS+=("$HOME/.bashrc") |
| [ -f "$HOME/.zshrc" ] && SHELL_CONFIGS+=("$HOME/.zshrc") |
| ;; |
| esac |
| |
| |
| [ -f "$HOME/.profile" ] && SHELL_CONFIGS+=("$HOME/.profile") |
|
|
| PATH_LINE='export PATH="$HOME/.local/bin:$PATH"' |
|
|
| for SHELL_CONFIG in "${SHELL_CONFIGS[@]}"; do |
| if ! grep -v '^[[:space:]]*#' "$SHELL_CONFIG" 2>/dev/null | grep -qE 'PATH=.*\.local/bin'; then |
| echo "" >> "$SHELL_CONFIG" |
| echo "# Hermes Agent β ensure ~/.local/bin is on PATH" >> "$SHELL_CONFIG" |
| echo "$PATH_LINE" >> "$SHELL_CONFIG" |
| log_success "Added ~/.local/bin to PATH in $SHELL_CONFIG" |
| fi |
| done |
|
|
| if [ ${#SHELL_CONFIGS[@]} -eq 0 ]; then |
| log_warn "Could not detect shell config file to add ~/.local/bin to PATH" |
| log_info "Add manually: $PATH_LINE" |
| fi |
| else |
| log_info "~/.local/bin already on PATH" |
| fi |
|
|
| |
| export PATH="$HOME/.local/bin:$PATH" |
|
|
| log_success "hermes command ready" |
| } |
|
|
| copy_config_templates() { |
| log_info "Setting up configuration files..." |
|
|
| |
| mkdir -p "$HERMES_HOME"/{cron,sessions,logs,pairing,hooks,image_cache,audio_cache,memories,skills,whatsapp/session} |
|
|
| |
| if [ ! -f "$HERMES_HOME/.env" ]; then |
| if [ -f "$INSTALL_DIR/.env.example" ]; then |
| cp "$INSTALL_DIR/.env.example" "$HERMES_HOME/.env" |
| log_success "Created ~/.hermes/.env from template" |
| else |
| touch "$HERMES_HOME/.env" |
| log_success "Created ~/.hermes/.env" |
| fi |
| else |
| log_info "~/.hermes/.env already exists, keeping it" |
| fi |
|
|
| |
| if [ ! -f "$HERMES_HOME/config.yaml" ]; then |
| if [ -f "$INSTALL_DIR/cli-config.yaml.example" ]; then |
| cp "$INSTALL_DIR/cli-config.yaml.example" "$HERMES_HOME/config.yaml" |
| log_success "Created ~/.hermes/config.yaml from template" |
| fi |
| else |
| log_info "~/.hermes/config.yaml already exists, keeping it" |
| fi |
|
|
| |
| if [ ! -f "$HERMES_HOME/SOUL.md" ]; then |
| cat > "$HERMES_HOME/SOUL.md" << 'SOUL_EOF' |
| |
|
|
| <!-- |
| This file defines the agent's personality and tone. |
| The agent will embody whatever you write here. |
| Edit this to customize how Hermes communicates with you. |
| |
| Examples: |
| - "You are a warm, playful assistant who uses kaomoji occasionally." |
| - "You are a concise technical expert. No fluff, just facts." |
| - "You speak like a friendly coworker who happens to know everything." |
| |
| This file is loaded fresh each message -- no restart needed. |
| Delete the contents (or this file) to use the default personality. |
| --> |
| SOUL_EOF |
| log_success "Created ~/.hermes/SOUL.md (edit to customize personality)" |
| fi |
| |
| log_success "Configuration directory ready: ~/.hermes/" |
| |
| # Seed bundled skills into ~/.hermes/skills/ (manifest-based, one-time per skill) |
| log_info "Syncing bundled skills to ~/.hermes/skills/ ..." |
| if "$INSTALL_DIR/venv/bin/python" "$INSTALL_DIR/tools/skills_sync.py" 2>/dev/null; then |
| log_success "Skills synced to ~/.hermes/skills/" |
| else |
| # Fallback: simple directory copy if Python sync fails |
| if [ -d "$INSTALL_DIR/skills" ] && [ ! "$(ls -A "$HERMES_HOME/skills/" 2>/dev/null | grep -v '.bundled_manifest')" ]; then |
| cp -r "$INSTALL_DIR/skills/"* "$HERMES_HOME/skills/" 2>/dev/null || true |
| log_success "Skills copied to ~/.hermes/skills/" |
| fi |
| fi |
| } |
| |
| install_node_deps() { |
| if [ "$HAS_NODE" = false ]; then |
| log_info "Skipping Node.js dependencies (Node not installed)" |
| return 0 |
| fi |
| |
| if [ -f "$INSTALL_DIR/package.json" ]; then |
| log_info "Installing Node.js dependencies (browser tools)..." |
| cd "$INSTALL_DIR" |
| npm install --silent 2>/dev/null || { |
| log_warn "npm install failed (browser tools may not work)" |
| } |
| log_success "Node.js dependencies installed" |
| |
| # Install Playwright browser + system dependencies. |
| # Playwright's install-deps only supports apt/dnf/zypper natively. |
| |
| log_info "Installing browser engine (Playwright Chromium)..." |
| case "$DISTRO" in |
| arch|manjaro) |
| if command -v pacman &> /dev/null; then |
| log_info "Arch/Manjaro detected β installing Chromium system dependencies via pacman..." |
| if command -v sudo &> /dev/null && sudo -n true 2>/dev/null; then |
| sudo NEEDRESTART_MODE=a pacman -S --noconfirm --needed \ |
| nss atk at-spi2-core cups libdrm libxkbcommon mesa pango cairo alsa-lib >/dev/null 2>&1 || true |
| elif [ "$(id -u)" -eq 0 ]; then |
| pacman -S --noconfirm --needed \ |
| nss atk at-spi2-core cups libdrm libxkbcommon mesa pango cairo alsa-lib >/dev/null 2>&1 || true |
| else |
| log_warn "Cannot install browser deps without sudo. Run manually:" |
| log_warn " sudo pacman -S nss atk at-spi2-core cups libdrm libxkbcommon mesa pango cairo alsa-lib" |
| fi |
| fi |
| cd "$INSTALL_DIR" && npx playwright install chromium 2>/dev/null || true |
| ;; |
| *) |
| log_info "Playwright may request sudo to install browser system dependencies (shared libraries)." |
| log_info "This is standard Playwright setup β Hermes itself does not require root access." |
| cd "$INSTALL_DIR" && npx playwright install --with-deps chromium 2>/dev/null || true |
| ;; |
| esac |
| log_success "Browser engine installed" |
| fi |
|
|
| |
| if [ -f "$INSTALL_DIR/scripts/whatsapp-bridge/package.json" ]; then |
| log_info "Installing WhatsApp bridge dependencies..." |
| cd "$INSTALL_DIR/scripts/whatsapp-bridge" |
| npm install --silent 2>/dev/null || { |
| log_warn "WhatsApp bridge npm install failed (WhatsApp may not work)" |
| } |
| log_success "WhatsApp bridge dependencies installed" |
| fi |
| } |
|
|
| run_setup_wizard() { |
| if [ "$RUN_SETUP" = false ]; then |
| log_info "Skipping setup wizard (--skip-setup)" |
| return 0 |
| fi |
|
|
| |
| |
| |
| if ! [ -e /dev/tty ]; then |
| log_info "Setup wizard skipped (no terminal available). Run 'hermes setup' after install." |
| return 0 |
| fi |
|
|
| echo "" |
| log_info "Starting setup wizard..." |
| echo "" |
|
|
| cd "$INSTALL_DIR" |
|
|
| |
| |
| if [ "$USE_VENV" = true ]; then |
| "$INSTALL_DIR/venv/bin/python" -m hermes_cli.main setup < /dev/tty |
| else |
| python -m hermes_cli.main setup < /dev/tty |
| fi |
| } |
|
|
| maybe_start_gateway() { |
| |
| ENV_FILE="$HERMES_HOME/.env" |
| if [ ! -f "$ENV_FILE" ]; then |
| return 0 |
| fi |
|
|
| HAS_MESSAGING=false |
| for VAR in TELEGRAM_BOT_TOKEN DISCORD_BOT_TOKEN SLACK_BOT_TOKEN SLACK_APP_TOKEN WHATSAPP_ENABLED; do |
| VAL=$(grep "^${VAR}=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2-) |
| if [ -n "$VAL" ] && [ "$VAL" != "your-token-here" ]; then |
| HAS_MESSAGING=true |
| break |
| fi |
| done |
|
|
| if [ "$HAS_MESSAGING" = false ]; then |
| return 0 |
| fi |
|
|
| echo "" |
| log_info "Messaging platform token detected!" |
| log_info "The gateway needs to be running for Hermes to send/receive messages." |
|
|
| |
| WHATSAPP_VAL=$(grep "^WHATSAPP_ENABLED=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2-) |
| WHATSAPP_SESSION="$HERMES_HOME/whatsapp/session/creds.json" |
| if [ "$WHATSAPP_VAL" = "true" ] && [ ! -f "$WHATSAPP_SESSION" ]; then |
| if [ "$IS_INTERACTIVE" = true ]; then |
| echo "" |
| log_info "WhatsApp is enabled but not yet paired." |
| log_info "Running 'hermes whatsapp' to pair via QR code..." |
| echo "" |
| read -p "Pair WhatsApp now? [Y/n] " -n 1 -r |
| echo |
| if [[ $REPLY =~ ^[Yy]$ ]] || [[ -z $REPLY ]]; then |
| HERMES_CMD="$HOME/.local/bin/hermes" |
| [ ! -x "$HERMES_CMD" ] && HERMES_CMD="hermes" |
| $HERMES_CMD whatsapp || true |
| fi |
| else |
| log_info "WhatsApp pairing skipped (non-interactive). Run 'hermes whatsapp' to pair." |
| fi |
| fi |
|
|
| if ! [ -e /dev/tty ]; then |
| log_info "Gateway setup skipped (no terminal available). Run 'hermes gateway install' later." |
| return 0 |
| fi |
|
|
| echo "" |
| read -p "Would you like to install the gateway as a background service? [Y/n] " -n 1 -r < /dev/tty |
| echo |
|
|
| if [[ $REPLY =~ ^[Yy]$ ]] || [[ -z $REPLY ]]; then |
| HERMES_CMD="$HOME/.local/bin/hermes" |
| if [ ! -x "$HERMES_CMD" ]; then |
| HERMES_CMD="hermes" |
| fi |
|
|
| if command -v systemctl &> /dev/null; then |
| log_info "Installing systemd service..." |
| if $HERMES_CMD gateway install 2>/dev/null; then |
| log_success "Gateway service installed" |
| if $HERMES_CMD gateway start 2>/dev/null; then |
| log_success "Gateway started! Your bot is now online." |
| else |
| log_warn "Service installed but failed to start. Try: hermes gateway start" |
| fi |
| else |
| log_warn "Systemd install failed. You can start manually: hermes gateway" |
| fi |
| else |
| log_info "systemd not available β starting gateway in background..." |
| nohup $HERMES_CMD gateway > "$HERMES_HOME/logs/gateway.log" 2>&1 & |
| GATEWAY_PID=$! |
| log_success "Gateway started (PID $GATEWAY_PID). Logs: ~/.hermes/logs/gateway.log" |
| log_info "To stop: kill $GATEWAY_PID" |
| log_info "To restart later: hermes gateway" |
| fi |
| else |
| log_info "Skipped. Start the gateway later with: hermes gateway" |
| fi |
| } |
|
|
| print_success() { |
| echo "" |
| echo -e "${GREEN}${BOLD}" |
| echo "βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ" |
| echo "β β Installation Complete! β" |
| echo "βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ" |
| echo -e "${NC}" |
| echo "" |
|
|
| |
| echo -e "${CYAN}${BOLD}π Your files (all in ~/.hermes/):${NC}" |
| echo "" |
| echo -e " ${YELLOW}Config:${NC} ~/.hermes/config.yaml" |
| echo -e " ${YELLOW}API Keys:${NC} ~/.hermes/.env" |
| echo -e " ${YELLOW}Data:${NC} ~/.hermes/cron/, sessions/, logs/" |
| echo -e " ${YELLOW}Code:${NC} ~/.hermes/hermes-agent/" |
| echo "" |
|
|
| echo -e "${CYAN}βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ${NC}" |
| echo "" |
| echo -e "${CYAN}${BOLD}π Commands:${NC}" |
| echo "" |
| echo -e " ${GREEN}hermes${NC} Start chatting" |
| echo -e " ${GREEN}hermes setup${NC} Configure API keys & settings" |
| echo -e " ${GREEN}hermes config${NC} View/edit configuration" |
| echo -e " ${GREEN}hermes config edit${NC} Open config in editor" |
| echo -e " ${GREEN}hermes gateway install${NC} Install gateway service (messaging + cron)" |
| echo -e " ${GREEN}hermes update${NC} Update to latest version" |
| echo "" |
|
|
| echo -e "${CYAN}βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ${NC}" |
| echo "" |
| echo -e "${YELLOW}β‘ Reload your shell to use 'hermes' command:${NC}" |
| echo "" |
| echo " source ~/.bashrc # or ~/.zshrc" |
| echo "" |
|
|
| |
| if [ "$HAS_NODE" = false ]; then |
| echo -e "${YELLOW}" |
| echo "Note: Node.js could not be installed automatically." |
| echo "Browser tools need Node.js. Install manually:" |
| echo " https://nodejs.org/en/download/" |
| echo -e "${NC}" |
| fi |
|
|
| |
| if [ "$HAS_RIPGREP" = false ]; then |
| echo -e "${YELLOW}" |
| echo "Note: ripgrep (rg) was not found. File search will use" |
| echo "grep as a fallback. For faster search in large codebases," |
| echo "install ripgrep: sudo apt install ripgrep (or brew install ripgrep)" |
| echo -e "${NC}" |
| fi |
| } |
|
|
| |
| |
| |
|
|
| main() { |
| print_banner |
|
|
| detect_os |
| install_uv |
| check_python |
| check_git |
| check_node |
| install_system_packages |
|
|
| clone_repo |
| setup_venv |
| install_deps |
| install_node_deps |
| setup_path |
| copy_config_templates |
| run_setup_wizard |
| maybe_start_gateway |
|
|
| print_success |
| } |
|
|
| main |
|
|