File size: 11,522 Bytes
43c6693
 
 
 
7308820
 
8e01a03
 
 
 
 
 
 
 
 
 
 
 
43c6693
 
 
8e01a03
43c6693
 
 
7308820
43c6693
8e01a03
 
 
43c6693
96dae5d
43c6693
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7308820
 
43c6693
 
 
 
7308820
 
 
 
 
 
 
 
675dd28
 
 
7308820
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43c6693
7308820
43c6693
 
 
 
 
 
 
 
7308820
43c6693
 
 
 
 
 
 
 
 
 
 
8e01a03
43c6693
 
 
 
 
 
 
 
 
8e01a03
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43c6693
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dcaca40
 
 
 
 
 
 
 
 
 
43c6693
 
 
8e01a03
43c6693
 
 
8e01a03
43c6693
 
 
 
 
 
 
 
 
164e59e
43c6693
164e59e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43c6693
 
 
 
 
 
9f020c3
8e01a03
9f020c3
8e01a03
 
9f020c3
 
8e01a03
164e59e
8e01a03
 
43c6693
8e01a03
43c6693
 
 
 
 
8e01a03
 
 
 
43c6693
9f020c3
 
 
 
164e59e
 
43c6693
 
 
 
 
 
8e01a03
43c6693
 
9f020c3
43c6693
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
#!/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/tty; }

export PATH="$HOME/.local/bin:$PATH"

printf "\n${C}βš• Hermes Agent β€” bucket bootstrap${N}\n\n"

# ----------------------------------------------------------------------------
# 1. hf CLI
# ----------------------------------------------------------------------------
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)"

# ----------------------------------------------------------------------------
# 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 - <<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

# ----------------------------------------------------------------------------
# 4. hf-mount
# ----------------------------------------------------------------------------
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)"

# ----------------------------------------------------------------------------
# 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" <<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)"

# ----------------------------------------------------------------------------
# 10. Patch the hermes entrypoint so HERMES_HOME defaults to the shadow dir
# ----------------------------------------------------------------------------
# Shell-rc exports require users to source/relaunch. Patching the launcher at
# ~/.local/bin/hermes makes plain `hermes` and `hermes gateway` Just Work in
# *any* shell, immediately, no sourcing. If a user explicitly sets HERMES_HOME
# (e.g. for a profile), that wins via :=.
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)
# Find the exec line and inject defaults right before it
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

# Clean up any prior shell-rc block from older bootstrap versions
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

# ----------------------------------------------------------------------------
# 11. Summary + launch
# ----------------------------------------------------------------------------
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