install.sh
1 #!/bin/bash 2 # ============================================================================ 3 # Hermes Agent Installer 4 # ============================================================================ 5 # Installation script for Linux, macOS, and Android/Termux. 6 # Uses uv for desktop/server installs and Python's stdlib venv + pip on Termux. 7 # 8 # Usage: 9 # curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash 10 # 11 # Or with options: 12 # curl -fsSL ... | bash -s -- --no-venv --skip-setup 13 # 14 # ============================================================================ 15 16 set -e 17 18 # Colors 19 RED='\033[0;31m' 20 GREEN='\033[0;32m' 21 YELLOW='\033[0;33m' 22 BLUE='\033[0;34m' 23 MAGENTA='\033[0;35m' 24 CYAN='\033[0;36m' 25 NC='\033[0m' # No Color 26 BOLD='\033[1m' 27 28 # Configuration 29 REPO_URL_SSH="git@github.com:NousResearch/hermes-agent.git" 30 REPO_URL_HTTPS="https://github.com/NousResearch/hermes-agent.git" 31 HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}" 32 # INSTALL_DIR is resolved AFTER arg parsing and OS detection so we can pick an 33 # FHS-style layout for root installs. Track whether the user gave us an 34 # explicit directory — if so we never override it. 35 if [ -n "${HERMES_INSTALL_DIR:-}" ]; then 36 INSTALL_DIR="$HERMES_INSTALL_DIR" 37 INSTALL_DIR_EXPLICIT=true 38 else 39 INSTALL_DIR="" 40 INSTALL_DIR_EXPLICIT=false 41 fi 42 PYTHON_VERSION="3.11" 43 NODE_VERSION="22" 44 45 # FHS-style root install layout (set by resolve_install_layout when applicable): 46 # code at /usr/local/lib/hermes-agent, command at /usr/local/bin/hermes, 47 # data still at /root/.hermes (HERMES_HOME). Matches Claude Code / Codex CLI 48 # and keeps Docker bind-mounted /root/ volumes lean. 49 ROOT_FHS_LAYOUT=false 50 51 # Options 52 USE_VENV=true 53 RUN_SETUP=true 54 BRANCH="main" 55 56 # Detect non-interactive mode (e.g. curl | bash) 57 # When stdin is not a terminal, read -p will fail with EOF, 58 # causing set -e to silently abort the entire script. 59 if [ -t 0 ]; then 60 IS_INTERACTIVE=true 61 else 62 IS_INTERACTIVE=false 63 fi 64 65 # Parse arguments 66 while [[ $# -gt 0 ]]; do 67 case $1 in 68 --no-venv) 69 USE_VENV=false 70 shift 71 ;; 72 --skip-setup) 73 RUN_SETUP=false 74 shift 75 ;; 76 --branch) 77 BRANCH="$2" 78 shift 2 79 ;; 80 --dir) 81 INSTALL_DIR="$2" 82 INSTALL_DIR_EXPLICIT=true 83 shift 2 84 ;; 85 --hermes-home) 86 HERMES_HOME="$2" 87 shift 2 88 ;; 89 -h|--help) 90 echo "Hermes Agent Installer" 91 echo "" 92 echo "Usage: install.sh [OPTIONS]" 93 echo "" 94 echo "Options:" 95 echo " --no-venv Don't create virtual environment" 96 echo " --skip-setup Skip interactive setup wizard" 97 echo " --branch NAME Git branch to install (default: main)" 98 echo " --dir PATH Installation directory" 99 echo " default (non-root): ~/.hermes/hermes-agent" 100 echo " default (root, Linux): /usr/local/lib/hermes-agent" 101 echo " --hermes-home PATH Data directory (default: ~/.hermes, or \$HERMES_HOME)" 102 echo " -h, --help Show this help" 103 echo "" 104 echo "Notes:" 105 echo " When running as root on Linux, Hermes installs the code under" 106 echo " /usr/local/lib/hermes-agent and links the command into" 107 echo " /usr/local/bin/hermes (FHS layout — matches Claude Code / Codex CLI)." 108 echo " Data, config, sessions, and logs still live in \$HERMES_HOME" 109 echo " (default /root/.hermes). This keeps Docker bind-mounted volumes" 110 echo " small and ensures the command is on PATH for all shells." 111 echo " Existing installs at \$HERMES_HOME/hermes-agent are preserved in-place." 112 exit 0 113 ;; 114 *) 115 echo "Unknown option: $1" 116 exit 1 117 ;; 118 esac 119 done 120 121 # ============================================================================ 122 # Helper functions 123 # ============================================================================ 124 125 print_banner() { 126 echo "" 127 echo -e "${MAGENTA}${BOLD}" 128 echo "┌─────────────────────────────────────────────────────────┐" 129 echo "│ ⚕ Hermes Agent Installer │" 130 echo "├─────────────────────────────────────────────────────────┤" 131 echo "│ An open source AI agent by Nous Research. │" 132 echo "└─────────────────────────────────────────────────────────┘" 133 echo -e "${NC}" 134 } 135 136 log_info() { 137 echo -e "${CYAN}→${NC} $1" 138 } 139 140 log_success() { 141 echo -e "${GREEN}✓${NC} $1" 142 } 143 144 log_warn() { 145 echo -e "${YELLOW}⚠${NC} $1" 146 } 147 148 log_error() { 149 echo -e "${RED}✗${NC} $1" 150 } 151 152 prompt_yes_no() { 153 local question="$1" 154 local default="${2:-yes}" 155 local prompt_suffix 156 local answer="" 157 158 # Use case patterns (not ${var,,}) so this works on bash 3.2 (macOS /bin/bash). 159 case "$default" in 160 [yY]|[yY][eE][sS]|[tT][rR][uU][eE]|1) prompt_suffix="[Y/n]" ;; 161 *) prompt_suffix="[y/N]" ;; 162 esac 163 164 if [ "$IS_INTERACTIVE" = true ]; then 165 read -r -p "$question $prompt_suffix " answer || answer="" 166 elif [ -r /dev/tty ] && [ -w /dev/tty ]; then 167 printf "%s %s " "$question" "$prompt_suffix" > /dev/tty 168 IFS= read -r answer < /dev/tty || answer="" 169 else 170 answer="" 171 fi 172 173 answer="${answer#"${answer%%[![:space:]]*}"}" 174 answer="${answer%"${answer##*[![:space:]]}"}" 175 176 if [ -z "$answer" ]; then 177 case "$default" in 178 [yY]|[yY][eE][sS]|[tT][rR][uU][eE]|1) return 0 ;; 179 *) return 1 ;; 180 esac 181 fi 182 183 case "$answer" in 184 [yY]|[yY][eE][sS]) return 0 ;; 185 *) return 1 ;; 186 esac 187 } 188 189 is_termux() { 190 [ -n "${TERMUX_VERSION:-}" ] || [[ "${PREFIX:-}" == *"com.termux/files/usr"* ]] 191 } 192 193 # Decide where the repo checkout + venv live, and where the `hermes` command 194 # symlink goes. Called after detect_os so $OS/$DISTRO are known. 195 # 196 # Defaults: 197 # - Non-root, any OS: INSTALL_DIR = $HERMES_HOME/hermes-agent 198 # command link in $HOME/.local/bin 199 # - Termux (any uid): INSTALL_DIR = $HERMES_HOME/hermes-agent 200 # command link in $PREFIX/bin (already on PATH) 201 # - Root on Linux (new): INSTALL_DIR = /usr/local/lib/hermes-agent 202 # command link in /usr/local/bin 203 # (unless a legacy install already exists at 204 # $HERMES_HOME/hermes-agent — then preserve it) 205 # 206 # Always no-op when the user set --dir or $HERMES_INSTALL_DIR. 207 resolve_install_layout() { 208 if [ "$INSTALL_DIR_EXPLICIT" = true ]; then 209 log_info "Install directory: $INSTALL_DIR (explicit)" 210 return 0 211 fi 212 213 # Termux: package manager manages /data/data/..., keep code in HERMES_HOME. 214 if is_termux; then 215 INSTALL_DIR="$HERMES_HOME/hermes-agent" 216 return 0 217 fi 218 219 # Root on Linux: prefer FHS layout unless a legacy install already exists. 220 # macOS root installs keep the legacy layout because /usr/local/ on macOS 221 # is Homebrew territory and we don't want to fight that. 222 if [ "$OS" = "linux" ] && [ "$(id -u)" -eq 0 ]; then 223 if [ -d "$HERMES_HOME/hermes-agent/.git" ]; then 224 INSTALL_DIR="$HERMES_HOME/hermes-agent" 225 log_info "Existing install detected at $INSTALL_DIR — keeping legacy layout" 226 log_info " (new root installs use /usr/local/lib/hermes-agent)" 227 return 0 228 fi 229 INSTALL_DIR="/usr/local/lib/hermes-agent" 230 ROOT_FHS_LAYOUT=true 231 log_info "Root install on Linux — using FHS layout" 232 log_info " Code: $INSTALL_DIR" 233 log_info " Command: /usr/local/bin/hermes" 234 log_info " Data: $HERMES_HOME (unchanged)" 235 return 0 236 fi 237 238 # Default: non-root, non-Termux → legacy user-scoped layout. 239 INSTALL_DIR="$HERMES_HOME/hermes-agent" 240 } 241 242 get_command_link_dir() { 243 if is_termux && [ -n "${PREFIX:-}" ]; then 244 echo "$PREFIX/bin" 245 elif [ "$ROOT_FHS_LAYOUT" = true ]; then 246 echo "/usr/local/bin" 247 else 248 echo "$HOME/.local/bin" 249 fi 250 } 251 252 get_command_link_display_dir() { 253 if is_termux && [ -n "${PREFIX:-}" ]; then 254 echo '$PREFIX/bin' 255 elif [ "$ROOT_FHS_LAYOUT" = true ]; then 256 echo '/usr/local/bin' 257 else 258 echo '~/.local/bin' 259 fi 260 } 261 262 get_hermes_command_path() { 263 local link_dir 264 link_dir="$(get_command_link_dir)" 265 if [ -x "$link_dir/hermes" ]; then 266 echo "$link_dir/hermes" 267 else 268 echo "hermes" 269 fi 270 } 271 272 # ============================================================================ 273 # System detection 274 # ============================================================================ 275 276 detect_os() { 277 case "$(uname -s)" in 278 Linux*) 279 if is_termux; then 280 OS="android" 281 DISTRO="termux" 282 else 283 OS="linux" 284 if [ -f /etc/os-release ]; then 285 . /etc/os-release 286 DISTRO="$ID" 287 else 288 DISTRO="unknown" 289 fi 290 fi 291 ;; 292 Darwin*) 293 OS="macos" 294 DISTRO="macos" 295 ;; 296 CYGWIN*|MINGW*|MSYS*) 297 OS="windows" 298 DISTRO="windows" 299 log_error "Windows detected. Please use the PowerShell installer:" 300 log_info " irm https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1 | iex" 301 exit 1 302 ;; 303 *) 304 OS="unknown" 305 DISTRO="unknown" 306 log_warn "Unknown operating system" 307 ;; 308 esac 309 310 log_success "Detected: $OS ($DISTRO)" 311 } 312 313 # ============================================================================ 314 # Dependency checks 315 # ============================================================================ 316 317 install_uv() { 318 if [ "$DISTRO" = "termux" ]; then 319 log_info "Termux detected — using Python's stdlib venv + pip instead of uv" 320 UV_CMD="" 321 return 0 322 fi 323 324 log_info "Checking for uv package manager..." 325 326 # Check common locations for uv 327 if command -v uv &> /dev/null; then 328 UV_CMD="uv" 329 UV_VERSION=$($UV_CMD --version 2>/dev/null) 330 log_success "uv found ($UV_VERSION)" 331 return 0 332 fi 333 334 # Check ~/.local/bin (default uv install location) even if not on PATH yet 335 if [ -x "$HOME/.local/bin/uv" ]; then 336 UV_CMD="$HOME/.local/bin/uv" 337 UV_VERSION=$($UV_CMD --version 2>/dev/null) 338 log_success "uv found at ~/.local/bin ($UV_VERSION)" 339 return 0 340 fi 341 342 # Check ~/.cargo/bin (alternative uv install location) 343 if [ -x "$HOME/.cargo/bin/uv" ]; then 344 UV_CMD="$HOME/.cargo/bin/uv" 345 UV_VERSION=$($UV_CMD --version 2>/dev/null) 346 log_success "uv found at ~/.cargo/bin ($UV_VERSION)" 347 return 0 348 fi 349 350 # Install uv 351 log_info "Installing uv (fast Python package manager)..." 352 if curl -LsSf https://astral.sh/uv/install.sh | sh 2>/dev/null; then 353 # uv installs to ~/.local/bin by default 354 if [ -x "$HOME/.local/bin/uv" ]; then 355 UV_CMD="$HOME/.local/bin/uv" 356 elif [ -x "$HOME/.cargo/bin/uv" ]; then 357 UV_CMD="$HOME/.cargo/bin/uv" 358 elif command -v uv &> /dev/null; then 359 UV_CMD="uv" 360 else 361 log_error "uv installed but not found on PATH" 362 log_info "Try adding ~/.local/bin to your PATH and re-running" 363 exit 1 364 fi 365 UV_VERSION=$($UV_CMD --version 2>/dev/null) 366 log_success "uv installed ($UV_VERSION)" 367 else 368 log_error "Failed to install uv" 369 log_info "Install manually: https://docs.astral.sh/uv/getting-started/installation/" 370 exit 1 371 fi 372 } 373 374 check_python() { 375 if [ "$DISTRO" = "termux" ]; then 376 log_info "Checking Termux Python..." 377 if command -v python >/dev/null 2>&1; then 378 PYTHON_PATH="$(command -v python)" 379 if "$PYTHON_PATH" -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 11) else 1)' 2>/dev/null; then 380 PYTHON_FOUND_VERSION="$("$PYTHON_PATH" --version 2>/dev/null)" 381 log_success "Python found: $PYTHON_FOUND_VERSION" 382 return 0 383 fi 384 fi 385 386 log_info "Installing Python via pkg..." 387 pkg install -y python >/dev/null 388 PYTHON_PATH="$(command -v python)" 389 PYTHON_FOUND_VERSION="$("$PYTHON_PATH" --version 2>/dev/null)" 390 log_success "Python installed: $PYTHON_FOUND_VERSION" 391 return 0 392 fi 393 394 log_info "Checking Python $PYTHON_VERSION..." 395 396 # Let uv handle Python — it can download and manage Python versions 397 # First check if a suitable Python is already available 398 if PYTHON_PATH="$("$UV_CMD" python find "$PYTHON_VERSION" 2>/dev/null)"; then 399 PYTHON_FOUND_VERSION="$("$PYTHON_PATH" --version 2>/dev/null)" 400 log_success "Python found: $PYTHON_FOUND_VERSION" 401 return 0 402 fi 403 404 # Python not found — use uv to install it (no sudo needed!) 405 log_info "Python $PYTHON_VERSION not found, installing via uv..." 406 if "$UV_CMD" python install "$PYTHON_VERSION"; then 407 PYTHON_PATH="$("$UV_CMD" python find "$PYTHON_VERSION")" 408 PYTHON_FOUND_VERSION="$("$PYTHON_PATH" --version 2>/dev/null)" 409 log_success "Python installed: $PYTHON_FOUND_VERSION" 410 else 411 log_error "Failed to install Python $PYTHON_VERSION" 412 log_info "Install Python $PYTHON_VERSION manually, then re-run this script" 413 exit 1 414 fi 415 } 416 417 check_git() { 418 log_info "Checking Git..." 419 420 if command -v git &> /dev/null; then 421 GIT_VERSION=$(git --version | awk '{print $3}') 422 log_success "Git $GIT_VERSION found" 423 return 0 424 fi 425 426 log_error "Git not found" 427 428 if [ "$DISTRO" = "termux" ]; then 429 log_info "Installing Git via pkg..." 430 pkg install -y git >/dev/null 431 if command -v git >/dev/null 2>&1; then 432 GIT_VERSION=$(git --version | awk '{print $3}') 433 log_success "Git $GIT_VERSION installed" 434 return 0 435 fi 436 fi 437 438 log_info "Please install Git:" 439 440 case "$OS" in 441 linux) 442 case "$DISTRO" in 443 ubuntu|debian) 444 log_info " sudo apt update && sudo apt install git" 445 ;; 446 fedora) 447 log_info " sudo dnf install git" 448 ;; 449 arch) 450 log_info " sudo pacman -S git" 451 ;; 452 *) 453 log_info " Use your package manager to install git" 454 ;; 455 esac 456 ;; 457 android) 458 log_info " pkg install git" 459 ;; 460 macos) 461 log_info " xcode-select --install" 462 log_info " Or: brew install git" 463 ;; 464 esac 465 466 exit 1 467 } 468 469 check_node() { 470 log_info "Checking Node.js (for browser tools)..." 471 472 if command -v node &> /dev/null; then 473 local found_ver=$(node --version) 474 log_success "Node.js $found_ver found" 475 HAS_NODE=true 476 return 0 477 fi 478 479 # Check our own managed install from a previous run 480 if [ -x "$HERMES_HOME/node/bin/node" ]; then 481 export PATH="$HERMES_HOME/node/bin:$PATH" 482 local found_ver=$("$HERMES_HOME/node/bin/node" --version) 483 log_success "Node.js $found_ver found (Hermes-managed)" 484 HAS_NODE=true 485 return 0 486 fi 487 488 if [ "$DISTRO" = "termux" ]; then 489 log_info "Node.js not found — installing Node.js via pkg..." 490 else 491 log_info "Node.js not found — installing Node.js $NODE_VERSION LTS..." 492 fi 493 install_node 494 } 495 496 install_node() { 497 if [ "$DISTRO" = "termux" ]; then 498 log_info "Installing Node.js via pkg..." 499 if pkg install -y nodejs >/dev/null; then 500 local installed_ver 501 installed_ver=$(node --version 2>/dev/null) 502 log_success "Node.js $installed_ver installed via pkg" 503 HAS_NODE=true 504 else 505 log_warn "Failed to install Node.js via pkg" 506 HAS_NODE=false 507 fi 508 return 0 509 fi 510 511 local arch=$(uname -m) 512 local node_arch 513 case "$arch" in 514 x86_64) node_arch="x64" ;; 515 aarch64|arm64) node_arch="arm64" ;; 516 armv7l) node_arch="armv7l" ;; 517 *) 518 log_warn "Unsupported architecture ($arch) for Node.js auto-install" 519 log_info "Install manually: https://nodejs.org/en/download/" 520 HAS_NODE=false 521 return 0 522 ;; 523 esac 524 525 local node_os 526 case "$OS" in 527 linux) node_os="linux" ;; 528 macos) node_os="darwin" ;; 529 *) 530 log_warn "Unsupported OS for Node.js auto-install" 531 HAS_NODE=false 532 return 0 533 ;; 534 esac 535 536 # Resolve the latest v22.x.x tarball name from the index page 537 local index_url="https://nodejs.org/dist/latest-v${NODE_VERSION}.x/" 538 local tarball_name 539 tarball_name=$(curl -fsSL "$index_url" \ 540 | grep -oE "node-v${NODE_VERSION}\.[0-9]+\.[0-9]+-${node_os}-${node_arch}\.tar\.xz" \ 541 | head -1) 542 543 # Fallback to .tar.gz if .tar.xz not available 544 if [ -z "$tarball_name" ]; then 545 tarball_name=$(curl -fsSL "$index_url" \ 546 | grep -oE "node-v${NODE_VERSION}\.[0-9]+\.[0-9]+-${node_os}-${node_arch}\.tar\.gz" \ 547 | head -1) 548 fi 549 550 if [ -z "$tarball_name" ]; then 551 log_warn "Could not find Node.js $NODE_VERSION binary for $node_os-$node_arch" 552 log_info "Install manually: https://nodejs.org/en/download/" 553 HAS_NODE=false 554 return 0 555 fi 556 557 local download_url="${index_url}${tarball_name}" 558 local tmp_dir 559 tmp_dir=$(mktemp -d) 560 561 log_info "Downloading $tarball_name..." 562 if ! curl -fsSL "$download_url" -o "$tmp_dir/$tarball_name"; then 563 log_warn "Download failed" 564 rm -rf "$tmp_dir" 565 HAS_NODE=false 566 return 0 567 fi 568 569 log_info "Extracting to ~/.hermes/node/..." 570 if [[ "$tarball_name" == *.tar.xz ]]; then 571 tar xf "$tmp_dir/$tarball_name" -C "$tmp_dir" 572 else 573 tar xzf "$tmp_dir/$tarball_name" -C "$tmp_dir" 574 fi 575 576 local extracted_dir 577 extracted_dir=$(ls -d "$tmp_dir"/node-v* 2>/dev/null | head -1) 578 579 if [ ! -d "$extracted_dir" ]; then 580 log_warn "Extraction failed" 581 rm -rf "$tmp_dir" 582 HAS_NODE=false 583 return 0 584 fi 585 586 # Place into ~/.hermes/node/ and symlink binaries to ~/.local/bin/ 587 rm -rf "$HERMES_HOME/node" 588 mkdir -p "$HERMES_HOME" 589 mv "$extracted_dir" "$HERMES_HOME/node" 590 rm -rf "$tmp_dir" 591 592 mkdir -p "$HOME/.local/bin" 593 ln -sf "$HERMES_HOME/node/bin/node" "$HOME/.local/bin/node" 594 ln -sf "$HERMES_HOME/node/bin/npm" "$HOME/.local/bin/npm" 595 ln -sf "$HERMES_HOME/node/bin/npx" "$HOME/.local/bin/npx" 596 597 export PATH="$HERMES_HOME/node/bin:$PATH" 598 599 local installed_ver 600 installed_ver=$("$HERMES_HOME/node/bin/node" --version 2>/dev/null) 601 log_success "Node.js $installed_ver installed to ~/.hermes/node/" 602 HAS_NODE=true 603 } 604 605 install_system_packages() { 606 # Detect what's missing 607 HAS_RIPGREP=false 608 HAS_FFMPEG=false 609 local need_ripgrep=false 610 local need_ffmpeg=false 611 612 log_info "Checking ripgrep (fast file search)..." 613 if command -v rg &> /dev/null; then 614 log_success "$(rg --version | head -1) found" 615 HAS_RIPGREP=true 616 else 617 need_ripgrep=true 618 fi 619 620 log_info "Checking ffmpeg (TTS voice messages)..." 621 if command -v ffmpeg &> /dev/null; then 622 local ffmpeg_ver=$(ffmpeg -version 2>/dev/null | head -1 | awk '{print $3}') 623 log_success "ffmpeg $ffmpeg_ver found" 624 HAS_FFMPEG=true 625 else 626 need_ffmpeg=true 627 fi 628 629 # Termux always needs the Android build toolchain for the tested pip path, 630 # even when ripgrep/ffmpeg are already present. 631 if [ "$DISTRO" = "termux" ]; then 632 local termux_pkgs=(clang rust make pkg-config libffi openssl) 633 if [ "$need_ripgrep" = true ]; then 634 termux_pkgs+=("ripgrep") 635 fi 636 if [ "$need_ffmpeg" = true ]; then 637 termux_pkgs+=("ffmpeg") 638 fi 639 640 log_info "Installing Termux packages: ${termux_pkgs[*]}" 641 if pkg install -y "${termux_pkgs[@]}" >/dev/null; then 642 [ "$need_ripgrep" = true ] && HAS_RIPGREP=true && log_success "ripgrep installed" 643 [ "$need_ffmpeg" = true ] && HAS_FFMPEG=true && log_success "ffmpeg installed" 644 log_success "Termux build dependencies installed" 645 return 0 646 fi 647 648 log_warn "Could not auto-install all Termux packages" 649 log_info "Install manually: pkg install ${termux_pkgs[*]}" 650 return 0 651 fi 652 653 # Nothing to install — done 654 if [ "$need_ripgrep" = false ] && [ "$need_ffmpeg" = false ]; then 655 return 0 656 fi 657 658 # Build a human-readable description + package list 659 local desc_parts=() 660 local pkgs=() 661 if [ "$need_ripgrep" = true ]; then 662 desc_parts+=("ripgrep for faster file search") 663 pkgs+=("ripgrep") 664 fi 665 if [ "$need_ffmpeg" = true ]; then 666 desc_parts+=("ffmpeg for TTS voice messages") 667 pkgs+=("ffmpeg") 668 fi 669 local description 670 description=$(IFS=" and "; echo "${desc_parts[*]}") 671 672 # ── macOS: brew ── 673 if [ "$OS" = "macos" ]; then 674 if command -v brew &> /dev/null; then 675 log_info "Installing ${pkgs[*]} via Homebrew..." 676 if brew install "${pkgs[@]}"; then 677 [ "$need_ripgrep" = true ] && HAS_RIPGREP=true && log_success "ripgrep installed" 678 [ "$need_ffmpeg" = true ] && HAS_FFMPEG=true && log_success "ffmpeg installed" 679 return 0 680 fi 681 fi 682 log_warn "Could not auto-install (brew not found or install failed)" 683 log_info "Install manually: brew install ${pkgs[*]}" 684 return 0 685 fi 686 687 # ── Linux: resolve package manager command ── 688 local pkg_install="" 689 case "$DISTRO" in 690 ubuntu|debian) pkg_install="apt install -y" ;; 691 fedora) pkg_install="dnf install -y" ;; 692 arch) pkg_install="pacman -S --noconfirm" ;; 693 esac 694 695 if [ -n "$pkg_install" ]; then 696 local install_cmd="$pkg_install ${pkgs[*]}" 697 698 # Prevent needrestart/whiptail dialogs from blocking non-interactive installs 699 case "$DISTRO" in 700 ubuntu|debian) export DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a ;; 701 esac 702 703 # Already root — just install 704 if [ "$(id -u)" -eq 0 ]; then 705 log_info "Installing ${pkgs[*]}..." 706 if $install_cmd; then 707 [ "$need_ripgrep" = true ] && HAS_RIPGREP=true && log_success "ripgrep installed" 708 [ "$need_ffmpeg" = true ] && HAS_FFMPEG=true && log_success "ffmpeg installed" 709 return 0 710 fi 711 # Passwordless sudo — just install 712 elif command -v sudo &> /dev/null && sudo -n true 2>/dev/null; then 713 log_info "Installing ${pkgs[*]}..." 714 if sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a $install_cmd; then 715 [ "$need_ripgrep" = true ] && HAS_RIPGREP=true && log_success "ripgrep installed" 716 [ "$need_ffmpeg" = true ] && HAS_FFMPEG=true && log_success "ffmpeg installed" 717 return 0 718 fi 719 # sudo needs password — ask once for everything 720 elif command -v sudo &> /dev/null; then 721 if [ "$IS_INTERACTIVE" = true ]; then 722 echo "" 723 log_info "sudo is needed ONLY to install optional system packages (${pkgs[*]}) via your package manager." 724 log_info "Hermes Agent itself does not require or retain root access." 725 if prompt_yes_no "Install ${description}? (requires sudo)" "no"; then 726 if sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a $install_cmd; then 727 [ "$need_ripgrep" = true ] && HAS_RIPGREP=true && log_success "ripgrep installed" 728 [ "$need_ffmpeg" = true ] && HAS_FFMPEG=true && log_success "ffmpeg installed" 729 return 0 730 fi 731 fi 732 elif (: </dev/tty) 2>/dev/null; then 733 # Non-interactive (e.g. curl | bash) but a terminal is available. 734 # Read the prompt from /dev/tty (same approach the setup wizard uses). 735 # Probe by actually opening /dev/tty: a bare existence test passes 736 # in Docker builds where the device node is in the mount namespace 737 # but opening fails with ENXIO. See #16746. 738 echo "" 739 log_info "sudo is needed ONLY to install optional system packages (${pkgs[*]}) via your package manager." 740 log_info "Hermes Agent itself does not require or retain root access." 741 if prompt_yes_no "Install ${description}?" "yes"; then 742 if sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a $install_cmd < /dev/tty; then 743 [ "$need_ripgrep" = true ] && HAS_RIPGREP=true && log_success "ripgrep installed" 744 [ "$need_ffmpeg" = true ] && HAS_FFMPEG=true && log_success "ffmpeg installed" 745 return 0 746 fi 747 fi 748 else 749 log_warn "Non-interactive mode and no terminal available — cannot install system packages" 750 log_info "Install manually after setup completes: sudo $install_cmd" 751 fi 752 fi 753 fi 754 755 # ── Fallback for ripgrep: cargo ── 756 if [ "$need_ripgrep" = true ] && [ "$HAS_RIPGREP" = false ]; then 757 if command -v cargo &> /dev/null; then 758 log_info "Trying cargo install ripgrep (no sudo needed)..." 759 if cargo install ripgrep; then 760 log_success "ripgrep installed via cargo" 761 HAS_RIPGREP=true 762 fi 763 fi 764 fi 765 766 # ── Show manual instructions for anything still missing ── 767 if [ "$HAS_RIPGREP" = false ] && [ "$need_ripgrep" = true ]; then 768 log_warn "ripgrep not installed (file search will use grep fallback)" 769 show_manual_install_hint "ripgrep" 770 fi 771 if [ "$HAS_FFMPEG" = false ] && [ "$need_ffmpeg" = true ]; then 772 log_warn "ffmpeg not installed (TTS voice messages will be limited)" 773 show_manual_install_hint "ffmpeg" 774 fi 775 } 776 777 show_manual_install_hint() { 778 local pkg="$1" 779 log_info "To install $pkg manually:" 780 case "$OS" in 781 linux) 782 case "$DISTRO" in 783 ubuntu|debian) log_info " sudo apt install $pkg" ;; 784 fedora) log_info " sudo dnf install $pkg" ;; 785 arch) log_info " sudo pacman -S $pkg" ;; 786 *) log_info " Use your package manager or visit the project homepage" ;; 787 esac 788 ;; 789 android) 790 log_info " pkg install $pkg" 791 ;; 792 macos) log_info " brew install $pkg" ;; 793 esac 794 } 795 796 # ============================================================================ 797 # Installation 798 # ============================================================================ 799 800 clone_repo() { 801 log_info "Installing to $INSTALL_DIR..." 802 803 if [ -d "$INSTALL_DIR" ]; then 804 if [ -d "$INSTALL_DIR/.git" ]; then 805 log_info "Existing installation found, updating..." 806 cd "$INSTALL_DIR" 807 808 local autostash_ref="" 809 if [ -n "$(git status --porcelain)" ]; then 810 local stash_name 811 stash_name="hermes-install-autostash-$(date -u +%Y%m%d-%H%M%S)" 812 log_info "Local changes detected, stashing before update..." 813 git stash push --include-untracked -m "$stash_name" 814 autostash_ref="$(git rev-parse --verify refs/stash)" 815 fi 816 817 git fetch origin 818 git checkout "$BRANCH" 819 git pull --ff-only origin "$BRANCH" 820 821 if [ -n "$autostash_ref" ]; then 822 local restore_now="yes" 823 if [ -t 0 ] && [ -t 1 ]; then 824 echo 825 log_warn "Local changes were stashed before updating." 826 log_warn "Restoring them may reapply local customizations onto the updated codebase." 827 printf "Restore local changes now? [Y/n] " 828 read -r restore_answer 829 case "$restore_answer" in 830 ""|y|Y|yes|YES|Yes) restore_now="yes" ;; 831 *) restore_now="no" ;; 832 esac 833 fi 834 835 if [ "$restore_now" = "yes" ]; then 836 log_info "Restoring local changes..." 837 if git stash apply "$autostash_ref"; then 838 git stash drop "$autostash_ref" >/dev/null 839 log_warn "Local changes were restored on top of the updated codebase." 840 log_warn "Review git diff / git status if Hermes behaves unexpectedly." 841 else 842 log_error "Update succeeded, but restoring local changes failed. Your changes are still preserved in git stash." 843 log_info "Resolve manually with: git stash apply $autostash_ref" 844 exit 1 845 fi 846 else 847 log_info "Skipped restoring local changes." 848 log_info "Your changes are still preserved in git stash." 849 log_info "Restore manually with: git stash apply $autostash_ref" 850 fi 851 fi 852 else 853 log_error "Directory exists but is not a git repository: $INSTALL_DIR" 854 log_info "Remove it or choose a different directory with --dir" 855 exit 1 856 fi 857 else 858 # Try SSH first (for private repo access), fall back to HTTPS 859 # GIT_SSH_COMMAND disables interactive prompts and sets a short timeout 860 # so SSH fails fast instead of hanging when no key is configured. 861 log_info "Trying SSH clone..." 862 if GIT_SSH_COMMAND="ssh -o BatchMode=yes -o ConnectTimeout=5" \ 863 git clone --branch "$BRANCH" "$REPO_URL_SSH" "$INSTALL_DIR" 2>/dev/null; then 864 log_success "Cloned via SSH" 865 else 866 rm -rf "$INSTALL_DIR" 2>/dev/null # Clean up partial SSH clone 867 log_info "SSH failed, trying HTTPS..." 868 if git clone --branch "$BRANCH" "$REPO_URL_HTTPS" "$INSTALL_DIR"; then 869 log_success "Cloned via HTTPS" 870 else 871 log_error "Failed to clone repository" 872 exit 1 873 fi 874 fi 875 fi 876 877 cd "$INSTALL_DIR" 878 879 log_success "Repository ready" 880 } 881 882 setup_venv() { 883 if [ "$USE_VENV" = false ]; then 884 log_info "Skipping virtual environment (--no-venv)" 885 return 0 886 fi 887 888 if [ "$DISTRO" = "termux" ]; then 889 log_info "Creating virtual environment with Termux Python..." 890 891 if [ -d "venv" ]; then 892 log_info "Virtual environment already exists, recreating..." 893 rm -rf venv 894 fi 895 896 "$PYTHON_PATH" -m venv venv 897 log_success "Virtual environment ready ($(./venv/bin/python --version 2>/dev/null))" 898 return 0 899 fi 900 901 log_info "Creating virtual environment with Python $PYTHON_VERSION..." 902 903 if [ -d "venv" ]; then 904 log_info "Virtual environment already exists, recreating..." 905 rm -rf venv 906 fi 907 908 # uv creates the venv and pins the Python version in one step 909 $UV_CMD venv venv --python "$PYTHON_VERSION" 910 911 log_success "Virtual environment ready (Python $PYTHON_VERSION)" 912 } 913 914 install_deps() { 915 log_info "Installing dependencies..." 916 917 if [ "$DISTRO" = "termux" ]; then 918 if [ "$USE_VENV" = true ]; then 919 export VIRTUAL_ENV="$INSTALL_DIR/venv" 920 PIP_PYTHON="$INSTALL_DIR/venv/bin/python" 921 else 922 PIP_PYTHON="$PYTHON_PATH" 923 fi 924 925 if [ -z "${ANDROID_API_LEVEL:-}" ]; then 926 ANDROID_API_LEVEL="$(getprop ro.build.version.sdk 2>/dev/null || true)" 927 if [ -z "$ANDROID_API_LEVEL" ]; then 928 ANDROID_API_LEVEL=24 929 fi 930 export ANDROID_API_LEVEL 931 log_info "Using ANDROID_API_LEVEL=$ANDROID_API_LEVEL for Android wheel builds" 932 fi 933 934 "$PIP_PYTHON" -m pip install --upgrade pip setuptools wheel >/dev/null 935 if ! "$PIP_PYTHON" -m pip install -e '.[termux]' -c constraints-termux.txt; then 936 log_warn "Termux feature install (.[termux]) failed, trying base install..." 937 if ! "$PIP_PYTHON" -m pip install -e '.' -c constraints-termux.txt; then 938 log_error "Package installation failed on Termux." 939 log_info "Ensure these packages are installed: pkg install clang rust make pkg-config libffi openssl" 940 log_info "Then re-run: cd $INSTALL_DIR && python -m pip install -e '.[termux]' -c constraints-termux.txt" 941 exit 1 942 fi 943 fi 944 945 log_success "Main package installed" 946 log_info "Termux note: browser/WhatsApp tooling is not installed by default; see the Termux guide for optional follow-up steps." 947 948 if [ -d "tinker-atropos" ] && [ -f "tinker-atropos/pyproject.toml" ]; then 949 log_info "tinker-atropos submodule found — skipping install (optional, for RL training)" 950 log_info " To install later: $PIP_PYTHON -m pip install -e \"./tinker-atropos\"" 951 fi 952 953 log_success "All dependencies installed" 954 return 0 955 fi 956 957 if [ "$USE_VENV" = true ]; then 958 # Tell uv to install into our venv (no need to activate) 959 export VIRTUAL_ENV="$INSTALL_DIR/venv" 960 fi 961 962 # On Debian/Ubuntu (including WSL), some Python packages need build tools. 963 # Check and offer to install them if missing. 964 if [ "$DISTRO" = "ubuntu" ] || [ "$DISTRO" = "debian" ]; then 965 local need_build_tools=false 966 for pkg in gcc python3-dev libffi-dev; do 967 if ! dpkg -s "$pkg" &>/dev/null; then 968 need_build_tools=true 969 break 970 fi 971 done 972 if [ "$need_build_tools" = true ]; then 973 log_info "Some build tools may be needed for Python packages..." 974 if command -v sudo &> /dev/null; then 975 if sudo -n true 2>/dev/null; then 976 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 977 log_success "Build tools installed" 978 else 979 log_info "sudo is needed ONLY to install build tools (build-essential, python3-dev, libffi-dev) via apt." 980 log_info "Hermes Agent itself does not require or retain root access." 981 if prompt_yes_no "Install build tools?" "yes"; then 982 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 983 log_success "Build tools installed" 984 fi 985 fi 986 fi 987 fi 988 fi 989 990 # Install the main package in editable mode with all extras. 991 # Try [all] first, fall back to base install if extras have issues. 992 ALL_INSTALL_LOG=$(mktemp) 993 if ! $UV_CMD pip install -e ".[all]" 2>"$ALL_INSTALL_LOG"; then 994 log_warn "Full install (.[all]) failed, trying base install..." 995 log_info "Reason: $(tail -5 "$ALL_INSTALL_LOG" | head -3)" 996 rm -f "$ALL_INSTALL_LOG" 997 if ! $UV_CMD pip install -e "."; then 998 log_error "Package installation failed." 999 log_info "Check that build tools are installed: sudo apt install build-essential python3-dev" 1000 log_info "Then re-run: cd $INSTALL_DIR && uv pip install -e '.[all]'" 1001 exit 1 1002 fi 1003 else 1004 rm -f "$ALL_INSTALL_LOG" 1005 fi 1006 1007 log_success "Main package installed" 1008 1009 # tinker-atropos (RL training) is optional — skip by default. 1010 # To enable RL tools: git submodule update --init tinker-atropos && uv pip install -e "./tinker-atropos" 1011 if [ -d "tinker-atropos" ] && [ -f "tinker-atropos/pyproject.toml" ]; then 1012 log_info "tinker-atropos submodule found — skipping install (optional, for RL training)" 1013 log_info " To install: $UV_CMD pip install -e \"./tinker-atropos\"" 1014 fi 1015 1016 log_success "All dependencies installed" 1017 } 1018 1019 setup_path() { 1020 log_info "Setting up hermes command..." 1021 1022 if [ "$USE_VENV" = true ]; then 1023 HERMES_BIN="$INSTALL_DIR/venv/bin/hermes" 1024 else 1025 HERMES_BIN="$(which hermes 2>/dev/null || echo "")" 1026 if [ -z "$HERMES_BIN" ]; then 1027 log_warn "hermes not found on PATH after install" 1028 return 0 1029 fi 1030 fi 1031 1032 # Verify the entry point script was actually generated 1033 if [ ! -x "$HERMES_BIN" ]; then 1034 log_warn "hermes entry point not found at $HERMES_BIN" 1035 log_info "This usually means the pip install didn't complete successfully." 1036 if [ "$DISTRO" = "termux" ]; then 1037 log_info "Try: cd $INSTALL_DIR && python -m pip install -e '.[termux]' -c constraints-termux.txt" 1038 else 1039 log_info "Try: cd $INSTALL_DIR && uv pip install -e '.[all]'" 1040 fi 1041 return 0 1042 fi 1043 1044 local command_link_dir 1045 local command_link_display_dir 1046 command_link_dir="$(get_command_link_dir)" 1047 command_link_display_dir="$(get_command_link_display_dir)" 1048 1049 # Create a user-facing shim for the hermes command. 1050 mkdir -p "$command_link_dir" 1051 ln -sf "$HERMES_BIN" "$command_link_dir/hermes" 1052 log_success "Symlinked hermes → $command_link_display_dir/hermes" 1053 1054 if [ "$DISTRO" = "termux" ]; then 1055 export PATH="$command_link_dir:$PATH" 1056 log_info "$command_link_display_dir is the native Termux command path" 1057 log_success "hermes command ready" 1058 return 0 1059 fi 1060 1061 # FHS layout: /usr/local/bin is normally on PATH for login shells (via 1062 # /etc/profile pathmunge), but on RHEL/CentOS/Rocky/Alma 8+ non-login 1063 # interactive root shells (su, sudo -s, tmux panes, some web terminals) 1064 # only source /etc/bashrc, which does NOT add /usr/local/bin — and 1065 # /root/.bash_profile doesn't either. So verify with `command -v` and 1066 # fall back to writing a PATH guard into /root/.bashrc when needed. 1067 if [ "$ROOT_FHS_LAYOUT" = true ]; then 1068 export PATH="$command_link_dir:$PATH" 1069 # Probe a fresh non-login interactive bash the way the user will use it. 1070 # `bash -i -c` sources ~/.bashrc but NOT ~/.bash_profile or /etc/profile, 1071 # which is the exact scenario where RHEL root loses /usr/local/bin. 1072 if env -i HOME="$HOME" TERM="${TERM:-dumb}" bash -i -c 'command -v hermes' \ 1073 >/dev/null 2>&1; then 1074 log_info "/usr/local/bin is already on PATH for all shells" 1075 log_success "hermes command ready" 1076 return 0 1077 fi 1078 1079 log_info "hermes not on PATH in non-login shells (common on RHEL-family)" 1080 PATH_LINE='export PATH="/usr/local/bin:$PATH"' 1081 PATH_COMMENT='# Hermes Agent — ensure /usr/local/bin is on PATH (RHEL non-login shells)' 1082 for SHELL_CONFIG in "$HOME/.bashrc" "$HOME/.bash_profile"; do 1083 [ -f "$SHELL_CONFIG" ] || continue 1084 if ! grep -v '^[[:space:]]*#' "$SHELL_CONFIG" 2>/dev/null \ 1085 | grep -qE 'PATH=.*(/usr/local/bin|\$command_link_dir)'; then 1086 echo "" >> "$SHELL_CONFIG" 1087 echo "$PATH_COMMENT" >> "$SHELL_CONFIG" 1088 echo "$PATH_LINE" >> "$SHELL_CONFIG" 1089 log_success "Added /usr/local/bin to PATH in $SHELL_CONFIG" 1090 fi 1091 done 1092 log_success "hermes command ready" 1093 return 0 1094 fi 1095 1096 # Check if ~/.local/bin is on PATH; if not, add it to shell config. 1097 # Detect the user's actual login shell (not the shell running this script, 1098 # which is always bash when piped from curl). 1099 if ! echo "$PATH" | tr ':' '\n' | grep -q "^$command_link_dir$"; then 1100 SHELL_CONFIGS=() 1101 IS_FISH=false 1102 LOGIN_SHELL="$(basename "${SHELL:-/bin/bash}")" 1103 case "$LOGIN_SHELL" in 1104 zsh) 1105 [ -f "$HOME/.zshrc" ] && SHELL_CONFIGS+=("$HOME/.zshrc") 1106 [ -f "$HOME/.zprofile" ] && SHELL_CONFIGS+=("$HOME/.zprofile") 1107 # If neither exists, create ~/.zshrc (common on fresh macOS installs) 1108 if [ ${#SHELL_CONFIGS[@]} -eq 0 ]; then 1109 touch "$HOME/.zshrc" 1110 SHELL_CONFIGS+=("$HOME/.zshrc") 1111 fi 1112 ;; 1113 bash) 1114 [ -f "$HOME/.bashrc" ] && SHELL_CONFIGS+=("$HOME/.bashrc") 1115 [ -f "$HOME/.bash_profile" ] && SHELL_CONFIGS+=("$HOME/.bash_profile") 1116 ;; 1117 fish) 1118 # fish uses ~/.config/fish/config.fish and fish_add_path — not export PATH= 1119 IS_FISH=true 1120 FISH_CONFIG="$HOME/.config/fish/config.fish" 1121 mkdir -p "$(dirname "$FISH_CONFIG")" 1122 touch "$FISH_CONFIG" 1123 ;; 1124 *) 1125 [ -f "$HOME/.bashrc" ] && SHELL_CONFIGS+=("$HOME/.bashrc") 1126 [ -f "$HOME/.zshrc" ] && SHELL_CONFIGS+=("$HOME/.zshrc") 1127 ;; 1128 esac 1129 # Also ensure ~/.profile has it (sourced by login shells on 1130 # Ubuntu/Debian/WSL even when ~/.bashrc is skipped) 1131 [ "$IS_FISH" = "false" ] && [ -f "$HOME/.profile" ] && SHELL_CONFIGS+=("$HOME/.profile") 1132 1133 PATH_LINE='export PATH="$HOME/.local/bin:$PATH"' 1134 1135 for SHELL_CONFIG in "${SHELL_CONFIGS[@]}"; do 1136 if ! grep -v '^[[:space:]]*#' "$SHELL_CONFIG" 2>/dev/null | grep -qE 'PATH=.*\.local/bin'; then 1137 echo "" >> "$SHELL_CONFIG" 1138 echo "# Hermes Agent — ensure ~/.local/bin is on PATH" >> "$SHELL_CONFIG" 1139 echo "$PATH_LINE" >> "$SHELL_CONFIG" 1140 log_success "Added ~/.local/bin to PATH in $SHELL_CONFIG" 1141 fi 1142 done 1143 1144 # fish uses fish_add_path instead of export PATH=... 1145 if [ "$IS_FISH" = "true" ]; then 1146 if ! grep -q 'fish_add_path.*\.local/bin' "$FISH_CONFIG" 2>/dev/null; then 1147 echo "" >> "$FISH_CONFIG" 1148 echo "# Hermes Agent — ensure ~/.local/bin is on PATH" >> "$FISH_CONFIG" 1149 echo 'fish_add_path "$HOME/.local/bin"' >> "$FISH_CONFIG" 1150 log_success "Added ~/.local/bin to PATH in $FISH_CONFIG" 1151 fi 1152 fi 1153 1154 if [ "$IS_FISH" = "false" ] && [ ${#SHELL_CONFIGS[@]} -eq 0 ]; then 1155 log_warn "Could not detect shell config file to add ~/.local/bin to PATH" 1156 log_info "Add manually: $PATH_LINE" 1157 fi 1158 else 1159 log_info "~/.local/bin already on PATH" 1160 fi 1161 1162 # Export for current session so hermes works immediately 1163 export PATH="$command_link_dir:$PATH" 1164 1165 log_success "hermes command ready" 1166 } 1167 1168 copy_config_templates() { 1169 log_info "Setting up configuration files..." 1170 1171 # Create ~/.hermes directory structure (config at top level, code in subdir) 1172 mkdir -p "$HERMES_HOME"/{cron,sessions,logs,pairing,hooks,image_cache,audio_cache,memories,skills} 1173 1174 # Create .env at ~/.hermes/.env (top level, easy to find) 1175 if [ ! -f "$HERMES_HOME/.env" ]; then 1176 if [ -f "$INSTALL_DIR/.env.example" ]; then 1177 cp "$INSTALL_DIR/.env.example" "$HERMES_HOME/.env" 1178 log_success "Created ~/.hermes/.env from template" 1179 else 1180 touch "$HERMES_HOME/.env" 1181 log_success "Created ~/.hermes/.env" 1182 fi 1183 else 1184 log_info "~/.hermes/.env already exists, keeping it" 1185 fi 1186 1187 # Create config.yaml at ~/.hermes/config.yaml (top level, easy to find) 1188 if [ ! -f "$HERMES_HOME/config.yaml" ]; then 1189 if [ -f "$INSTALL_DIR/cli-config.yaml.example" ]; then 1190 cp "$INSTALL_DIR/cli-config.yaml.example" "$HERMES_HOME/config.yaml" 1191 log_success "Created ~/.hermes/config.yaml from template" 1192 fi 1193 else 1194 log_info "~/.hermes/config.yaml already exists, keeping it" 1195 fi 1196 1197 # Create SOUL.md if it doesn't exist (global persona file) 1198 if [ ! -f "$HERMES_HOME/SOUL.md" ]; then 1199 cat > "$HERMES_HOME/SOUL.md" << 'SOUL_EOF' 1200 # Hermes Agent Persona 1201 1202 <!-- 1203 This file defines the agent's personality and tone. 1204 The agent will embody whatever you write here. 1205 Edit this to customize how Hermes communicates with you. 1206 1207 Examples: 1208 - "You are a warm, playful assistant who uses kaomoji occasionally." 1209 - "You are a concise technical expert. No fluff, just facts." 1210 - "You speak like a friendly coworker who happens to know everything." 1211 1212 This file is loaded fresh each message -- no restart needed. 1213 Delete the contents (or this file) to use the default personality. 1214 --> 1215 SOUL_EOF 1216 log_success "Created ~/.hermes/SOUL.md (edit to customize personality)" 1217 fi 1218 1219 log_success "Configuration directory ready: ~/.hermes/" 1220 1221 # Seed bundled skills into ~/.hermes/skills/ (manifest-based, one-time per skill) 1222 log_info "Syncing bundled skills to ~/.hermes/skills/ ..." 1223 if "$INSTALL_DIR/venv/bin/python" "$INSTALL_DIR/tools/skills_sync.py" 2>/dev/null; then 1224 log_success "Skills synced to ~/.hermes/skills/" 1225 else 1226 # Fallback: simple directory copy if Python sync fails 1227 if [ -d "$INSTALL_DIR/skills" ] && [ ! "$(ls -A "$HERMES_HOME/skills/" 2>/dev/null | grep -v '.bundled_manifest')" ]; then 1228 cp -r "$INSTALL_DIR/skills/"* "$HERMES_HOME/skills/" 2>/dev/null || true 1229 log_success "Skills copied to ~/.hermes/skills/" 1230 fi 1231 fi 1232 } 1233 1234 install_node_deps() { 1235 if [ "$HAS_NODE" = false ]; then 1236 log_info "Skipping Node.js dependencies (Node not installed)" 1237 return 0 1238 fi 1239 1240 if [ "$DISTRO" = "termux" ]; then 1241 log_info "Skipping automatic Node/browser dependency setup on Termux" 1242 log_info "Browser automation is not part of the tested Termux install path yet." 1243 log_info "If you want to experiment manually later, run: cd $INSTALL_DIR && npm install" 1244 return 0 1245 fi 1246 1247 if [ -f "$INSTALL_DIR/package.json" ]; then 1248 log_info "Installing Node.js dependencies (browser tools)..." 1249 cd "$INSTALL_DIR" 1250 npm install --silent 2>/dev/null || { 1251 log_warn "npm install failed (browser tools may not work)" 1252 } 1253 log_success "Node.js dependencies installed" 1254 1255 # Install Playwright browser + system dependencies. 1256 # Playwright's --with-deps only supports apt-based systems natively. 1257 # For Arch/Manjaro we install the system libs via pacman first. 1258 # Other systems must install Chromium dependencies manually. 1259 log_info "Installing browser engine (Playwright Chromium)..." 1260 case "$DISTRO" in 1261 ubuntu|debian|raspbian|pop|linuxmint|elementary|zorin|kali|parrot) 1262 log_info "Playwright may request sudo to install browser system dependencies (shared libraries)." 1263 log_info "This is standard Playwright setup — Hermes itself does not require root access." 1264 cd "$INSTALL_DIR" && npx playwright install --with-deps chromium 2>/dev/null || { 1265 log_warn "Playwright browser installation failed — browser tools will not work." 1266 log_warn "Try running manually: cd $INSTALL_DIR && npx playwright install --with-deps chromium" 1267 } 1268 ;; 1269 arch|manjaro) 1270 if command -v pacman &> /dev/null; then 1271 log_info "Arch/Manjaro detected — installing Chromium system dependencies via pacman..." 1272 if command -v sudo &> /dev/null && sudo -n true 2>/dev/null; then 1273 sudo NEEDRESTART_MODE=a pacman -S --noconfirm --needed \ 1274 nss atk at-spi2-core cups libdrm libxkbcommon mesa pango cairo alsa-lib >/dev/null 2>&1 || true 1275 elif [ "$(id -u)" -eq 0 ]; then 1276 pacman -S --noconfirm --needed \ 1277 nss atk at-spi2-core cups libdrm libxkbcommon mesa pango cairo alsa-lib >/dev/null 2>&1 || true 1278 else 1279 log_warn "Cannot install browser deps without sudo. Run manually:" 1280 log_warn " sudo pacman -S nss atk at-spi2-core cups libdrm libxkbcommon mesa pango cairo alsa-lib" 1281 fi 1282 fi 1283 cd "$INSTALL_DIR" && npx playwright install chromium 2>/dev/null || { 1284 log_warn "Playwright browser installation failed — browser tools will not work." 1285 } 1286 ;; 1287 fedora|rhel|centos|rocky|alma) 1288 log_warn "Playwright does not support automatic dependency installation on RPM-based systems." 1289 log_info "Install Chromium system dependencies manually before using browser tools:" 1290 log_info " sudo dnf install nss atk at-spi2-core cups-libs libdrm libxkbcommon mesa-libgbm pango cairo alsa-lib" 1291 cd "$INSTALL_DIR" && npx playwright install chromium 2>/dev/null || { 1292 log_warn "Playwright browser installation failed — install dependencies above and retry." 1293 } 1294 ;; 1295 opensuse*|sles) 1296 log_warn "Playwright does not support automatic dependency installation on zypper-based systems." 1297 log_info "Install Chromium system dependencies manually before using browser tools:" 1298 log_info " sudo zypper install mozilla-nss libatk-1_0-0 at-spi2-core cups-libs libdrm2 libxkbcommon0 Mesa-libgbm1 pango cairo libasound2" 1299 cd "$INSTALL_DIR" && npx playwright install chromium 2>/dev/null || { 1300 log_warn "Playwright browser installation failed — install dependencies above and retry." 1301 } 1302 ;; 1303 *) 1304 log_warn "Playwright does not support automatic dependency installation on $DISTRO." 1305 log_info "Install Chromium/browser system dependencies for your distribution, then run:" 1306 log_info " cd $INSTALL_DIR && npx playwright install chromium" 1307 log_info "Browser tools will not work until dependencies are installed." 1308 cd "$INSTALL_DIR" && npx playwright install chromium 2>/dev/null || true 1309 ;; 1310 esac 1311 log_success "Browser engine setup complete" 1312 fi 1313 1314 # Install TUI dependencies 1315 if [ -f "$INSTALL_DIR/ui-tui/package.json" ]; then 1316 log_info "Installing TUI dependencies..." 1317 cd "$INSTALL_DIR/ui-tui" 1318 npm install --silent 2>/dev/null || { 1319 log_warn "TUI npm install failed (hermes --tui may not work)" 1320 } 1321 log_success "TUI dependencies installed" 1322 fi 1323 1324 1325 } 1326 1327 run_setup_wizard() { 1328 if [ "$RUN_SETUP" = false ]; then 1329 log_info "Skipping setup wizard (--skip-setup)" 1330 return 0 1331 fi 1332 1333 # The setup wizard reads from /dev/tty, so it works even when the 1334 # install script itself is piped (curl | bash). Only skip if no 1335 # terminal is available at all (e.g. Docker build, CI). 1336 # 1337 # Probe by actually opening /dev/tty: a bare existence test passes 1338 # in Docker builds where the device node is in the mount namespace 1339 # but opening fails with ENXIO, so the wizard would proceed and 1340 # then crash on `< /dev/tty` below. 1341 if ! (: </dev/tty) 2>/dev/null; then 1342 log_info "Setup wizard skipped (no terminal available). Run 'hermes setup' after install." 1343 return 0 1344 fi 1345 1346 echo "" 1347 log_info "Starting setup wizard..." 1348 echo "" 1349 1350 cd "$INSTALL_DIR" 1351 1352 # Run hermes setup using the venv Python directly (no activation needed). 1353 # Redirect stdin from /dev/tty so interactive prompts work when piped from curl. 1354 if [ "$USE_VENV" = true ]; then 1355 "$INSTALL_DIR/venv/bin/python" -m hermes_cli.main setup < /dev/tty 1356 else 1357 python -m hermes_cli.main setup < /dev/tty 1358 fi 1359 } 1360 1361 maybe_start_gateway() { 1362 # Check if any messaging platform tokens were configured 1363 ENV_FILE="$HERMES_HOME/.env" 1364 if [ ! -f "$ENV_FILE" ]; then 1365 return 0 1366 fi 1367 1368 HAS_MESSAGING=false 1369 for VAR in TELEGRAM_BOT_TOKEN DISCORD_BOT_TOKEN SLACK_BOT_TOKEN SLACK_APP_TOKEN WHATSAPP_ENABLED; do 1370 VAL=$(grep "^${VAR}=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2-) 1371 if [ -n "$VAL" ] && [ "$VAL" != "your-token-here" ]; then 1372 HAS_MESSAGING=true 1373 break 1374 fi 1375 done 1376 1377 if [ "$HAS_MESSAGING" = false ]; then 1378 return 0 1379 fi 1380 1381 echo "" 1382 log_info "Messaging platform token detected!" 1383 log_info "The gateway needs to be running for Hermes to send/receive messages." 1384 1385 # If WhatsApp is enabled and no session exists yet, run foreground first for QR scan 1386 WHATSAPP_VAL=$(grep "^WHATSAPP_ENABLED=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2-) 1387 WHATSAPP_SESSION="$HERMES_HOME/whatsapp/session/creds.json" 1388 if [ "$WHATSAPP_VAL" = "true" ] && [ ! -f "$WHATSAPP_SESSION" ]; then 1389 if [ "$IS_INTERACTIVE" = true ]; then 1390 echo "" 1391 log_info "WhatsApp is enabled but not yet paired." 1392 log_info "Running 'hermes whatsapp' to pair via QR code..." 1393 echo "" 1394 if prompt_yes_no "Pair WhatsApp now?" "yes"; then 1395 HERMES_CMD="$(get_hermes_command_path)" 1396 $HERMES_CMD whatsapp || true 1397 fi 1398 else 1399 log_info "WhatsApp pairing skipped (non-interactive). Run 'hermes whatsapp' to pair." 1400 fi 1401 fi 1402 1403 # Probe by actually opening /dev/tty: a bare existence test passes 1404 # in Docker builds where the device node is in the mount namespace 1405 # but opening fails with ENXIO. See #16746. 1406 if ! (: </dev/tty) 2>/dev/null; then 1407 log_info "Gateway setup skipped (no terminal available). Run 'hermes gateway install' later." 1408 return 0 1409 fi 1410 1411 echo "" 1412 local should_install_gateway=false 1413 if [ "$DISTRO" = "termux" ]; then 1414 if prompt_yes_no "Would you like to start the gateway in the background?" "yes"; then 1415 should_install_gateway=true 1416 fi 1417 else 1418 if prompt_yes_no "Would you like to install the gateway as a background service?" "yes"; then 1419 should_install_gateway=true 1420 fi 1421 fi 1422 1423 if [ "$should_install_gateway" = true ]; then 1424 HERMES_CMD="$(get_hermes_command_path)" 1425 1426 if [ "$DISTRO" != "termux" ] && command -v systemctl &> /dev/null; then 1427 log_info "Installing systemd service..." 1428 if $HERMES_CMD gateway install 2>/dev/null; then 1429 log_success "Gateway service installed" 1430 if $HERMES_CMD gateway start 2>/dev/null; then 1431 log_success "Gateway started! Your bot is now online." 1432 else 1433 log_warn "Service installed but failed to start. Try: hermes gateway start" 1434 fi 1435 else 1436 log_warn "Systemd install failed. You can start manually: hermes gateway" 1437 fi 1438 else 1439 if [ "$DISTRO" = "termux" ]; then 1440 log_info "Termux detected — starting gateway in best-effort background mode..." 1441 else 1442 log_info "systemd not available — starting gateway in background..." 1443 fi 1444 nohup $HERMES_CMD gateway > "$HERMES_HOME/logs/gateway.log" 2>&1 & 1445 GATEWAY_PID=$! 1446 log_success "Gateway started (PID $GATEWAY_PID). Logs: ~/.hermes/logs/gateway.log" 1447 log_info "To stop: kill $GATEWAY_PID" 1448 log_info "To restart later: hermes gateway" 1449 if [ "$DISTRO" = "termux" ]; then 1450 log_warn "Android may stop background processes when Termux is suspended or the system reclaims resources." 1451 fi 1452 fi 1453 else 1454 log_info "Skipped. Start the gateway later with: hermes gateway" 1455 fi 1456 } 1457 1458 print_success() { 1459 echo "" 1460 echo -e "${GREEN}${BOLD}" 1461 echo "┌─────────────────────────────────────────────────────────┐" 1462 echo "│ ✓ Installation Complete! │" 1463 echo "└─────────────────────────────────────────────────────────┘" 1464 echo -e "${NC}" 1465 echo "" 1466 1467 # Show file locations 1468 echo -e "${CYAN}${BOLD}📁 Your files:${NC}" 1469 echo "" 1470 echo -e " ${YELLOW}Config:${NC} $HERMES_HOME/config.yaml" 1471 echo -e " ${YELLOW}API Keys:${NC} $HERMES_HOME/.env" 1472 echo -e " ${YELLOW}Data:${NC} $HERMES_HOME/cron/, sessions/, logs/" 1473 echo -e " ${YELLOW}Code:${NC} $INSTALL_DIR" 1474 echo "" 1475 1476 echo -e "${CYAN}─────────────────────────────────────────────────────────${NC}" 1477 echo "" 1478 echo -e "${CYAN}${BOLD}🚀 Commands:${NC}" 1479 echo "" 1480 echo -e " ${GREEN}hermes${NC} Start chatting" 1481 echo -e " ${GREEN}hermes setup${NC} Configure API keys & settings" 1482 echo -e " ${GREEN}hermes config${NC} View/edit configuration" 1483 echo -e " ${GREEN}hermes config edit${NC} Open config in editor" 1484 echo -e " ${GREEN}hermes gateway install${NC} Install gateway service (messaging + cron)" 1485 echo -e " ${GREEN}hermes update${NC} Update to latest version" 1486 echo "" 1487 1488 echo -e "${CYAN}─────────────────────────────────────────────────────────${NC}" 1489 echo "" 1490 if [ "$DISTRO" = "termux" ]; then 1491 echo -e "${YELLOW}⚡ 'hermes' was linked into $(get_command_link_display_dir), which is already on PATH in Termux.${NC}" 1492 echo "" 1493 elif [ "$ROOT_FHS_LAYOUT" = true ]; then 1494 echo -e "${YELLOW}⚡ 'hermes' was linked into /usr/local/bin and is ready to use — no shell reload needed.${NC}" 1495 echo "" 1496 else 1497 echo -e "${YELLOW}⚡ Reload your shell to use 'hermes' command:${NC}" 1498 echo "" 1499 LOGIN_SHELL="$(basename "${SHELL:-/bin/bash}")" 1500 if [ "$LOGIN_SHELL" = "zsh" ]; then 1501 echo " source ~/.zshrc" 1502 elif [ "$LOGIN_SHELL" = "bash" ]; then 1503 echo " source ~/.bashrc" 1504 elif [ "$LOGIN_SHELL" = "fish" ]; then 1505 echo " source ~/.config/fish/config.fish" 1506 else 1507 echo " source ~/.bashrc # or ~/.zshrc" 1508 fi 1509 echo "" 1510 fi 1511 1512 # Show Node.js warning if auto-install failed 1513 if [ "$HAS_NODE" = false ]; then 1514 echo -e "${YELLOW}" 1515 echo "Note: Node.js could not be installed automatically." 1516 echo "Browser tools need Node.js. Install manually:" 1517 if [ "$DISTRO" = "termux" ]; then 1518 echo " pkg install nodejs" 1519 else 1520 echo " https://nodejs.org/en/download/" 1521 fi 1522 echo -e "${NC}" 1523 fi 1524 1525 # Show ripgrep note if not installed 1526 if [ "$HAS_RIPGREP" = false ]; then 1527 echo -e "${YELLOW}" 1528 echo "Note: ripgrep (rg) was not found. File search will use" 1529 echo "grep as a fallback. For faster search in large codebases," 1530 if [ "$DISTRO" = "termux" ]; then 1531 echo "install ripgrep: pkg install ripgrep" 1532 else 1533 echo "install ripgrep: sudo apt install ripgrep (or brew install ripgrep)" 1534 fi 1535 echo -e "${NC}" 1536 fi 1537 } 1538 1539 # ============================================================================ 1540 # Main 1541 # ============================================================================ 1542 1543 main() { 1544 print_banner 1545 1546 detect_os 1547 resolve_install_layout 1548 install_uv 1549 check_python 1550 check_git 1551 check_node 1552 install_system_packages 1553 1554 clone_repo 1555 setup_venv 1556 install_deps 1557 install_node_deps 1558 setup_path 1559 copy_config_templates 1560 run_setup_wizard 1561 maybe_start_gateway 1562 1563 print_success 1564 } 1565 1566 main