| #!/bin/bash
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| set -e
|
|
|
|
|
|
|
|
|
|
|
| if [ -n "${PYTHONPATH:-}" ]; then
|
| echo "⚠ Ignoring inherited PYTHONPATH during install to avoid module shadowing"
|
| unset PYTHONPATH
|
| fi
|
| if [ -n "${PYTHONHOME:-}" ]; then
|
| echo "⚠ Ignoring inherited PYTHONHOME during install"
|
| unset PYTHONHOME
|
| fi
|
|
|
|
|
|
|
| export UV_NO_CONFIG=1
|
|
|
|
|
| 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="${HERMES_HOME:-$HOME/.hermes}"
|
|
|
|
|
|
|
| if [ -n "${HERMES_INSTALL_DIR:-}" ]; then
|
| INSTALL_DIR="$HERMES_INSTALL_DIR"
|
| INSTALL_DIR_EXPLICIT=true
|
| else
|
| INSTALL_DIR=""
|
| INSTALL_DIR_EXPLICIT=false
|
| fi
|
| PYTHON_VERSION="3.11"
|
| NODE_VERSION="22"
|
|
|
|
|
|
|
|
|
|
|
| ROOT_FHS_LAYOUT=false
|
| DETECTED_BROWSER_EXECUTABLE=""
|
|
|
|
|
| USE_VENV=true
|
| RUN_SETUP=true
|
| SKIP_BROWSER=false
|
| BRANCH="main"
|
| ENSURE_DEPS=""
|
| POSTINSTALL_MODE=false
|
|
|
|
|
|
|
|
|
| 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
|
| ;;
|
| --skip-browser|--no-playwright)
|
| SKIP_BROWSER=true
|
| shift
|
| ;;
|
| --branch)
|
| BRANCH="$2"
|
| shift 2
|
| ;;
|
| --dir)
|
| INSTALL_DIR="$2"
|
| INSTALL_DIR_EXPLICIT=true
|
| shift 2
|
| ;;
|
| --hermes-home)
|
| HERMES_HOME="$2"
|
| shift 2
|
| ;;
|
| --ensure)
|
| ENSURE_DEPS="$2"
|
| shift 2
|
| ;;
|
| --postinstall)
|
| POSTINSTALL_MODE=true
|
| shift
|
| ;;
|
| -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 " --skip-browser Skip Playwright/Chromium install (browser tools won't work)"
|
| echo " --branch NAME Git branch to install (default: main)"
|
| echo " --dir PATH Installation directory"
|
| echo " default (non-root): ~/.hermes/hermes-agent"
|
| echo " default (root, Linux): /usr/local/lib/hermes-agent"
|
| echo " --hermes-home PATH Data directory (default: ~/.hermes, or \$HERMES_HOME)"
|
| echo " -h, --help Show this help"
|
| echo ""
|
| echo "Notes:"
|
| echo " When running as root on Linux, Hermes installs the code under"
|
| echo " /usr/local/lib/hermes-agent and links the command into"
|
| echo " /usr/local/bin/hermes (FHS layout — matches Claude Code / Codex CLI)."
|
| echo " Data, config, sessions, and logs still live in \$HERMES_HOME"
|
| echo " (default /root/.hermes). This keeps Docker bind-mounted volumes"
|
| echo " small and ensures the command is on PATH for all shells."
|
| echo " Existing installs at \$HERMES_HOME/hermes-agent are preserved in-place."
|
| echo " --ensure DEPS Install only specified deps (comma-separated)"
|
| echo " Supported: node, browser, ripgrep, ffmpeg"
|
| echo " Does NOT clone repo or create venv"
|
| echo " --postinstall Run post-install setup only (for pip users)"
|
| echo " Installs optional deps + runs hermes setup"
|
| echo " Does NOT clone repo or create venv"
|
| 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"
|
| }
|
|
|
| prompt_yes_no() {
|
| local question="$1"
|
| local default="${2:-yes}"
|
| local prompt_suffix
|
| local answer=""
|
|
|
|
|
| case "$default" in
|
| [yY]|[yY][eE][sS]|[tT][rR][uU][eE]|1) prompt_suffix="[Y/n]" ;;
|
| *) prompt_suffix="[y/N]" ;;
|
| esac
|
|
|
| if [ "$IS_INTERACTIVE" = true ]; then
|
| read -r -p "$question $prompt_suffix " answer || answer=""
|
| elif [ -r /dev/tty ] && [ -w /dev/tty ]; then
|
| printf "%s %s " "$question" "$prompt_suffix" > /dev/tty
|
| IFS= read -r answer < /dev/tty || answer=""
|
| else
|
| answer=""
|
| fi
|
|
|
| answer="${answer#"${answer%%[![:space:]]*}"}"
|
| answer="${answer%"${answer##*[![:space:]]}"}"
|
|
|
| if [ -z "$answer" ]; then
|
| case "$default" in
|
| [yY]|[yY][eE][sS]|[tT][rR][uU][eE]|1) return 0 ;;
|
| *) return 1 ;;
|
| esac
|
| fi
|
|
|
| case "$answer" in
|
| [yY]|[yY][eE][sS]) return 0 ;;
|
| *) return 1 ;;
|
| esac
|
| }
|
|
|
| is_termux() {
|
| [ -n "${TERMUX_VERSION:-}" ] || [[ "${PREFIX:-}" == *"com.termux/files/usr"* ]]
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| resolve_install_layout() {
|
| if [ "$INSTALL_DIR_EXPLICIT" = true ]; then
|
| log_info "Install directory: $INSTALL_DIR (explicit)"
|
| return 0
|
| fi
|
|
|
|
|
| if is_termux; then
|
| INSTALL_DIR="$HERMES_HOME/hermes-agent"
|
| return 0
|
| fi
|
|
|
|
|
|
|
|
|
| if [ "$OS" = "linux" ] && [ "$(id -u)" -eq 0 ]; then
|
| if [ -d "$HERMES_HOME/hermes-agent/.git" ]; then
|
| INSTALL_DIR="$HERMES_HOME/hermes-agent"
|
| log_info "Existing install detected at $INSTALL_DIR — keeping legacy layout"
|
| log_info " (new root installs use /usr/local/lib/hermes-agent)"
|
| return 0
|
| fi
|
| INSTALL_DIR="/usr/local/lib/hermes-agent"
|
| ROOT_FHS_LAYOUT=true
|
|
|
|
|
|
|
|
|
|
|
| export UV_PYTHON_INSTALL_DIR="${UV_PYTHON_INSTALL_DIR:-/usr/local/share/uv/python}"
|
| export UV_PYTHON_BIN_DIR="${UV_PYTHON_BIN_DIR:-/usr/local/share/uv/bin}"
|
| log_info "Root install on Linux — using FHS layout"
|
| log_info " Code: $INSTALL_DIR"
|
| log_info " Command: /usr/local/bin/hermes"
|
| log_info " Data: $HERMES_HOME (unchanged)"
|
| log_info " uv Python: $UV_PYTHON_INSTALL_DIR (world-readable)"
|
| return 0
|
| fi
|
|
|
|
|
| INSTALL_DIR="$HERMES_HOME/hermes-agent"
|
| }
|
|
|
| get_command_link_dir() {
|
| if is_termux && [ -n "${PREFIX:-}" ]; then
|
| echo "$PREFIX/bin"
|
| elif [ "$ROOT_FHS_LAYOUT" = true ]; then
|
| echo "/usr/local/bin"
|
| else
|
| echo "$HOME/.local/bin"
|
| fi
|
| }
|
|
|
| get_command_link_display_dir() {
|
| if is_termux && [ -n "${PREFIX:-}" ]; then
|
| echo '$PREFIX/bin'
|
| elif [ "$ROOT_FHS_LAYOUT" = true ]; then
|
| echo '/usr/local/bin'
|
| else
|
| echo '~/.local/bin'
|
| fi
|
| }
|
|
|
| get_hermes_command_path() {
|
| local link_dir
|
| link_dir="$(get_command_link_dir)"
|
| if [ -x "$link_dir/hermes" ]; then
|
| echo "$link_dir/hermes"
|
| else
|
| echo "hermes"
|
| fi
|
| }
|
|
|
|
|
|
|
|
|
|
|
| detect_os() {
|
| case "$(uname -s)" in
|
| Linux*)
|
| if is_termux; then
|
| OS="android"
|
| DISTRO="termux"
|
| else
|
| OS="linux"
|
| if [ -f /etc/os-release ]; then
|
| . /etc/os-release
|
| DISTRO="$ID"
|
| else
|
| DISTRO="unknown"
|
| fi
|
| fi
|
| ;;
|
| Darwin*)
|
| OS="macos"
|
| DISTRO="macos"
|
| ;;
|
| CYGWIN*|MINGW*|MSYS*)
|
| OS="windows"
|
| DISTRO="windows"
|
| log_error "Windows detected. Please use the PowerShell installer:"
|
| log_info " iex (irm https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1)"
|
| exit 1
|
| ;;
|
| *)
|
| OS="unknown"
|
| DISTRO="unknown"
|
| log_warn "Unknown operating system"
|
| ;;
|
| esac
|
|
|
| log_success "Detected: $OS ($DISTRO)"
|
| }
|
|
|
|
|
|
|
|
|
|
|
| install_uv() {
|
| if [ "$DISTRO" = "termux" ]; then
|
| log_info "Termux detected — using Python's stdlib venv + pip instead of uv"
|
| UV_CMD=""
|
| return 0
|
| fi
|
|
|
| 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)..."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| local _uv_install_log _uv_installer
|
| _uv_install_log="$(mktemp 2>/dev/null || echo "/tmp/hermes-uv-install.$$.log")"
|
| _uv_installer="$(mktemp 2>/dev/null || echo "/tmp/hermes-uv-installer.$$.sh")"
|
| if ! curl -LsSf https://astral.sh/uv/install.sh -o "$_uv_installer" 2>"$_uv_install_log"; then
|
| log_error "Failed to download uv installer from https://astral.sh/uv/install.sh"
|
| log_info "curl output:"
|
| sed 's/^/ /' "$_uv_install_log" >&2
|
| log_info "Install manually: https://docs.astral.sh/uv/getting-started/installation/"
|
| rm -f "$_uv_install_log" "$_uv_installer"
|
| exit 1
|
| fi
|
| if sh "$_uv_installer" >>"$_uv_install_log" 2>&1; then
|
| rm -f "$_uv_installer"
|
|
|
| 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 installer reported success but binary not found on PATH"
|
| log_info "Installer output:"
|
| sed 's/^/ /' "$_uv_install_log" >&2
|
| log_info "Try adding ~/.local/bin to your PATH and re-running"
|
| rm -f "$_uv_install_log"
|
| exit 1
|
| fi
|
| rm -f "$_uv_install_log"
|
| UV_VERSION=$($UV_CMD --version 2>/dev/null)
|
| log_success "uv installed ($UV_VERSION)"
|
| else
|
| log_error "Failed to install uv"
|
| log_info "Installer output:"
|
| sed 's/^/ /' "$_uv_install_log" >&2
|
| log_info "Install manually: https://docs.astral.sh/uv/getting-started/installation/"
|
| rm -f "$_uv_install_log" "$_uv_installer"
|
| exit 1
|
| fi
|
| }
|
|
|
| check_python() {
|
| if [ "$DISTRO" = "termux" ]; then
|
| log_info "Checking Termux Python..."
|
| if command -v python >/dev/null 2>&1; then
|
| PYTHON_PATH="$(command -v python)"
|
| if "$PYTHON_PATH" -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 11) else 1)' 2>/dev/null; then
|
| PYTHON_FOUND_VERSION="$("$PYTHON_PATH" --version 2>/dev/null)"
|
| log_success "Python found: $PYTHON_FOUND_VERSION"
|
| return 0
|
| fi
|
| fi
|
|
|
| log_info "Installing Python via pkg..."
|
| pkg install -y python >/dev/null
|
| PYTHON_PATH="$(command -v python)"
|
| PYTHON_FOUND_VERSION="$("$PYTHON_PATH" --version 2>/dev/null)"
|
| log_success "Python installed: $PYTHON_FOUND_VERSION"
|
| return 0
|
| fi
|
|
|
| log_info "Checking Python $PYTHON_VERSION..."
|
|
|
|
|
|
|
| if PYTHON_PATH="$("$UV_CMD" python find "$PYTHON_VERSION" 2>/dev/null)"; then
|
| 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"
|
|
|
| if [ "$DISTRO" = "termux" ]; then
|
| log_info "Installing Git via pkg..."
|
| pkg install -y git >/dev/null
|
| if command -v git >/dev/null 2>&1; then
|
| GIT_VERSION=$(git --version | awk '{print $3}')
|
| log_success "Git $GIT_VERSION installed"
|
| return 0
|
| fi
|
| fi
|
|
|
| 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
|
| ;;
|
| android)
|
| log_info " pkg install git"
|
| ;;
|
| 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
|
|
|
| if [ "$DISTRO" = "termux" ]; then
|
| log_info "Node.js not found — installing Node.js via pkg..."
|
| else
|
| log_info "Node.js not found — installing Node.js $NODE_VERSION LTS..."
|
| fi
|
| install_node
|
| }
|
|
|
| install_node() {
|
| if [ "$DISTRO" = "termux" ]; then
|
| log_info "Installing Node.js via pkg..."
|
| if pkg install -y nodejs >/dev/null; then
|
| local installed_ver
|
| installed_ver=$(node --version 2>/dev/null)
|
| log_success "Node.js $installed_ver installed via pkg"
|
| HAS_NODE=true
|
| else
|
| log_warn "Failed to install Node.js via pkg"
|
| HAS_NODE=false
|
| fi
|
| return 0
|
| fi
|
|
|
| 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
|
| }
|
|
|
| check_network_prerequisites() {
|
| log_info "Checking internet connectivity for package install and web tools..."
|
|
|
| local url
|
| local failed=false
|
| local checks=("https://pypi.org/simple/" "https://duckduckgo.com/")
|
|
|
| if ! command -v curl >/dev/null 2>&1; then
|
| log_warn "curl not found; skipping connectivity probes"
|
| return 0
|
| fi
|
|
|
| for url in "${checks[@]}"; do
|
| if ! curl -fsSI --max-time 8 "$url" >/dev/null 2>&1; then
|
| failed=true
|
| log_warn "Could not reach $url"
|
| fi
|
| done
|
|
|
| if [ "$failed" = false ]; then
|
| log_success "Internet connectivity looks good"
|
| return 0
|
| fi
|
|
|
| if [ "$DISTRO" = "termux" ]; then
|
| log_warn "Termux network prerequisites may be incomplete."
|
| log_info "Try: pkg install -y ca-certificates curl && pkg update"
|
| log_info "If mirrors are stale: termux-change-repo"
|
| log_info "Then test: curl -I https://pypi.org/simple/ && curl -I https://duckduckgo.com/"
|
| else
|
| log_warn "Network checks failed. Hermes install may complete, but web search and dependency downloads can fail."
|
| log_info "Verify internet/DNS and retry if pip install fails."
|
| fi
|
| }
|
|
|
| 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 [ "$DISTRO" = "termux" ]; then
|
| local termux_pkgs=(clang rust make pkg-config libffi openssl ca-certificates curl)
|
| if [ "$need_ripgrep" = true ]; then
|
| termux_pkgs+=("ripgrep")
|
| fi
|
| if [ "$need_ffmpeg" = true ]; then
|
| termux_pkgs+=("ffmpeg")
|
| fi
|
|
|
| log_info "Installing Termux packages: ${termux_pkgs[*]}"
|
| if pkg install -y "${termux_pkgs[@]}" >/dev/null; then
|
| [ "$need_ripgrep" = true ] && HAS_RIPGREP=true && log_success "ripgrep installed"
|
| [ "$need_ffmpeg" = true ] && HAS_FFMPEG=true && log_success "ffmpeg installed"
|
| log_success "Termux build dependencies installed"
|
| return 0
|
| fi
|
|
|
| log_warn "Could not auto-install all Termux packages"
|
| log_info "Install manually: pkg install ${termux_pkgs[*]}"
|
| return 0
|
| 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."
|
| if prompt_yes_no "Install ${description}? (requires sudo)" "no"; 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 (: </dev/tty) 2>/dev/null; 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."
|
| if prompt_yes_no "Install ${description}?" "yes"; 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
|
| ;;
|
| android)
|
| log_info " pkg install $pkg"
|
| ;;
|
| 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="stash@{0}"
|
| 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
|
|
|
| if [ "$DISTRO" = "termux" ]; then
|
| log_info "Creating virtual environment with Termux Python..."
|
|
|
| if [ -d "venv" ]; then
|
| log_info "Virtual environment already exists, recreating..."
|
| rm -rf venv
|
| fi
|
|
|
| "$PYTHON_PATH" -m venv venv
|
| log_success "Virtual environment ready ($(./venv/bin/python --version 2>/dev/null))"
|
| 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 [ "$DISTRO" = "termux" ]; then
|
| if [ "$USE_VENV" = true ]; then
|
| export VIRTUAL_ENV="$INSTALL_DIR/venv"
|
| PIP_PYTHON="$INSTALL_DIR/venv/bin/python"
|
| else
|
| PIP_PYTHON="$PYTHON_PATH"
|
| fi
|
|
|
| if [ -z "${ANDROID_API_LEVEL:-}" ]; then
|
| ANDROID_API_LEVEL="$(getprop ro.build.version.sdk 2>/dev/null || true)"
|
| if [ -z "$ANDROID_API_LEVEL" ]; then
|
| ANDROID_API_LEVEL=24
|
| fi
|
| export ANDROID_API_LEVEL
|
| log_info "Using ANDROID_API_LEVEL=$ANDROID_API_LEVEL for Android wheel builds"
|
| fi
|
|
|
| "$PIP_PYTHON" -m pip install --upgrade pip setuptools wheel >/dev/null
|
|
|
|
|
|
|
|
|
|
|
|
|
| if "$PIP_PYTHON" -c 'import sys; raise SystemExit(0 if sys.platform == "android" else 1)' 2>/dev/null; then
|
| log_info "Android Python detected: prebuilding psutil compatibility shim..."
|
| if ! "$PIP_PYTHON" "$INSTALL_DIR/scripts/install_psutil_android.py" --pip "$PIP_PYTHON -m pip"; then
|
| log_warn "psutil Android prebuild failed — package install will likely fail next."
|
| log_info "Workaround: manually rerun 'python scripts/install_psutil_android.py' once your toolchain is set up."
|
| fi
|
| fi
|
|
|
|
|
|
|
| if ! "$PIP_PYTHON" -m pip install -e '.[termux-all]' -c constraints-termux.txt; then
|
| log_warn "Termux broad profile (.[termux-all]) failed, trying baseline Termux profile..."
|
| if ! "$PIP_PYTHON" -m pip install -e '.[termux]' -c constraints-termux.txt; then
|
| log_warn "Termux baseline profile (.[termux]) failed, trying base install..."
|
| if ! "$PIP_PYTHON" -m pip install -e '.' -c constraints-termux.txt; then
|
| log_error "Package installation failed on Termux."
|
| log_info "Ensure these packages are installed: pkg install clang rust make pkg-config libffi openssl ca-certificates curl"
|
| log_info "Then re-run: cd $INSTALL_DIR && python -m pip install -e '.[termux-all]' -c constraints-termux.txt"
|
| exit 1
|
| fi
|
| fi
|
| fi
|
|
|
| log_success "Main package installed"
|
| log_info "Termux note: matrix e2ee and local faster-whisper extras are excluded from .[termux-all] due to upstream Android wheel/toolchain blockers."
|
| log_info "Termux note: browser/WhatsApp tooling is not installed by default; see the Termux guide for optional follow-up steps."
|
|
|
| log_success "All dependencies installed"
|
| return 0
|
| fi
|
|
|
| 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."
|
| if prompt_yes_no "Install build tools?" "yes"; 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 [ -f "uv.lock" ]; then
|
| log_info "Trying tier: hash-verified (uv.lock) ..."
|
| log_info "(this resolves + downloads the curated [all] set — first run on a"
|
| log_info " fresh venv can take 1-5 minutes; uv prints progress below)"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| if UV_PROJECT_ENVIRONMENT="$INSTALL_DIR/venv" $UV_CMD sync --extra all --locked; then
|
| log_success "Main package installed (hash-verified via uv.lock)"
|
| log_success "All dependencies installed"
|
| return 0
|
| fi
|
| log_warn "uv.lock sync failed (see uv output above), falling back to PyPI resolve..."
|
| else
|
| log_info "uv.lock not found — falling back to PyPI resolve (no hash verification)"
|
| fi
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| local _BROKEN_EXTRAS=()
|
|
|
|
|
|
|
|
|
| local _ALL_EXTRAS_CSV
|
| _ALL_EXTRAS_CSV="$(
|
| "$PYTHON_PATH" - <<'PY' 2>/dev/null
|
| import re, sys, tomllib
|
| try:
|
| with open("pyproject.toml", "rb") as fh:
|
| data = tomllib.load(fh)
|
| specs = data["project"]["optional-dependencies"]["all"]
|
| extras = []
|
| for s in specs:
|
| m = re.search(r"hermes-agent\[([\w-]+)\]", s)
|
| if m:
|
| extras.append(m.group(1))
|
| print(",".join(extras))
|
| except Exception as e:
|
| print("", file=sys.stderr)
|
| sys.exit(1)
|
| PY
|
| )"
|
| if [ -z "$_ALL_EXTRAS_CSV" ]; then
|
| log_warn "Could not parse [all] from pyproject.toml; falling back to .[all] only."
|
| _ALL_EXTRAS_CSV=""
|
| fi
|
|
|
|
|
| local _SAFE_SPEC=".[all]"
|
| if [ -n "$_ALL_EXTRAS_CSV" ] && [ "${#_BROKEN_EXTRAS[@]}" -gt 0 ]; then
|
| local _SAFE_EXTRAS=()
|
| local _e _b _skip
|
| IFS=',' read -ra _ALL_EXTRAS_ARR <<< "$_ALL_EXTRAS_CSV"
|
| for _e in "${_ALL_EXTRAS_ARR[@]}"; do
|
| _skip=false
|
| for _b in "${_BROKEN_EXTRAS[@]}"; do
|
| if [ "$_e" = "$_b" ]; then _skip=true; break; fi
|
| done
|
| if [ "$_skip" = false ]; then _SAFE_EXTRAS+=("$_e"); fi
|
| done
|
| _SAFE_SPEC=".[$(IFS=,; echo "${_SAFE_EXTRAS[*]}")]"
|
| fi
|
|
|
| ALL_INSTALL_LOG=$(mktemp)
|
| local _installed=false
|
| local _tier_name=""
|
|
|
| install_tier() {
|
| local name="$1"; local spec="$2"
|
| log_info "Trying tier: $name ..."
|
| if $UV_CMD pip install -e "$spec" 2>"$ALL_INSTALL_LOG"; then
|
| log_success "Main package installed ($name)"
|
| _installed=true
|
| _tier_name="$name"
|
| return 0
|
| fi
|
| log_warn "Tier '$name' failed. Top of pip output:"
|
| head -5 "$ALL_INSTALL_LOG" | sed 's/^/ /' >&2
|
| return 1
|
| }
|
|
|
| install_tier "all" ".[all]" \
|
| || install_tier "all minus known-broken (${_BROKEN_EXTRAS[*]:-none})" "$_SAFE_SPEC" \
|
| || install_tier "core only (no extras)" "."
|
|
|
| rm -f "$ALL_INSTALL_LOG"
|
|
|
| if [ "$_installed" = false ]; then
|
| log_error "Package installation failed even with no extras."
|
| 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
|
|
|
| if [ "$_tier_name" != "all (with RL/matrix extras)" ]; then
|
| log_warn "Note: installed via fallback tier ($_tier_name)."
|
| log_info "Some optional features may be missing. After resolving any"
|
| log_info "PyPI/network issue, re-run: $UV_CMD pip install -e '.[all]'"
|
| fi
|
|
|
| log_success "Main package installed"
|
|
|
| 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."
|
| if [ "$DISTRO" = "termux" ]; then
|
| log_info "Try: cd $INSTALL_DIR && python -m pip install -e '.[termux-all]' -c constraints-termux.txt"
|
| else
|
| log_info "Try: cd $INSTALL_DIR && uv pip install -e '.[all]'"
|
| fi
|
| return 0
|
| fi
|
|
|
| local command_link_dir
|
| local command_link_display_dir
|
| command_link_dir="$(get_command_link_dir)"
|
| command_link_display_dir="$(get_command_link_display_dir)"
|
|
|
|
|
|
|
|
|
| mkdir -p "$command_link_dir"
|
|
|
|
|
|
|
| rm -f "$command_link_dir/hermes"
|
| cat > "$command_link_dir/hermes" <<EOF
|
| #!/usr/bin/env bash
|
| unset PYTHONPATH
|
| unset PYTHONHOME
|
| exec "$HERMES_BIN" "\$@"
|
| EOF
|
| chmod +x "$command_link_dir/hermes"
|
| log_success "Installed hermes launcher → $command_link_display_dir/hermes"
|
|
|
| if [ "$DISTRO" = "termux" ]; then
|
| export PATH="$command_link_dir:$PATH"
|
| log_info "$command_link_display_dir is the native Termux command path"
|
| log_success "hermes command ready"
|
| return 0
|
| fi
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| if [ "$ROOT_FHS_LAYOUT" = true ]; then
|
| export PATH="$command_link_dir:$PATH"
|
|
|
|
|
|
|
| if env -i HOME="$HOME" TERM="${TERM:-dumb}" bash -i -c 'command -v hermes' \
|
| >/dev/null 2>&1; then
|
| log_info "/usr/local/bin is already on PATH for all shells"
|
| log_success "hermes command ready"
|
| return 0
|
| fi
|
|
|
| log_info "hermes not on PATH in non-login shells (common on RHEL-family)"
|
| PATH_LINE='export PATH="/usr/local/bin:$PATH"'
|
| PATH_COMMENT='# Hermes Agent — ensure /usr/local/bin is on PATH (RHEL non-login shells)'
|
| for SHELL_CONFIG in "$HOME/.bashrc" "$HOME/.bash_profile"; do
|
| [ -f "$SHELL_CONFIG" ] || continue
|
| if ! grep -v '^[[:space:]]*#' "$SHELL_CONFIG" 2>/dev/null \
|
| | grep -qE 'PATH=.*(/usr/local/bin|\$command_link_dir)'; then
|
| echo "" >> "$SHELL_CONFIG"
|
| echo "$PATH_COMMENT" >> "$SHELL_CONFIG"
|
| echo "$PATH_LINE" >> "$SHELL_CONFIG"
|
| log_success "Added /usr/local/bin to PATH in $SHELL_CONFIG"
|
| fi
|
| done
|
| log_success "hermes command ready"
|
| return 0
|
| fi
|
|
|
|
|
|
|
|
|
| if ! echo "$PATH" | tr ':' '\n' | grep -q "^$command_link_dir$"; then
|
| SHELL_CONFIGS=()
|
| IS_FISH=false
|
| 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")
|
| ;;
|
| fish)
|
|
|
| IS_FISH=true
|
| FISH_CONFIG="$HOME/.config/fish/config.fish"
|
| mkdir -p "$(dirname "$FISH_CONFIG")"
|
| touch "$FISH_CONFIG"
|
| ;;
|
| *)
|
| [ -f "$HOME/.bashrc" ] && SHELL_CONFIGS+=("$HOME/.bashrc")
|
| [ -f "$HOME/.zshrc" ] && SHELL_CONFIGS+=("$HOME/.zshrc")
|
| ;;
|
| esac
|
|
|
|
|
| [ "$IS_FISH" = "false" ] && [ -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 [ "$IS_FISH" = "true" ]; then
|
| if ! grep -q 'fish_add_path.*\.local/bin' "$FISH_CONFIG" 2>/dev/null; then
|
| echo "" >> "$FISH_CONFIG"
|
| echo "# Hermes Agent — ensure ~/.local/bin is on PATH" >> "$FISH_CONFIG"
|
| echo 'fish_add_path "$HOME/.local/bin"' >> "$FISH_CONFIG"
|
| log_success "Added ~/.local/bin to PATH in $FISH_CONFIG"
|
| fi
|
| fi
|
|
|
| if [ "$IS_FISH" = "false" ] && [ ${#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="$command_link_dir:$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}
|
|
|
|
|
| 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
|
|
|
|
|
|
|
| chmod 600 "$HERMES_HOME/.env"
|
| configure_browser_env_from_system_browser
|
|
|
|
|
| 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
|
| }
|
|
|
| find_system_browser() {
|
| # Prefer a user-specified browser path, then common Linux/macOS Chrome and
|
| # Chromium command names. Arch-family distributions commonly ship plain
|
| # `chromium`, while Debian-family systems often use `chromium-browser`.
|
| if [ -n "${AGENT_BROWSER_EXECUTABLE_PATH:-}" ]; then
|
| if [ -x "$AGENT_BROWSER_EXECUTABLE_PATH" ]; then
|
| echo "$AGENT_BROWSER_EXECUTABLE_PATH"
|
| return 0
|
| fi
|
| if command -v "$AGENT_BROWSER_EXECUTABLE_PATH" >/dev/null 2>&1; then
|
| command -v "$AGENT_BROWSER_EXECUTABLE_PATH"
|
| return 0
|
| fi
|
| fi
|
|
|
| local candidate
|
| for candidate in google-chrome google-chrome-stable chromium chromium-browser chrome; do
|
| if command -v "$candidate" >/dev/null 2>&1; then
|
| command -v "$candidate"
|
| return 0
|
| fi
|
| done
|
|
|
| if [ "$(uname)" = "Darwin" ]; then
|
| for app in \
|
| "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
|
| "/Applications/Chromium.app/Contents/MacOS/Chromium"; do
|
| if [ -x "$app" ]; then
|
| echo "$app"
|
| return 0
|
| fi
|
| done
|
| fi
|
|
|
| return 1
|
| }
|
|
|
| run_browser_install_with_timeout() {
|
| local timeout_seconds="$1"
|
| shift
|
|
|
| if command -v timeout >/dev/null 2>&1; then
|
| timeout "$timeout_seconds" "$@"
|
| else
|
| "$@"
|
| fi
|
| }
|
|
|
| configure_browser_env_from_system_browser() {
|
| local env_file="$HERMES_HOME/.env"
|
| local browser_path="${DETECTED_BROWSER_EXECUTABLE:-}"
|
|
|
| if [ -z "$browser_path" ]; then
|
| browser_path="$(find_system_browser 2>/dev/null || true)"
|
| fi
|
|
|
| if [ -z "$browser_path" ]; then
|
| return 0
|
| fi
|
|
|
| mkdir -p "$HERMES_HOME"
|
| if [ ! -f "$env_file" ]; then
|
| touch "$env_file"
|
| fi
|
|
|
| if grep -q '^AGENT_BROWSER_EXECUTABLE_PATH=' "$env_file" 2>/dev/null; then
|
| log_info "AGENT_BROWSER_EXECUTABLE_PATH already configured"
|
| return 0
|
| fi
|
|
|
| {
|
| echo ""
|
| echo "# Hermes Agent browser tools — use the system Chrome/Chromium binary."
|
| echo "AGENT_BROWSER_EXECUTABLE_PATH=$browser_path"
|
| } >> "$env_file"
|
| log_success "Configured browser tools to use $browser_path"
|
| }
|
|
|
| install_node_deps() {
|
| if [ "$HAS_NODE" = false ]; then
|
| log_info "Skipping Node.js dependencies (Node not installed)"
|
| return 0
|
| fi
|
|
|
| if [ "$DISTRO" = "termux" ]; then
|
| log_info "Skipping automatic Node/browser dependency setup on Termux"
|
| log_info "Browser automation is not part of the tested Termux install path yet."
|
| log_info "If you want to experiment manually later, run: cd $INSTALL_DIR && npm install"
|
| 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 --with-deps only supports apt-based systems natively.
|
|
|
|
|
| if [ "$SKIP_BROWSER" = true ]; then
|
| log_info "Skipping Playwright/Chromium install (--skip-browser)"
|
| log_info "Browser tools will be unavailable until you run manually:"
|
| log_info " cd $INSTALL_DIR && npx playwright install chromium"
|
| log_info "On apt-based systems, an admin also needs to run:"
|
| log_info " sudo npx playwright install-deps chromium"
|
| else
|
| log_info "Installing browser engine (Playwright Chromium)..."
|
| DETECTED_BROWSER_EXECUTABLE="$(find_system_browser 2>/dev/null || true)"
|
| if [ -n "$DETECTED_BROWSER_EXECUTABLE" ]; then
|
| log_success "Found system Chrome/Chromium at $DETECTED_BROWSER_EXECUTABLE"
|
| log_info "Skipping Playwright browser download; Hermes will use the system browser."
|
| else
|
| case "$DISTRO" in
|
| ubuntu|debian|raspbian|pop|linuxmint|elementary|zorin|kali|parrot)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| if [ "$(id -u)" -eq 0 ] || (command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null); then
|
| log_info "Installing Playwright Chromium with system dependencies..."
|
| cd "$INSTALL_DIR" && run_browser_install_with_timeout 600 npx playwright install --with-deps chromium 2>/dev/null || {
|
| log_warn "Playwright browser installation failed — browser tools will not work."
|
| log_warn "Try running manually: cd $INSTALL_DIR && npx playwright install --with-deps chromium"
|
| }
|
| else
|
| log_warn "No sudo available — skipping system-library install (--with-deps)."
|
| log_info "Ask an administrator to run, one time, as root:"
|
| log_info " sudo npx playwright install-deps chromium"
|
| log_info " (from $INSTALL_DIR, after Node.js deps are installed)"
|
| log_info "Installing Chromium binary into this user's Playwright cache..."
|
| cd "$INSTALL_DIR" && run_browser_install_with_timeout 600 npx playwright install chromium 2>/dev/null || {
|
| log_warn "Playwright browser installation failed — browser tools will not work."
|
| log_warn "Try running manually: cd $INSTALL_DIR && npx playwright install chromium"
|
| }
|
| fi
|
| ;;
|
| arch|manjaro|cachyos|endeavouros|garuda)
|
| if command -v pacman &> /dev/null; then
|
| log_info "Arch-family distro 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" && run_browser_install_with_timeout 600 npx playwright install chromium 2>/dev/null || {
|
| log_warn "Playwright browser installation failed — browser tools will not work."
|
| }
|
| ;;
|
| fedora|rhel|centos|rocky|alma)
|
| log_warn "Playwright does not support automatic dependency installation on RPM-based systems."
|
| log_info "Install Chromium system dependencies manually before using browser tools:"
|
| log_info " sudo dnf install nss atk at-spi2-core cups-libs libdrm libxkbcommon mesa-libgbm pango cairo alsa-lib"
|
| cd "$INSTALL_DIR" && run_browser_install_with_timeout 600 npx playwright install chromium 2>/dev/null || {
|
| log_warn "Playwright browser installation failed — install dependencies above and retry."
|
| }
|
| ;;
|
| opensuse*|sles)
|
| log_warn "Playwright does not support automatic dependency installation on zypper-based systems."
|
| log_info "Install Chromium system dependencies manually before using browser tools:"
|
| log_info " sudo zypper install mozilla-nss libatk-1_0-0 at-spi2-core cups-libs libdrm2 libxkbcommon0 Mesa-libgbm1 pango cairo libasound2"
|
| cd "$INSTALL_DIR" && run_browser_install_with_timeout 600 npx playwright install chromium 2>/dev/null || {
|
| log_warn "Playwright browser installation failed — install dependencies above and retry."
|
| }
|
| ;;
|
| *)
|
| log_warn "Playwright does not support automatic dependency installation on $DISTRO."
|
| log_info "Install Chromium/browser system dependencies for your distribution, then run:"
|
| log_info " cd $INSTALL_DIR && npx playwright install chromium"
|
| log_info "Browser tools will not work until dependencies are installed."
|
| cd "$INSTALL_DIR" && run_browser_install_with_timeout 600 npx playwright install chromium 2>/dev/null || true
|
| ;;
|
| esac
|
| fi
|
| fi
|
| log_success "Browser engine setup complete"
|
| fi
|
|
|
|
|
| if [ -f "$INSTALL_DIR/ui-tui/package.json" ]; then
|
| log_info "Installing TUI dependencies..."
|
| cd "$INSTALL_DIR/ui-tui"
|
| npm install --silent 2>/dev/null || {
|
| log_warn "TUI npm install failed (hermes --tui may not work)"
|
| }
|
| log_success "TUI dependencies installed"
|
| fi
|
|
|
|
|
| }
|
|
|
| run_setup_wizard() {
|
| if [ "$RUN_SETUP" = false ]; then
|
| log_info "Skipping setup wizard (--skip-setup)"
|
| return 0
|
| fi
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| if ! (: </dev/tty) 2>/dev/null; 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 ""
|
| if prompt_yes_no "Pair WhatsApp now?" "yes"; then
|
| HERMES_CMD="$(get_hermes_command_path)"
|
| $HERMES_CMD whatsapp || true
|
| fi
|
| else
|
| log_info "WhatsApp pairing skipped (non-interactive). Run 'hermes whatsapp' to pair."
|
| fi
|
| fi
|
|
|
|
|
|
|
|
|
| if ! (: </dev/tty) 2>/dev/null; then
|
| log_info "Gateway setup skipped (no terminal available). Run 'hermes gateway install' later."
|
| return 0
|
| fi
|
|
|
| echo ""
|
| local should_install_gateway=false
|
| if [ "$DISTRO" = "termux" ]; then
|
| if prompt_yes_no "Would you like to start the gateway in the background?" "yes"; then
|
| should_install_gateway=true
|
| fi
|
| else
|
| if prompt_yes_no "Would you like to install the gateway as a background service?" "yes"; then
|
| should_install_gateway=true
|
| fi
|
| fi
|
|
|
| if [ "$should_install_gateway" = true ]; then
|
| HERMES_CMD="$(get_hermes_command_path)"
|
|
|
| if [ "$DISTRO" != "termux" ] && 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
|
| if [ "$DISTRO" = "termux" ]; then
|
| log_info "Termux detected — starting gateway in best-effort background mode..."
|
| else
|
| log_info "systemd not available — starting gateway in background..."
|
| fi
|
| 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"
|
| if [ "$DISTRO" = "termux" ]; then
|
| log_warn "Android may stop background processes when Termux is suspended or the system reclaims resources."
|
| fi
|
| 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:${NC}"
|
| echo ""
|
| echo -e " ${YELLOW}Config:${NC} $HERMES_HOME/config.yaml"
|
| echo -e " ${YELLOW}API Keys:${NC} $HERMES_HOME/.env"
|
| echo -e " ${YELLOW}Data:${NC} $HERMES_HOME/cron/, sessions/, logs/"
|
| echo -e " ${YELLOW}Code:${NC} $INSTALL_DIR"
|
| 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 ""
|
| if [ "$DISTRO" = "termux" ]; then
|
| echo -e "${YELLOW}⚡ 'hermes' was linked into $(get_command_link_display_dir), which is already on PATH in Termux.${NC}"
|
| echo ""
|
| elif [ "$ROOT_FHS_LAYOUT" = true ]; then
|
| echo -e "${YELLOW}⚡ 'hermes' was linked into /usr/local/bin and is ready to use — no shell reload needed.${NC}"
|
| echo ""
|
| else
|
| echo -e "${YELLOW}⚡ Reload your shell to use 'hermes' command:${NC}"
|
| echo ""
|
| LOGIN_SHELL="$(basename "${SHELL:-/bin/bash}")"
|
| if [ "$LOGIN_SHELL" = "zsh" ]; then
|
| echo " source ~/.zshrc"
|
| elif [ "$LOGIN_SHELL" = "bash" ]; then
|
| echo " source ~/.bashrc"
|
| elif [ "$LOGIN_SHELL" = "fish" ]; then
|
| echo " source ~/.config/fish/config.fish"
|
| else
|
| echo " source ~/.bashrc # or ~/.zshrc"
|
| fi
|
| echo ""
|
| fi
|
|
|
|
|
| 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:"
|
| if [ "$DISTRO" = "termux" ]; then
|
| echo " pkg install nodejs"
|
| else
|
| echo " https://nodejs.org/en/download/"
|
| fi
|
| 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,"
|
| if [ "$DISTRO" = "termux" ]; then
|
| echo "install ripgrep: pkg install ripgrep"
|
| else
|
| echo "install ripgrep: sudo apt install ripgrep (or brew install ripgrep)"
|
| fi
|
| echo -e "${NC}"
|
| fi
|
| }
|
|
|
| ensure_browser() {
|
| if ! command -v node >/dev/null 2>&1; then
|
| local node_bin="$HERMES_HOME/node/bin/node"
|
| if [ -x "$node_bin" ]; then
|
| export PATH="$HERMES_HOME/node/bin:$PATH"
|
| else
|
| log_error "Node.js not found. Run with --ensure node first."
|
| return 1
|
| fi
|
| fi
|
|
|
| local npm_bin
|
| npm_bin="$(command -v npm 2>/dev/null || echo "$HERMES_HOME/node/bin/npm")"
|
| if [ ! -x "$npm_bin" ]; then
|
| log_error "npm not found"
|
| return 1
|
| fi
|
|
|
| log_info "Installing agent-browser..."
|
| local log_file
|
| log_file="$(mktemp)"
|
| if ! "$npm_bin" install -g --prefix "$HERMES_HOME/node" --silent --ignore-scripts \
|
| "agent-browser@^0.26.0" \
|
| "@askjo/camofox-browser@^1.5.2" \
|
| >"$log_file" 2>&1; then
|
| log_error "npm install failed:"
|
| cat "$log_file" >&2
|
| rm -f "$log_file"
|
| return 1
|
| fi
|
| rm -f "$log_file"
|
| export PATH="$HERMES_HOME/node/bin:$PATH"
|
|
|
| local sys_browser
|
| sys_browser="$(find_system_browser 2>/dev/null || true)"
|
| if [ -n "$sys_browser" ]; then
|
| configure_browser_env_from_system_browser "$sys_browser"
|
| log_info "System browser detected -- skipping Chromium download"
|
| return 0
|
| fi
|
|
|
| log_info "Installing Chromium via agent-browser install..."
|
| local ab_bin="$HERMES_HOME/node/bin/agent-browser"
|
| if [ -x "$ab_bin" ]; then
|
| "$ab_bin" install 2>/dev/null || {
|
| log_warn "Chromium install failed. Browser tools may not work without a system browser."
|
|
|
|
|
| case "${DISTRO:-unknown}" in
|
| ubuntu|debian)
|
| log_info "Try: sudo apt-get install -y chromium-browser"
|
| ;;
|
| arch)
|
| log_info "Try: sudo pacman -S chromium"
|
| ;;
|
| fedora|rhel|centos)
|
| log_info "Try: sudo dnf install -y chromium"
|
| ;;
|
| esac
|
| }
|
| else
|
| log_warn "agent-browser not found at $ab_bin"
|
| fi
|
|
|
| return 0
|
| }
|
|
|
| ensure_mode() {
|
| detect_os
|
|
|
| IFS=',' read -ra DEPS <<< "$ENSURE_DEPS"
|
| for dep in "${DEPS[@]}"; do
|
| dep="$(echo "$dep" | tr -d '[:space:]')"
|
| case "$dep" in
|
| node)
|
| check_node
|
| ;;
|
| browser)
|
| check_node
|
| if [ "$HAS_NODE" = true ]; then
|
| ensure_browser
|
| fi
|
| ;;
|
| ripgrep)
|
| if ! command -v rg &>/dev/null; then
|
| HAS_RIPGREP=false
|
| HAS_FFMPEG=true
|
| install_system_packages
|
| fi
|
| ;;
|
| ffmpeg)
|
| if ! command -v ffmpeg &>/dev/null; then
|
| HAS_FFMPEG=false
|
| HAS_RIPGREP=true
|
| install_system_packages
|
| fi
|
| ;;
|
| *)
|
| log_warn "Unknown dependency: $dep"
|
| ;;
|
| esac
|
| done
|
| }
|
|
|
| postinstall_mode() {
|
| print_banner
|
| detect_os
|
|
|
| log_info "Post-install mode: setting up Hermes for pip install"
|
|
|
| check_node
|
| check_network_prerequisites
|
| install_system_packages
|
|
|
| if [ "$HAS_NODE" = true ] && [ "$SKIP_BROWSER" = false ]; then
|
| ensure_browser
|
| fi
|
|
|
| HERMES_CMD="$(command -v hermes 2>/dev/null || echo "")"
|
| if [ -n "$HERMES_CMD" ]; then
|
| log_info "Running hermes setup..."
|
| "$HERMES_CMD" setup
|
| else
|
| log_warn "hermes command not found on PATH"
|
| log_info "Try: python -m hermes_cli.main setup"
|
| fi
|
| }
|
|
|
|
|
|
|
|
|
|
|
| main() {
|
| print_banner
|
|
|
| detect_os
|
| resolve_install_layout
|
| install_uv
|
| check_python
|
| check_git
|
| check_node
|
| check_network_prerequisites
|
| 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
|
|
|
| echo "git" > "$HERMES_HOME/.install_method"
|
| }
|
|
|
| if [ -n "$ENSURE_DEPS" ]; then
|
| ensure_mode
|
| elif [ "$POSTINSTALL_MODE" = true ]; then
|
| postinstall_mode
|
| else
|
| main
|
| fi
|
|
|