| #!/usr/bin/env bash |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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/tty; } |
|
|
| export PATH="$HOME/.local/bin:$PATH" |
|
|
| printf "\n${C}β Hermes Agent β bucket bootstrap${N}\n\n" |
|
|
| |
| |
| |
| if ! command -v hf >/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)" |
|
|
| |
| |
| |
| 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" |
|
|
| |
| |
| |
| 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 - <<PY |
| from huggingface_hub import HfApi |
| HfApi().copy_files( |
| source="hf://buckets/$TEMPLATE/", |
| destination="hf://buckets/$BUCKET/", |
| ) |
| PY |
| ok "Template copied into $BUCKET" |
| else |
| ok "$BUCKET already populated ($FILE_COUNT files) β skipping template copy" |
| fi |
|
|
| |
| |
| |
| if ! command -v hf-mount >/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)" |
|
|
| |
| |
| |
| 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 |
|
|
| |
| |
| |
| 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)" |
|
|
| |
| |
| |
| say "Setting up shadow HERMES_HOME at $HOME_DIR..." |
| mkdir -p "$HOME_DIR" "$HOME_DIR/audio_cache" "$HOME_DIR/image_cache" "$HOME_DIR/logs" |
|
|
| |
| for d in memories cron hooks pairing sessions; do |
| mkdir -p "$MOUNT/$d" |
| done |
|
|
| |
| |
| |
| for f in config.yaml SOUL.md README.md .env.example; do |
| ln -sfn "$MOUNT/$f" "$HOME_DIR/$f" |
| done |
| |
| 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)" |
|
|
| |
| 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 |
|
|
| |
| |
| |
| 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 |
|
|
| |
| |
| |
| umask 077 |
| cat > "$SECRETS" <<EOF |
| # Hermes secrets β local-only ($HOME_DIR is HERMES_HOME, not a mount) |
| HF_TOKEN=$HF_TOKEN_VAL |
| TELEGRAM_BOT_TOKEN=$TG_TOKEN |
| TELEGRAM_ALLOWED_USERS=$USER_ID |
| TELEGRAM_HOME_CHANNEL=$USER_ID |
| EOF |
| chmod 600 "$SECRETS" |
| ok "Secrets saved to $SECRETS (mode 600)" |
|
|
| |
| |
| |
| |
| |
| |
| |
| HERMES_BIN="$HOME/.local/bin/hermes" |
| if [ -f "$HERMES_BIN" ] && ! grep -q "HERMES_HOME:=" "$HERMES_BIN"; then |
| python3 - "$HERMES_BIN" "$HOME_DIR" <<'PY' |
| import sys, pathlib |
| bin_path, home_dir = sys.argv[1], sys.argv[2] |
| p = pathlib.Path(bin_path) |
| lines = p.read_text().splitlines(keepends=True) |
| |
| out = [] |
| injected = False |
| for line in lines: |
| if not injected and line.lstrip().startswith("exec "): |
| out.append(f': "${{HERMES_HOME:={home_dir}}}"\n') |
| out.append("export HERMES_HOME\n") |
| injected = True |
| out.append(line) |
| p.write_text("".join(out)) |
| PY |
| ok "Patched $HERMES_BIN so 'hermes' defaults to HERMES_HOME=$HOME_DIR" |
| else |
| ok "$HERMES_BIN already patched (or not found)" |
| fi |
|
|
| |
| SHELL_NAME="${SHELL##*/}" |
| case "$SHELL_NAME" in |
| zsh) SHELL_RC="$HOME/.zshrc" ;; |
| bash) SHELL_RC="$HOME/.bashrc" ;; |
| *) SHELL_RC="$HOME/.profile" ;; |
| esac |
| if [ -f "$SHELL_RC" ] && grep -q "# hermes-bucket" "$SHELL_RC"; then |
| python3 - "$SHELL_RC" <<'PY' |
| import sys, pathlib, re |
| p = pathlib.Path(sys.argv[1]) |
| text = p.read_text() |
| new = re.sub(r"\n*# hermes-bucket[^\n]*\n(?:[^\n]*\n)*?(?=\n|\Z)", "\n", text) |
| p.write_text(new) |
| PY |
| ok "Removed legacy hermes-bucket block from $SHELL_RC" |
| fi |
|
|
| |
| |
| |
| cat <<EOF |
| |
| ${G}β Ready.${N} |
| |
| Your bucket: https://huggingface.co/buckets/$BUCKET |
| Bucket mount: $MOUNT |
| HERMES_HOME: $HOME_DIR (secrets local; everything else symlinks to mount) |
| Model: Qwen/Qwen3.6-35B-A3B (HF Inference Providers β deepinfra) |
| |
| ${C}hermes${N} β chat in your terminal |
| ${C}hermes gateway run${N} β start Telegram bot in the foreground (Ctrl-C to stop) |
| ${C}hermes gateway install${N} && ${C}hermes gateway start${N} β run gateway as a background service |
| |
| These work in any shell β no sourcing, no aliases. The hermes launcher |
| itself now defaults HERMES_HOME to $HOME_DIR. |
| |
| EOF |
|
|
| if [ -t 0 ] && [ -t 1 ]; then |
| say "Launching Hermes now..." |
| sleep 1 |
| export HERMES_HOME="$HOME_DIR" |
| exec hermes |
| else |
| say "Run ${C}source $SHELL_RC${N} then ${C}hermes${N} to start chatting." |
| fi |
|
|