947 lines
32 KiB
Bash
Executable File
947 lines
32 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"
|
|
mkdir -p "$HERMES_HOME"
|
|
mv "$extracted_dir" "$HERMES_HOME/node"
|
|
rm -rf "$tmp_dir"
|
|
|
|
mkdir -p "$HOME/.local/bin"
|
|
ln -sf "$HERMES_HOME/node/bin/node" "$HOME/.local/bin/node"
|
|
ln -sf "$HERMES_HOME/node/bin/npm" "$HOME/.local/bin/npm"
|
|
ln -sf "$HERMES_HOME/node/bin/npx" "$HOME/.local/bin/npx"
|
|
|
|
export PATH="$HERMES_HOME/node/bin:$PATH"
|
|
|
|
local installed_ver
|
|
installed_ver=$("$HERMES_HOME/node/bin/node" --version 2>/dev/null)
|
|
log_success "Node.js $installed_ver installed to ~/.hermes/node/"
|
|
HAS_NODE=true
|
|
}
|
|
|
|
install_system_packages() {
|
|
# 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
|
|
# GIT_SSH_COMMAND disables interactive prompts and sets a short timeout
|
|
# so SSH fails fast instead of hanging when no key is configured.
|
|
log_info "Trying SSH clone..."
|
|
if GIT_SSH_COMMAND="ssh -o BatchMode=yes -o ConnectTimeout=5" \
|
|
git clone --branch "$BRANCH" --recurse-submodules "$REPO_URL_SSH" "$INSTALL_DIR" 2>/dev/null; then
|
|
log_success "Cloned via SSH"
|
|
else
|
|
rm -rf "$INSTALL_DIR" 2>/dev/null # Clean up partial SSH clone
|
|
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"
|
|
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,whatsapp/session}
|
|
|
|
# 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 (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"
|
|
fi
|
|
|
|
# Install WhatsApp bridge dependencies
|
|
if [ -f "$INSTALL_DIR/scripts/whatsapp-bridge/package.json" ]; then
|
|
log_info "Installing WhatsApp bridge dependencies..."
|
|
cd "$INSTALL_DIR/scripts/whatsapp-bridge"
|
|
npm install --silent 2>/dev/null || {
|
|
log_warn "WhatsApp bridge npm install failed (WhatsApp may not work)"
|
|
}
|
|
log_success "WhatsApp bridge dependencies installed"
|
|
fi
|
|
}
|
|
|
|
run_setup_wizard() {
|
|
if [ "$RUN_SETUP" = false ]; then
|
|
log_info "Skipping setup wizard (--skip-setup)"
|
|
return 0
|
|
fi
|
|
|
|
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."
|
|
|
|
# If WhatsApp is enabled and no session exists yet, run foreground first for QR scan
|
|
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
|
|
echo ""
|
|
log_info "WhatsApp is enabled but not yet paired."
|
|
log_info "Running 'hermes whatsapp' to pair via QR code..."
|
|
echo ""
|
|
read -p "Pair WhatsApp now? [Y/n] " -n 1 -r
|
|
echo
|
|
if [[ $REPLY =~ ^[Yy]$ ]] || [[ -z $REPLY ]]; then
|
|
HERMES_CMD="$HOME/.local/bin/hermes"
|
|
[ ! -x "$HERMES_CMD" ] && HERMES_CMD="hermes"
|
|
$HERMES_CMD whatsapp || true
|
|
fi
|
|
fi
|
|
|
|
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
|