Skip to content

Instantly share code, notes, and snippets.

@as3k
Created April 27, 2026 16:33
Show Gist options
  • Select an option

  • Save as3k/935d39622e37356ef49f93887cf00d42 to your computer and use it in GitHub Desktop.

Select an option

Save as3k/935d39622e37356ef49f93887cf00d42 to your computer and use it in GitHub Desktop.
alpine hermes agent
#!/bin/sh
# ============================================================================
# Hermes Agent Installer — Alpine Linux
# ============================================================================
# Alpine uses musl libc, busybox sh, and apk. Adapted accordingly.
# Usage:
# wget -qO- https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | sh
# Or: sh install.sh [--no-venv] [--skip-setup] [--branch NAME] [--dir PATH]
# ============================================================================
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
NC='\033[0m'
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 (busybox sh compatible — no [[ ]])
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)
printf "Hermes Agent Installer\n\nUsage: install.sh [OPTIONS]\n\n"
printf " --no-venv Don't create virtual environment\n"
printf " --skip-setup Skip interactive setup wizard\n"
printf " --branch NAME Git branch to install (default: main)\n"
printf " --dir PATH Installation directory (default: ~/.hermes/hermes-agent)\n"
exit 0
;;
*) printf "Unknown option: %s\n" "$1"; exit 1 ;;
esac
done
# ============================================================================
# Helpers
# ============================================================================
print_banner() {
printf "\n${MAGENTA}${BOLD}"
printf "┌─────────────────────────────────────────────────────────┐\n"
printf "│ ⚕ Hermes Agent Installer (Alpine) │\n"
printf "├─────────────────────────────────────────────────────────┤\n"
printf "│ An open source AI agent by Nous Research. │\n"
printf "└─────────────────────────────────────────────────────────┘\n"
printf "${NC}\n"
}
log_info() { printf "${CYAN}→${NC} %s\n" "$1"; }
log_success() { printf "${GREEN}✓${NC} %s\n" "$1"; }
log_warn() { printf "${YELLOW}⚠${NC} %s\n" "$1"; }
log_error() { printf "${RED}✗${NC} %s\n" "$1"; }
# ============================================================================
# System detection
# ============================================================================
detect_os() {
OS="linux"
DISTRO="alpine"
if [ -f /etc/os-release ]; then
# shellcheck disable=SC1091
. /etc/os-release
DISTRO="${ID:-alpine}"
fi
log_success "Detected: $OS ($DISTRO)"
if [ "$DISTRO" != "alpine" ]; then
log_warn "This script is optimised for Alpine. Proceeding anyway — some steps may fail."
fi
}
# ============================================================================
# Dependency checks
# ============================================================================
require_apk_pkg() {
PKG="$1"
if ! command -v "$2" > /dev/null 2>&1; then
log_info "Installing $PKG via apk..."
if [ "$(id -u)" -eq 0 ]; then
apk add --no-cache "$PKG"
elif command -v sudo > /dev/null 2>&1; then
sudo apk add --no-cache "$PKG"
else
log_error "Cannot install $PKG — run as root or install sudo."
exit 1
fi
fi
}
install_uv() {
log_info "Checking for uv package manager..."
UV_CMD=""
for candidate in uv "$HOME/.local/bin/uv" "$HOME/.cargo/bin/uv"; do
if [ -x "$candidate" ] || command -v "$candidate" > /dev/null 2>&1; then
UV_CMD="$candidate"
break
fi
done
if [ -n "$UV_CMD" ]; then
UV_VERSION=$("$UV_CMD" --version 2>/dev/null)
log_success "uv found ($UV_VERSION)"
return 0
fi
# uv's installer needs curl and sh; Alpine needs libgcc for the uv binary
log_info "Installing uv (fast Python package manager)..."
require_apk_pkg curl curl
# uv on musl needs libgcc (it ships a glibc binary; use musl-compatible static build via installer)
apk add --no-cache libgcc 2>/dev/null || true
if curl -LsSf https://astral.sh/uv/install.sh | sh; then
for candidate in "$HOME/.local/bin/uv" "$HOME/.cargo/bin/uv" uv; do
if [ -x "$candidate" ] || command -v "$candidate" > /dev/null 2>&1; then
UV_CMD="$candidate"
break
fi
done
if [ -z "$UV_CMD" ]; then
log_error "uv installed but not found on PATH. Add ~/.local/bin to PATH and retry."
exit 1
fi
UV_VERSION=$("$UV_CMD" --version 2>/dev/null)
log_success "uv installed ($UV_VERSION)"
else
log_error "Failed to install uv."
log_info "Install manually: https://docs.astral.sh/uv/getting-started/installation/"
exit 1
fi
}
check_python() {
log_info "Checking Python $PYTHON_VERSION..."
if "$UV_CMD" python find "$PYTHON_VERSION" > /dev/null 2>&1; 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
# uv can download a standalone Python build (musl-compatible for Alpine)
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
# Fallback: system python3 from apk
log_warn "uv python install failed, trying apk python3..."
require_apk_pkg python3 python3
apk add --no-cache py3-pip 2>/dev/null || true
PYTHON_PATH="$(command -v python3)"
log_success "Using system Python: $("$PYTHON_PATH" --version 2>/dev/null)"
fi
}
check_git() {
log_info "Checking Git..."
if command -v git > /dev/null 2>&1; then
log_success "Git $(git --version | awk '{print $3}') found"
return 0
fi
log_info "Git not found — installing via apk..."
require_apk_pkg git git
log_success "Git installed"
}
check_node() {
log_info "Checking Node.js (for browser tools)..."
HAS_NODE=false
if command -v node > /dev/null 2>&1; then
log_success "Node.js $(node --version) found"
HAS_NODE=true
return 0
fi
if [ -x "$HERMES_HOME/node/bin/node" ]; then
export PATH="$HERMES_HOME/node/bin:$PATH"
log_success "Node.js $("$HERMES_HOME/node/bin/node" --version) found (Hermes-managed)"
HAS_NODE=true
return 0
fi
log_info "Node.js not found — attempting install..."
install_node
}
install_node() {
# Prefer apk on Alpine — much simpler and musl-native
if apk info nodejs > /dev/null 2>&1 || apk add --no-cache nodejs npm 2>/dev/null; then
log_success "Node.js $(node --version) installed via apk"
HAS_NODE=true
return 0
fi
# Fallback: official tarball (Alpine needs the musl build, which Node doesn't ship;
# the glibc binary works if gcompat is installed)
ARCH=$(uname -m)
case "$ARCH" in
x86_64) NODE_ARCH="x64" ;;
aarch64|arm64) NODE_ARCH="arm64" ;;
*) log_warn "Unsupported arch ($ARCH) for Node tarball install"; HAS_NODE=false; return 0 ;;
esac
log_info "Installing gcompat for glibc compatibility..."
apk add --no-cache gcompat 2>/dev/null || true
INDEX_URL="https://nodejs.org/dist/latest-v${NODE_VERSION}.x/"
TARBALL=$(wget -qO- "$INDEX_URL" \
| grep -oE "node-v${NODE_VERSION}\.[0-9]+\.[0-9]+-linux-${NODE_ARCH}\.tar\.xz" \
| head -1)
if [ -z "$TARBALL" ]; then
log_warn "Could not resolve Node.js tarball. Install manually: https://nodejs.org/en/download/"
HAS_NODE=false
return 0
fi
TMP=$(mktemp -d)
log_info "Downloading $TARBALL..."
wget -qO "$TMP/$TARBALL" "${INDEX_URL}${TARBALL}" || { log_warn "Download failed"; HAS_NODE=false; rm -rf "$TMP"; return 0; }
tar xf "$TMP/$TARBALL" -C "$TMP"
EXTRACTED=$(find "$TMP" -maxdepth 1 -name "node-v*" -type d | head -1)
[ -d "$EXTRACTED" ] || { log_warn "Extraction failed"; HAS_NODE=false; rm -rf "$TMP"; return 0; }
rm -rf "$HERMES_HOME/node"
mv "$EXTRACTED" "$HERMES_HOME/node"
rm -rf "$TMP"
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"
log_success "Node.js $("$HERMES_HOME/node/bin/node" --version) installed to ~/.hermes/node/"
HAS_NODE=true
}
install_system_packages() {
HAS_RIPGREP=false
HAS_FFMPEG=false
log_info "Checking ripgrep..."
if command -v rg > /dev/null 2>&1; then
log_success "$(rg --version | head -1) found"
HAS_RIPGREP=true
else
if apk add --no-cache ripgrep 2>/dev/null; then
log_success "ripgrep installed"
HAS_RIPGREP=true
else
log_warn "ripgrep not available — file search will fall back to grep"
fi
fi
log_info "Checking ffmpeg..."
if command -v ffmpeg > /dev/null 2>&1; then
log_success "ffmpeg $(ffmpeg -version 2>/dev/null | head -1 | awk '{print $3}') found"
HAS_FFMPEG=true
else
if apk add --no-cache ffmpeg 2>/dev/null; then
log_success "ffmpeg installed"
HAS_FFMPEG=true
else
log_warn "ffmpeg not available — TTS voice messages will be limited"
fi
fi
}
# ============================================================================
# Installation
# ============================================================================
clone_repo() {
log_info "Installing to $INSTALL_DIR..."
if [ -d "$INSTALL_DIR" ]; then
if [ -d "$INSTALL_DIR/.git" ]; then
log_info "Existing install 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 repo: $INSTALL_DIR"
log_info "Remove it or use --dir to choose another path."
exit 1
fi
else
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."
exit 1
fi
fi
fi
cd "$INSTALL_DIR"
log_info "Initializing submodules..."
git submodule update --init --recursive
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..."
[ -d "venv" ] && rm -rf venv
"$UV_CMD" venv venv --python "$PYTHON_VERSION"
log_success "Virtual environment ready"
}
install_deps() {
log_info "Installing dependencies..."
if [ "$USE_VENV" = "true" ]; then
export VIRTUAL_ENV="$INSTALL_DIR/venv"
fi
"$UV_CMD" pip install -e ".[all]" 2>/dev/null || "$UV_CMD" pip install -e "."
log_success "Main package installed"
log_info "Installing mini-swe-agent..."
if [ -f "mini-swe-agent/pyproject.toml" ]; then
"$UV_CMD" pip install -e "./mini-swe-agent" || log_warn "mini-swe-agent install failed"
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..."
if [ -f "tinker-atropos/pyproject.toml" ]; then
"$UV_CMD" pip install -e "./tinker-atropos" || log_warn "tinker-atropos install failed"
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="$(command -v hermes 2>/dev/null || true)"
if [ -z "$HERMES_BIN" ]; then
log_warn "hermes not found on PATH after install"
return 0
fi
fi
mkdir -p "$HOME/.local/bin"
ln -sf "$HERMES_BIN" "$HOME/.local/bin/hermes"
log_success "Symlinked hermes → ~/.local/bin/hermes"
PATH_LINE='export PATH="$HOME/.local/bin:$PATH"'
# Detect shell config — Alpine default shell is ash/sh but users may have bash/zsh
SHELL_CONFIG=""
if [ -f "$HOME/.bashrc" ]; then
SHELL_CONFIG="$HOME/.bashrc"
elif [ -f "$HOME/.bash_profile" ]; then
SHELL_CONFIG="$HOME/.bash_profile"
elif [ -f "$HOME/.zshrc" ]; then
SHELL_CONFIG="$HOME/.zshrc"
elif [ -f "$HOME/.profile" ]; then
SHELL_CONFIG="$HOME/.profile"
fi
if ! printf '%s' "$PATH" | tr ':' '\n' | grep -qx "$HOME/.local/bin"; then
if [ -n "$SHELL_CONFIG" ]; then
if ! grep -q '\.local/bin' "$SHELL_CONFIG" 2>/dev/null; then
printf '\n# Hermes Agent — ensure ~/.local/bin is on PATH\n%s\n' "$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 PATH="$HOME/.local/bin:$PATH"
log_success "hermes command ready"
}
copy_config_templates() {
log_info "Setting up configuration files..."
mkdir -p "$HERMES_HOME/cron" "$HERMES_HOME/sessions" "$HERMES_HOME/logs" \
"$HERMES_HOME/pairing" "$HERMES_HOME/hooks" "$HERMES_HOME/image_cache" \
"$HERMES_HOME/audio_cache" "$HERMES_HOME/memories" "$HERMES_HOME/skills"
if [ ! -f "$HERMES_HOME/.env" ]; then
if [ -f "$INSTALL_DIR/.env.example" ]; then
cp "$INSTALL_DIR/.env.example" "$HERMES_HOME/.env"
else
touch "$HERMES_HOME/.env"
fi
log_success "Created ~/.hermes/.env"
else
log_info "~/.hermes/.env already exists, keeping it"
fi
if [ ! -f "$HERMES_HOME/config.yaml" ] && [ -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"
fi
if [ ! -f "$HERMES_HOME/SOUL.md" ]; then
cat > "$HERMES_HOME/SOUL.md" << 'SOUL_EOF'
# Hermes Agent Persona
<!--
Edit this to customise how Hermes communicates with you.
Loaded fresh each message — no restart needed.
-->
SOUL_EOF
log_success "Created ~/.hermes/SOUL.md"
fi
log_info "Syncing bundled skills..."
if "$INSTALL_DIR/venv/bin/python" "$INSTALL_DIR/tools/skills_sync.py" 2>/dev/null; then
log_success "Skills synced"
elif [ -d "$INSTALL_DIR/skills" ]; then
cp -r "$INSTALL_DIR/skills/"* "$HERMES_HOME/skills/" 2>/dev/null || true
log_success "Skills copied"
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)"
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
printf "\n"
log_info "Starting setup wizard..."
printf "\n"
cd "$INSTALL_DIR"
if [ "$USE_VENV" = "true" ]; then
"$INSTALL_DIR/venv/bin/python" -m hermes_cli.main setup
else
python3 -m hermes_cli.main setup
fi
}
maybe_start_gateway() {
ENV_FILE="$HERMES_HOME/.env"
[ -f "$ENV_FILE" ] || return 0
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
[ "$HAS_MESSAGING" = "true" ] || return 0
printf "\n"
log_info "Messaging platform token detected!"
log_info "The gateway must be running for Hermes to send/receive messages."
printf "\n"
printf "Install gateway as a background service? [Y/n] "
read -r REPLY
case "$REPLY" in
""|y|Y)
HERMES_CMD="$HOME/.local/bin/hermes"
command -v "$HERMES_CMD" > /dev/null 2>&1 || HERMES_CMD="hermes"
# Alpine uses OpenRC, not systemd
if command -v rc-service > /dev/null 2>&1 || command -v openrc > /dev/null 2>&1; then
log_info "OpenRC detected — installing service..."
if "$HERMES_CMD" gateway install 2>/dev/null; then
log_success "Gateway service installed"
"$HERMES_CMD" gateway start 2>/dev/null && log_success "Gateway started!" \
|| log_warn "Service installed but failed to start. Try: hermes gateway start"
else
log_warn "OpenRC install failed. Starting gateway in background..."
nohup "$HERMES_CMD" gateway > "$HERMES_HOME/logs/gateway.log" 2>&1 &
log_success "Gateway started (PID $!). Logs: ~/.hermes/logs/gateway.log"
fi
elif command -v systemctl > /dev/null 2>&1; then
# Some Alpine setups run systemd inside containers
log_info "systemd detected — installing service..."
if "$HERMES_CMD" gateway install 2>/dev/null; then
log_success "Gateway service installed"
"$HERMES_CMD" gateway start 2>/dev/null && log_success "Gateway started!" \
|| log_warn "Try: hermes gateway start"
else
log_warn "systemd install failed. Starting in background..."
nohup "$HERMES_CMD" gateway > "$HERMES_HOME/logs/gateway.log" 2>&1 &
log_success "Gateway started (PID $!)"
fi
else
log_info "No init system detected — starting gateway in background..."
nohup "$HERMES_CMD" gateway > "$HERMES_HOME/logs/gateway.log" 2>&1 &
log_success "Gateway started (PID $!). Logs: ~/.hermes/logs/gateway.log"
log_info "To stop: kill $!"
log_info "To restart: hermes gateway"
fi
;;
*)
log_info "Skipped. Start later with: hermes gateway"
;;
esac
}
print_success() {
printf "\n${GREEN}${BOLD}"
printf "┌─────────────────────────────────────────────────────────┐\n"
printf "│ ✓ Installation Complete! │\n"
printf "└─────────────────────────────────────────────────────────┘\n"
printf "${NC}\n\n"
printf "${CYAN}${BOLD}📁 Your files (all in ~/.hermes/):${NC}\n\n"
printf " ${YELLOW}Config:${NC} ~/.hermes/config.yaml\n"
printf " ${YELLOW}API Keys:${NC} ~/.hermes/.env\n"
printf " ${YELLOW}Data:${NC} ~/.hermes/cron/, sessions/, logs/\n"
printf " ${YELLOW}Code:${NC} ~/.hermes/hermes-agent/\n\n"
printf "${CYAN}${BOLD}🚀 Commands:${NC}\n\n"
printf " ${GREEN}hermes${NC} Start chatting\n"
printf " ${GREEN}hermes setup${NC} Configure API keys & settings\n"
printf " ${GREEN}hermes config${NC} View/edit configuration\n"
printf " ${GREEN}hermes gateway install${NC} Install gateway service\n"
printf " ${GREEN}hermes update${NC} Update to latest version\n\n"
printf "${YELLOW}⚡ Reload your shell to use 'hermes':${NC}\n\n"
printf " source ~/.profile # or ~/.bashrc / ~/.zshrc\n\n"
[ "$HAS_NODE" = "false" ] && printf "${YELLOW}Note: Node.js not installed. Browser tools need it: https://nodejs.org/en/download/${NC}\n\n"
[ "$HAS_RIPGREP" = "false" ] && printf "${YELLOW}Note: ripgrep not found. File search will use grep. Install: apk add ripgrep${NC}\n\n"
}
# ============================================================================
# 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment