#!/usr/bin/env bash # ============================================================================ # Hermes Agent on a Bucket — one-shot bootstrap # ============================================================================ # Creates your own HF bucket from the merve/hermes-agent template, mounts it, # installs Hermes, sets up Telegram, and launches the agent. # # Layout: # ~/hermes-bucket mount of YOUR bucket (config, SOUL, skills, memories, ...) # ~/.hermes-home HERMES_HOME (LOCAL). Secrets live here, everything else # is symlinked to ~/hermes-bucket so Hermes data syncs. # ~/.hermes Hermes Python install (code + venv) # # Why the shadow dir: Hermes hardcodes its secrets file at $HERMES_HOME/.env # and atomically rewrites it on `hermes setup` / sanitization. If HERMES_HOME # were the mount, those writes would land in a public bucket. The shadow dir # keeps .env local while every other Hermes write still passes through to the # bucket via symlink. # # bash <(curl -fsSL https://huggingface.co/merve/hermes-agent-bootstrap/resolve/main/bootstrap.sh) # # Idempotent: re-runs are safe. # ============================================================================ set -euo pipefail TEMPLATE="merve/hermes-agent" MOUNT="$HOME/hermes-bucket" HOME_DIR="$HOME/.hermes-home" LEGACY_SECRETS="$HOME/.hermes-secrets.env" SECRETS="$HOME_DIR/.env" G=$'\033[0;32m'; Y=$'\033[0;33m'; C=$'\033[0;36m'; R=$'\033[0;31m'; N=$'\033[0m' say() { printf "${C}→${N} %s\n" "$*"; } ok() { printf "${G}✓${N} %s\n" "$*"; } warn() { printf "${Y}!${N} %s\n" "$*"; } die() { printf "${R}✗${N} %s\n" "$*" >&2; exit 1; } ask() { local __var=$1; shift; local __prompt="$*"; read -rp " $__prompt" "$__var" /dev/null 2>&1; then say "Installing hf CLI..." curl -LsSf https://hf.co/cli/install.sh | bash export PATH="$HOME/.local/bin:$PATH" fi ok "hf CLI: $(command -v hf)" # ---------------------------------------------------------------------------- # 2. Hugging Face login # ---------------------------------------------------------------------------- if ! hf auth whoami >/dev/null 2>&1; then warn "Not logged in to Hugging Face." echo " Run this in another terminal, then re-run the bootstrap:" echo " hf auth login" exit 1 fi HF_USER="$(hf auth whoami --format json 2>/dev/null | python3 -c 'import json,sys; print(json.load(sys.stdin).get("user",""))')" [ -n "$HF_USER" ] || die "Couldn't determine your HF username from 'hf auth whoami'" HF_TOKEN_VAL="$(python3 -c 'from huggingface_hub import get_token; print(get_token() or "")' 2>/dev/null || true)" [ -n "$HF_TOKEN_VAL" ] || die "Couldn't read HF token from local cache" ok "Hugging Face: logged in as $HF_USER" BUCKET="$HF_USER/hermes-agent" # ---------------------------------------------------------------------------- # 3. Your bucket — create if missing, copy template if empty # ---------------------------------------------------------------------------- if hf buckets info "$BUCKET" >/dev/null 2>&1; then ok "Bucket $BUCKET already exists" else say "Creating $BUCKET (private)..." hf buckets create "$BUCKET" --private --exist-ok >/dev/null ok "Bucket created (private): https://huggingface.co/buckets/$BUCKET" fi FILE_COUNT="$(hf buckets list "$BUCKET" --recursive --quiet 2>/dev/null | wc -l | tr -d ' ')" if [ "${FILE_COUNT:-0}" -lt 3 ]; then say "Seeding $BUCKET from template $TEMPLATE (server-side copy, ~1s)..." python3 - </dev/null 2>&1; then say "Installing hf-mount..." curl -fsSL https://raw.githubusercontent.com/huggingface/hf-mount/main/install.sh | sh fi ok "hf-mount: $(command -v hf-mount)" # ---------------------------------------------------------------------------- # 5. Mount your bucket # ---------------------------------------------------------------------------- mkdir -p "$MOUNT" if hf-mount status 2>/dev/null | grep -q "$MOUNT"; then ok "Bucket already mounted at $MOUNT" else say "Mounting $BUCKET at $MOUNT..." hf-mount start --hf-token "$HF_TOKEN_VAL" bucket "$BUCKET" "$MOUNT" >/dev/null ok "Mounted $BUCKET → $MOUNT" fi # ---------------------------------------------------------------------------- # 6. Hermes Agent install # ---------------------------------------------------------------------------- if ! command -v hermes >/dev/null 2>&1; then say "Installing Hermes Agent (this takes a minute)..." curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash -s -- --skip-setup --skip-browser >/dev/null export PATH="$HOME/.local/bin:$PATH" fi ok "Hermes: $(command -v hermes)" # ---------------------------------------------------------------------------- # 7. Shadow HERMES_HOME — keeps .env local, symlinks everything else to bucket # ---------------------------------------------------------------------------- say "Setting up shadow HERMES_HOME at $HOME_DIR..." mkdir -p "$HOME_DIR" "$HOME_DIR/audio_cache" "$HOME_DIR/image_cache" "$HOME_DIR/logs" # Make sure bucket subdirs that Hermes writes to exist (so symlinks resolve) for d in memories cron hooks pairing sessions; do mkdir -p "$MOUNT/$d" done # Files mirrored from the bucket (read mostly; if Hermes ever rewrites one # atomically the symlink severs and the local copy takes over — safe but no # longer syncing for that file) for f in config.yaml SOUL.md README.md .env.example; do ln -sfn "$MOUNT/$f" "$HOME_DIR/$f" done # Directory trees that Hermes appends to: writes inside flow through to bucket for d in skills memories cron hooks pairing sessions; do ln -sfn "$MOUNT/$d" "$HOME_DIR/$d" done ok "Shadow dir wired: $HOME_DIR (.env stays local, data symlinks → $MOUNT)" # Migrate legacy secrets file from a previous bootstrap, if present if [ -f "$LEGACY_SECRETS" ] && [ ! -f "$SECRETS" ]; then cp "$LEGACY_SECRETS" "$SECRETS" chmod 600 "$SECRETS" warn "Migrated legacy $LEGACY_SECRETS → $SECRETS" warn " You can delete $LEGACY_SECRETS once you confirm things work." fi # ---------------------------------------------------------------------------- # 8. Telegram (required step) # ---------------------------------------------------------------------------- printf "\n${C}Telegram setup${N}\n" EXISTING_TG="" if [ -f "$SECRETS" ] && grep -q "^TELEGRAM_BOT_TOKEN=" "$SECRETS"; then EXISTING_TG="$(grep "^TELEGRAM_BOT_TOKEN=" "$SECRETS" | head -1 | cut -d= -f2-)" fi if [ -n "$EXISTING_TG" ]; then ok "Telegram bot token already on disk ($SECRETS)" TG_TOKEN="$EXISTING_TG" USER_ID="$(grep "^TELEGRAM_ALLOWED_USERS=" "$SECRETS" 2>/dev/null | head -1 | cut -d= -f2- || true)" else echo " 1. Open Telegram, message @BotFather, send /newbot, follow the prompts." echo " 2. Paste the bot token below (looks like 123456:ABC-DEF...)" echo "" ask TG_TOKEN "Telegram bot token: " [ -n "$TG_TOKEN" ] || die "Telegram bot token required" echo "" echo " 3. Now message @userinfobot on Telegram. It replies with your numeric" echo " user id (looks like 123456789). Paste it below — this goes into" echo " TELEGRAM_ALLOWED_USERS so only you can DM the bot." echo "" ask USER_ID "Your Telegram numeric user id: " [ -n "$USER_ID" ] || die "Telegram user id required" case "$USER_ID" in *[!0-9]*) die "User id must be numeric (got: $USER_ID)" ;; esac ok "Telegram user id: $USER_ID" fi # ---------------------------------------------------------------------------- # 9. Write secrets file (LOCAL, never reaches the bucket) # ---------------------------------------------------------------------------- umask 077 cat > "$SECRETS" <