- Introduced automatic installation of Node.js version 22 if not found on the system, enhancing the setup process for browser tools. - Improved the check for existing Node.js installations, including support for Hermes-managed installations. - Added logic to download and extract the appropriate Node.js binary based on the system architecture and OS. - Updated the installation script to handle missing dependencies like ripgrep and ffmpeg, providing installation prompts for macOS users.
918 lines
31 KiB
Bash
Executable File
918 lines
31 KiB
Bash
Executable File
#!/bin/bash
|
|
# ============================================================================
|
|
# Hermes Agent Installer
|
|
# ============================================================================
|
|
# Installation script for Linux and macOS.
|
|
# Uses uv for fast Python provisioning and package management.
|
|
#
|
|
# Usage:
|
|
# curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash
|
|
#
|
|
# Or with options:
|
|
# curl -fsSL ... | bash -s -- --no-venv --skip-setup
|
|
#
|
|
# ============================================================================
|
|
|
|
set -e
|
|
|
|
# Colors
|
|
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' # No Color
|
|
BOLD='\033[1m'
|
|
|
|
# Configuration
|
|
REPO_URL_SSH="git@github.com:NousResearch/hermes-agent.git"
|
|
REPO_URL_HTTPS="https://github.com/NousResearch/hermes-agent.git"
|
|
HERMES_HOME="$HOME/.hermes"
|
|
INSTALL_DIR="${HERMES_INSTALL_DIR:-$HERMES_HOME/hermes-agent}"
|
|
PYTHON_VERSION="3.11"
|
|
NODE_VERSION="22"
|
|
|
|
# Options
|
|
USE_VENV=true
|
|
RUN_SETUP=true
|
|
BRANCH="main"
|
|
|
|
# Parse arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--no-venv)
|
|
USE_VENV=false
|
|
shift
|
|
;;
|
|
--skip-setup)
|
|
RUN_SETUP=false
|
|
shift
|
|
;;
|
|
--branch)
|
|
BRANCH="$2"
|
|
shift 2
|
|
;;
|
|
--dir)
|
|
INSTALL_DIR="$2"
|
|
shift 2
|
|
;;
|
|
-h|--help)
|
|
echo "Hermes Agent Installer"
|
|
echo ""
|
|
echo "Usage: install.sh [OPTIONS]"
|
|
echo ""
|
|
echo "Options:"
|
|
echo " --no-venv Don't create virtual environment"
|
|
echo " --skip-setup Skip interactive setup wizard"
|
|
echo " --branch NAME Git branch to install (default: main)"
|
|
echo " --dir PATH Installation directory (default: ~/.hermes/hermes-agent)"
|
|
echo " -h, --help Show this help"
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown option: $1"
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# ============================================================================
|
|
# Helper functions
|
|
# ============================================================================
|
|
|
|
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"
|
|
}
|
|
|
|
# ============================================================================
|
|
# System detection
|
|
# ============================================================================
|
|
|
|
detect_os() {
|
|
case "$(uname -s)" in
|
|
Linux*)
|
|
OS="linux"
|
|
if [ -f /etc/os-release ]; then
|
|
. /etc/os-release
|
|
DISTRO="$ID"
|
|
else
|
|
DISTRO="unknown"
|
|
fi
|
|
;;
|
|
Darwin*)
|
|
OS="macos"
|
|
DISTRO="macos"
|
|
;;
|
|
CYGWIN*|MINGW*|MSYS*)
|
|
OS="windows"
|
|
DISTRO="windows"
|
|
log_error "Windows detected. Please use the PowerShell installer:"
|
|
log_info " irm https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1 | iex"
|
|
exit 1
|
|
;;
|
|
*)
|
|
OS="unknown"
|
|
DISTRO="unknown"
|
|
log_warn "Unknown operating system"
|
|
;;
|
|
esac
|
|
|
|
log_success "Detected: $OS ($DISTRO)"
|
|
}
|
|
|
|
# ============================================================================
|
|
# Dependency checks
|
|
# ============================================================================
|
|
|
|
install_uv() {
|
|
log_info "Checking for uv package manager..."
|
|
|
|
# Check common locations for uv
|
|
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
|
|
|
|
# Check ~/.local/bin (default uv install location) even if not on PATH yet
|
|
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
|
|
|
|
# Check ~/.cargo/bin (alternative uv install location)
|
|
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
|
|
|
|
# Install uv
|
|
log_info "Installing uv (fast Python package manager)..."
|
|
if curl -LsSf https://astral.sh/uv/install.sh | sh 2>/dev/null; then
|
|
# uv installs to ~/.local/bin by default
|
|
if [ -x "$HOME/.local/bin/uv" ]; then
|
|
UV_CMD="$HOME/.local/bin/uv"
|
|
elif [ -x "$HOME/.cargo/bin/uv" ]; then
|
|
UV_CMD="$HOME/.cargo/bin/uv"
|
|
elif command -v uv &> /dev/null; then
|
|
UV_CMD="uv"
|
|
else
|
|
log_error "uv installed but not found on PATH"
|
|
log_info "Try adding ~/.local/bin to your PATH and re-running"
|
|
exit 1
|
|
fi
|
|
UV_VERSION=$($UV_CMD --version 2>/dev/null)
|
|
log_success "uv installed ($UV_VERSION)"
|
|
else
|
|
log_error "Failed to install uv"
|
|
log_info "Install manually: https://docs.astral.sh/uv/getting-started/installation/"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
check_python() {
|
|
log_info "Checking Python $PYTHON_VERSION..."
|
|
|
|
# Let uv handle Python — it can download and manage Python versions
|
|
# First check if a suitable Python is already available
|
|
if $UV_CMD python find "$PYTHON_VERSION" &> /dev/null; then
|
|
PYTHON_PATH=$($UV_CMD python find "$PYTHON_VERSION")
|
|
PYTHON_FOUND_VERSION=$($PYTHON_PATH --version 2>/dev/null)
|
|
log_success "Python found: $PYTHON_FOUND_VERSION"
|
|
return 0
|
|
fi
|
|
|
|
# Python not found — use uv to install it (no sudo needed!)
|
|
log_info "Python $PYTHON_VERSION not found, installing via uv..."
|
|
if $UV_CMD python install "$PYTHON_VERSION"; then
|
|
PYTHON_PATH=$($UV_CMD python find "$PYTHON_VERSION")
|
|
PYTHON_FOUND_VERSION=$($PYTHON_PATH --version 2>/dev/null)
|
|
log_success "Python installed: $PYTHON_FOUND_VERSION"
|
|
else
|
|
log_error "Failed to install Python $PYTHON_VERSION"
|
|
log_info "Install Python $PYTHON_VERSION manually, then re-run this script"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
check_git() {
|
|
log_info "Checking Git..."
|
|
|
|
if command -v git &> /dev/null; then
|
|
GIT_VERSION=$(git --version | awk '{print $3}')
|
|
log_success "Git $GIT_VERSION found"
|
|
return 0
|
|
fi
|
|
|
|
log_error "Git not found"
|
|
log_info "Please install Git:"
|
|
|
|
case "$OS" in
|
|
linux)
|
|
case "$DISTRO" in
|
|
ubuntu|debian)
|
|
log_info " sudo apt update && sudo apt install git"
|
|
;;
|
|
fedora)
|
|
log_info " sudo dnf install git"
|
|
;;
|
|
arch)
|
|
log_info " sudo pacman -S git"
|
|
;;
|
|
*)
|
|
log_info " Use your package manager to install git"
|
|
;;
|
|
esac
|
|
;;
|
|
macos)
|
|
log_info " xcode-select --install"
|
|
log_info " Or: brew install git"
|
|
;;
|
|
esac
|
|
|
|
exit 1
|
|
}
|
|
|
|
check_node() {
|
|
log_info "Checking Node.js (for browser tools)..."
|
|
|
|
if command -v node &> /dev/null; then
|
|
local found_ver=$(node --version)
|
|
log_success "Node.js $found_ver found"
|
|
HAS_NODE=true
|
|
return 0
|
|
fi
|
|
|
|
# Check our own managed install from a previous run
|
|
if [ -x "$HERMES_HOME/node/bin/node" ]; then
|
|
export PATH="$HERMES_HOME/node/bin:$PATH"
|
|
local found_ver=$("$HERMES_HOME/node/bin/node" --version)
|
|
log_success "Node.js $found_ver found (Hermes-managed)"
|
|
HAS_NODE=true
|
|
return 0
|
|
fi
|
|
|
|
log_info "Node.js not found — installing Node.js $NODE_VERSION LTS..."
|
|
install_node
|
|
}
|
|
|
|
install_node() {
|
|
local arch=$(uname -m)
|
|
local node_arch
|
|
case "$arch" in
|
|
x86_64) node_arch="x64" ;;
|
|
aarch64|arm64) node_arch="arm64" ;;
|
|
armv7l) node_arch="armv7l" ;;
|
|
*)
|
|
log_warn "Unsupported architecture ($arch) for Node.js auto-install"
|
|
log_info "Install manually: https://nodejs.org/en/download/"
|
|
HAS_NODE=false
|
|
return 0
|
|
;;
|
|
esac
|
|
|
|
local node_os
|
|
case "$OS" in
|
|
linux) node_os="linux" ;;
|
|
macos) node_os="darwin" ;;
|
|
*)
|
|
log_warn "Unsupported OS for Node.js auto-install"
|
|
HAS_NODE=false
|
|
return 0
|
|
;;
|
|
esac
|
|
|
|
# Resolve the latest v22.x.x tarball name from the index page
|
|
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)
|
|
|
|
# Fallback to .tar.gz if .tar.xz not available
|
|
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
|
|
|
|
# Place into ~/.hermes/node/ and symlink binaries to ~/.local/bin/
|
|
rm -rf "$HERMES_HOME/node"
|
|
mv "$extracted_dir" "$HERMES_HOME/node"
|
|
rm -rf "$tmp_dir"
|
|
|
|
mkdir -p "$HOME/.local/bin"
|
|
ln -sf "$HERMES_HOME/node/bin/node" "$HOME/.local/bin/node"
|
|
ln -sf "$HERMES_HOME/node/bin/npm" "$HOME/.local/bin/npm"
|
|
ln -sf "$HERMES_HOME/node/bin/npx" "$HOME/.local/bin/npx"
|
|
|
|
export PATH="$HERMES_HOME/node/bin:$PATH"
|
|
|
|
local installed_ver
|
|
installed_ver=$("$HERMES_HOME/node/bin/node" --version 2>/dev/null)
|
|
log_success "Node.js $installed_ver installed to ~/.hermes/node/"
|
|
HAS_NODE=true
|
|
}
|
|
|
|
install_system_packages() {
|
|
# Detect what's missing
|
|
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
|
|
|
|
# Nothing to install — done
|
|
if [ "$need_ripgrep" = false ] && [ "$need_ffmpeg" = false ]; then
|
|
return 0
|
|
fi
|
|
|
|
# Build a human-readable description + package list
|
|
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[*]}")
|
|
|
|
# ── macOS: brew ──
|
|
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
|
|
|
|
# ── Linux: resolve package manager command ──
|
|
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[*]}"
|
|
|
|
# Already root — just install
|
|
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
|
|
# Passwordless sudo — just install
|
|
elif command -v sudo &> /dev/null && sudo -n true 2>/dev/null; then
|
|
log_info "Installing ${pkgs[*]}..."
|
|
if sudo $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
|
|
# sudo needs password — ask once for everything
|
|
elif command -v sudo &> /dev/null; then
|
|
echo ""
|
|
read -p "Install ${description}? (requires sudo) [y/N] " -n 1 -r
|
|
echo
|
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
if sudo $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
|
|
fi
|
|
fi
|
|
|
|
# ── Fallback for ripgrep: cargo ──
|
|
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
|
|
|
|
# ── Show manual instructions for anything still missing ──
|
|
if [ "$HAS_RIPGREP" = false ] && [ "$need_ripgrep" = true ]; then
|
|
log_warn "ripgrep not installed (file search will use grep fallback)"
|
|
show_manual_install_hint "ripgrep"
|
|
fi
|
|
if [ "$HAS_FFMPEG" = false ] && [ "$need_ffmpeg" = true ]; then
|
|
log_warn "ffmpeg not installed (TTS voice messages will be limited)"
|
|
show_manual_install_hint "ffmpeg"
|
|
fi
|
|
}
|
|
|
|
show_manual_install_hint() {
|
|
local pkg="$1"
|
|
log_info "To install $pkg manually:"
|
|
case "$OS" in
|
|
linux)
|
|
case "$DISTRO" in
|
|
ubuntu|debian) log_info " sudo apt install $pkg" ;;
|
|
fedora) log_info " sudo dnf install $pkg" ;;
|
|
arch) log_info " sudo pacman -S $pkg" ;;
|
|
*) log_info " Use your package manager or visit the project homepage" ;;
|
|
esac
|
|
;;
|
|
macos) log_info " brew install $pkg" ;;
|
|
esac
|
|
}
|
|
|
|
# ============================================================================
|
|
# Installation
|
|
# ============================================================================
|
|
|
|
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"
|
|
git fetch origin
|
|
git checkout "$BRANCH"
|
|
git pull origin "$BRANCH"
|
|
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
|
|
# Try SSH first (for private repo access), fall back to HTTPS
|
|
# Use --recurse-submodules to also clone mini-swe-agent and tinker-atropos
|
|
log_info "Trying SSH clone..."
|
|
if git clone --branch "$BRANCH" --recurse-submodules "$REPO_URL_SSH" "$INSTALL_DIR" 2>/dev/null; then
|
|
log_success "Cloned via SSH"
|
|
else
|
|
log_info "SSH failed, trying HTTPS..."
|
|
if git clone --branch "$BRANCH" --recurse-submodules "$REPO_URL_HTTPS" "$INSTALL_DIR"; then
|
|
log_success "Cloned via HTTPS"
|
|
else
|
|
log_error "Failed to clone repository"
|
|
log_info "For private repo access, ensure your SSH key is added to GitHub:"
|
|
log_info " ssh-add ~/.ssh/id_rsa"
|
|
log_info " ssh -T git@github.com # Test connection"
|
|
exit 1
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
cd "$INSTALL_DIR"
|
|
|
|
# Ensure submodules are initialized and updated (for existing installs or if --recurse failed)
|
|
log_info "Initializing submodules (mini-swe-agent, tinker-atropos)..."
|
|
git submodule update --init --recursive
|
|
log_success "Submodules ready"
|
|
|
|
log_success "Repository ready"
|
|
}
|
|
|
|
setup_venv() {
|
|
if [ "$USE_VENV" = false ]; then
|
|
log_info "Skipping virtual environment (--no-venv)"
|
|
return 0
|
|
fi
|
|
|
|
log_info "Creating virtual environment with Python $PYTHON_VERSION..."
|
|
|
|
if [ -d "venv" ]; then
|
|
log_info "Virtual environment already exists, recreating..."
|
|
rm -rf venv
|
|
fi
|
|
|
|
# uv creates the venv and pins the Python version in one step
|
|
$UV_CMD venv venv --python "$PYTHON_VERSION"
|
|
|
|
log_success "Virtual environment ready (Python $PYTHON_VERSION)"
|
|
}
|
|
|
|
install_deps() {
|
|
log_info "Installing dependencies..."
|
|
|
|
if [ "$USE_VENV" = true ]; then
|
|
# Tell uv to install into our venv (no need to activate)
|
|
export VIRTUAL_ENV="$INSTALL_DIR/venv"
|
|
fi
|
|
|
|
# Install the main package in editable mode with all extras
|
|
$UV_CMD pip install -e ".[all]" || $UV_CMD pip install -e "."
|
|
|
|
log_success "Main package installed"
|
|
|
|
# Install submodules
|
|
log_info "Installing mini-swe-agent (terminal tool backend)..."
|
|
if [ -d "mini-swe-agent" ] && [ -f "mini-swe-agent/pyproject.toml" ]; then
|
|
$UV_CMD pip install -e "./mini-swe-agent" || log_warn "mini-swe-agent install failed (terminal tools may not work)"
|
|
log_success "mini-swe-agent installed"
|
|
else
|
|
log_warn "mini-swe-agent not found (run: git submodule update --init)"
|
|
fi
|
|
|
|
log_info "Installing tinker-atropos (RL training backend)..."
|
|
if [ -d "tinker-atropos" ] && [ -f "tinker-atropos/pyproject.toml" ]; then
|
|
$UV_CMD pip install -e "./tinker-atropos" || log_warn "tinker-atropos install failed (RL tools may not work)"
|
|
log_success "tinker-atropos installed"
|
|
else
|
|
log_warn "tinker-atropos not found (run: git submodule update --init)"
|
|
fi
|
|
|
|
log_success "All dependencies installed"
|
|
}
|
|
|
|
setup_path() {
|
|
log_info "Setting up hermes command..."
|
|
|
|
if [ "$USE_VENV" = true ]; then
|
|
HERMES_BIN="$INSTALL_DIR/venv/bin/hermes"
|
|
else
|
|
HERMES_BIN="$(which hermes 2>/dev/null || echo "")"
|
|
if [ -z "$HERMES_BIN" ]; then
|
|
log_warn "hermes not found on PATH after install"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
# Create symlink in ~/.local/bin (standard user binary location, usually on PATH)
|
|
mkdir -p "$HOME/.local/bin"
|
|
ln -sf "$HERMES_BIN" "$HOME/.local/bin/hermes"
|
|
log_success "Symlinked hermes → ~/.local/bin/hermes"
|
|
|
|
# Check if ~/.local/bin is on PATH; if not, add it to shell config
|
|
if ! echo "$PATH" | tr ':' '\n' | grep -q "^$HOME/.local/bin$"; then
|
|
SHELL_CONFIG=""
|
|
if [ -n "$BASH_VERSION" ]; then
|
|
if [ -f "$HOME/.bashrc" ]; then
|
|
SHELL_CONFIG="$HOME/.bashrc"
|
|
elif [ -f "$HOME/.bash_profile" ]; then
|
|
SHELL_CONFIG="$HOME/.bash_profile"
|
|
fi
|
|
elif [ -n "$ZSH_VERSION" ] || [ -f "$HOME/.zshrc" ]; then
|
|
SHELL_CONFIG="$HOME/.zshrc"
|
|
fi
|
|
|
|
PATH_LINE='export PATH="$HOME/.local/bin:$PATH"'
|
|
|
|
if [ -n "$SHELL_CONFIG" ]; then
|
|
if ! grep -q '\.local/bin' "$SHELL_CONFIG" 2>/dev/null; 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"
|
|
else
|
|
log_info "~/.local/bin already referenced in $SHELL_CONFIG"
|
|
fi
|
|
fi
|
|
else
|
|
log_info "~/.local/bin already on PATH"
|
|
fi
|
|
|
|
# Export for current session so hermes works immediately
|
|
export PATH="$HOME/.local/bin:$PATH"
|
|
|
|
log_success "hermes command ready"
|
|
}
|
|
|
|
copy_config_templates() {
|
|
log_info "Setting up configuration files..."
|
|
|
|
# Create ~/.hermes directory structure (config at top level, code in subdir)
|
|
mkdir -p "$HERMES_HOME"/{cron,sessions,logs,pairing,hooks,image_cache,audio_cache,memories,skills}
|
|
|
|
# Create .env at ~/.hermes/.env (top level, easy to find)
|
|
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
|
|
|
|
# Create config.yaml at ~/.hermes/config.yaml (top level, easy to find)
|
|
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
|
|
|
|
# Create SOUL.md if it doesn't exist (global persona file)
|
|
if [ ! -f "$HERMES_HOME/SOUL.md" ]; then
|
|
cat > "$HERMES_HOME/SOUL.md" << 'SOUL_EOF'
|
|
# Hermes Agent Persona
|
|
|
|
<!--
|
|
This file defines the agent's personality and tone.
|
|
The agent will embody whatever you write here.
|
|
Edit this to customize how Hermes communicates with you.
|
|
|
|
Examples:
|
|
- "You are a warm, playful assistant who uses kaomoji occasionally."
|
|
- "You are a concise technical expert. No fluff, just facts."
|
|
- "You speak like a friendly coworker who happens to know everything."
|
|
|
|
This file is loaded fresh each message -- no restart needed.
|
|
Delete the contents (or this file) to use the default personality.
|
|
-->
|
|
SOUL_EOF
|
|
log_success "Created ~/.hermes/SOUL.md (edit to customize personality)"
|
|
fi
|
|
|
|
log_success "Configuration directory ready: ~/.hermes/"
|
|
|
|
# Seed bundled skills into ~/.hermes/skills/ (manifest-based, one-time per skill)
|
|
log_info "Syncing bundled skills to ~/.hermes/skills/ ..."
|
|
if "$INSTALL_DIR/venv/bin/python" "$INSTALL_DIR/tools/skills_sync.py" 2>/dev/null; then
|
|
log_success "Skills synced to ~/.hermes/skills/"
|
|
else
|
|
# Fallback: simple directory copy if Python sync fails
|
|
if [ -d "$INSTALL_DIR/skills" ] && [ ! "$(ls -A "$HERMES_HOME/skills/" 2>/dev/null | grep -v '.bundled_manifest')" ]; then
|
|
cp -r "$INSTALL_DIR/skills/"* "$HERMES_HOME/skills/" 2>/dev/null || true
|
|
log_success "Skills copied to ~/.hermes/skills/"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
install_node_deps() {
|
|
if [ "$HAS_NODE" = false ]; then
|
|
log_info "Skipping Node.js dependencies (Node not installed)"
|
|
return 0
|
|
fi
|
|
|
|
if [ -f "$INSTALL_DIR/package.json" ]; then
|
|
log_info "Installing Node.js dependencies..."
|
|
cd "$INSTALL_DIR"
|
|
npm install --silent 2>/dev/null || {
|
|
log_warn "npm install failed (browser tools may not work)"
|
|
return 0
|
|
}
|
|
log_success "Node.js dependencies installed"
|
|
fi
|
|
}
|
|
|
|
run_setup_wizard() {
|
|
if [ "$RUN_SETUP" = false ]; then
|
|
log_info "Skipping setup wizard (--skip-setup)"
|
|
return 0
|
|
fi
|
|
|
|
echo ""
|
|
log_info "Starting setup wizard..."
|
|
echo ""
|
|
|
|
cd "$INSTALL_DIR"
|
|
|
|
# Run hermes setup using the venv Python directly (no activation needed)
|
|
if [ "$USE_VENV" = true ]; then
|
|
"$INSTALL_DIR/venv/bin/python" -m hermes_cli.main setup
|
|
else
|
|
python -m hermes_cli.main setup
|
|
fi
|
|
}
|
|
|
|
maybe_start_gateway() {
|
|
# Check if any messaging platform tokens were configured
|
|
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."
|
|
echo ""
|
|
read -p "Would you like to install the gateway as a background service? [Y/n] " -n 1 -r
|
|
echo
|
|
|
|
if [[ $REPLY =~ ^[Yy]$ ]] || [[ -z $REPLY ]]; then
|
|
HERMES_CMD="$HOME/.local/bin/hermes"
|
|
if [ ! -x "$HERMES_CMD" ]; then
|
|
HERMES_CMD="hermes"
|
|
fi
|
|
|
|
if command -v systemctl &> /dev/null; then
|
|
log_info "Installing systemd service..."
|
|
if $HERMES_CMD gateway install 2>/dev/null; then
|
|
log_success "Gateway service installed"
|
|
if $HERMES_CMD gateway start 2>/dev/null; then
|
|
log_success "Gateway started! Your bot is now online."
|
|
else
|
|
log_warn "Service installed but failed to start. Try: hermes gateway start"
|
|
fi
|
|
else
|
|
log_warn "Systemd install failed. You can start manually: hermes gateway"
|
|
fi
|
|
else
|
|
log_info "systemd not available — starting gateway in background..."
|
|
nohup $HERMES_CMD gateway > "$HERMES_HOME/logs/gateway.log" 2>&1 &
|
|
GATEWAY_PID=$!
|
|
log_success "Gateway started (PID $GATEWAY_PID). Logs: ~/.hermes/logs/gateway.log"
|
|
log_info "To stop: kill $GATEWAY_PID"
|
|
log_info "To restart later: hermes gateway"
|
|
fi
|
|
else
|
|
log_info "Skipped. Start the gateway later with: hermes gateway"
|
|
fi
|
|
}
|
|
|
|
print_success() {
|
|
echo ""
|
|
echo -e "${GREEN}${BOLD}"
|
|
echo "┌─────────────────────────────────────────────────────────┐"
|
|
echo "│ ✓ Installation Complete! │"
|
|
echo "└─────────────────────────────────────────────────────────┘"
|
|
echo -e "${NC}"
|
|
echo ""
|
|
|
|
# Show file locations
|
|
echo -e "${CYAN}${BOLD}📁 Your files (all in ~/.hermes/):${NC}"
|
|
echo ""
|
|
echo -e " ${YELLOW}Config:${NC} ~/.hermes/config.yaml"
|
|
echo -e " ${YELLOW}API Keys:${NC} ~/.hermes/.env"
|
|
echo -e " ${YELLOW}Data:${NC} ~/.hermes/cron/, sessions/, logs/"
|
|
echo -e " ${YELLOW}Code:${NC} ~/.hermes/hermes-agent/"
|
|
echo ""
|
|
|
|
echo -e "${CYAN}─────────────────────────────────────────────────────────${NC}"
|
|
echo ""
|
|
echo -e "${CYAN}${BOLD}🚀 Commands:${NC}"
|
|
echo ""
|
|
echo -e " ${GREEN}hermes${NC} Start chatting"
|
|
echo -e " ${GREEN}hermes setup${NC} Configure API keys & settings"
|
|
echo -e " ${GREEN}hermes config${NC} View/edit configuration"
|
|
echo -e " ${GREEN}hermes config edit${NC} Open config in editor"
|
|
echo -e " ${GREEN}hermes gateway install${NC} Install gateway service (messaging + cron)"
|
|
echo -e " ${GREEN}hermes update${NC} Update to latest version"
|
|
echo ""
|
|
|
|
echo -e "${CYAN}─────────────────────────────────────────────────────────${NC}"
|
|
echo ""
|
|
echo -e "${YELLOW}⚡ Reload your shell to use 'hermes' command:${NC}"
|
|
echo ""
|
|
echo " source ~/.bashrc # or ~/.zshrc"
|
|
echo ""
|
|
|
|
# Show Node.js warning if auto-install failed
|
|
if [ "$HAS_NODE" = false ]; then
|
|
echo -e "${YELLOW}"
|
|
echo "Note: Node.js could not be installed automatically."
|
|
echo "Browser tools need Node.js. Install manually:"
|
|
echo " https://nodejs.org/en/download/"
|
|
echo -e "${NC}"
|
|
fi
|
|
|
|
# Show ripgrep note if not installed
|
|
if [ "$HAS_RIPGREP" = false ]; then
|
|
echo -e "${YELLOW}"
|
|
echo "Note: ripgrep (rg) was not found. File search will use"
|
|
echo "grep as a fallback. For faster search in large codebases,"
|
|
echo "install ripgrep: sudo apt install ripgrep (or brew install ripgrep)"
|
|
echo -e "${NC}"
|
|
fi
|
|
}
|
|
|
|
# ============================================================================
|
|
# Main
|
|
# ============================================================================
|
|
|
|
main() {
|
|
print_banner
|
|
|
|
detect_os
|
|
install_uv
|
|
check_python
|
|
check_git
|
|
check_node
|
|
install_system_packages
|
|
|
|
clone_repo
|
|
setup_venv
|
|
install_deps
|
|
install_node_deps
|
|
setup_path
|
|
copy_config_templates
|
|
run_setup_wizard
|
|
maybe_start_gateway
|
|
|
|
print_success
|
|
}
|
|
|
|
main
|