#!/bin/bash set -euo pipefail umask 0077 APP_DIR="${HERMES_HF_APP_DIR:-/opt/hermes-hf}" HERMES_HOME="${HERMES_HOME:-/opt/data}" PUBLIC_PORT="${PORT:-7861}" GATEWAY_API_PORT="${API_SERVER_PORT:-8642}" DASHBOARD_PORT="${DASHBOARD_PORT:-9119}" TELEGRAM_WEBHOOK_PORT="${TELEGRAM_WEBHOOK_PORT:-8765}" SYNC_INTERVAL="${SYNC_INTERVAL:-600}" BACKUP_DATASET="${BACKUP_DATASET_NAME:-hermes-hf-backup}" CF_PROXY_ENV_FILE="/tmp/hermes-hf-cloudflare-proxy.env" STARTUP_FILE="$HERMES_HOME/workspace/startup.sh" export HERMES_HOME export API_SERVER_ENABLED="${API_SERVER_ENABLED:-true}" export API_SERVER_HOST="${API_SERVER_HOST:-127.0.0.1}" export API_SERVER_PORT="$GATEWAY_API_PORT" export GATEWAY_HEALTH_URL="${GATEWAY_HEALTH_URL:-http://127.0.0.1:${GATEWAY_API_PORT}}" export TELEGRAM_WEBHOOK_PORT echo "" echo " ╔══════════════════════════════════════════╗" echo " ║ Hermes Agent on Hugging Face Spaces ║" echo " ╚══════════════════════════════════════════╝" echo "" if [ -z "${API_SERVER_KEY:-}" ]; then if [ -n "${GATEWAY_TOKEN:-}" ]; then export API_SERVER_KEY="$GATEWAY_TOKEN" else API_SERVER_KEY="$(python3 - <<'PY' import secrets print(secrets.token_urlsafe(32)) PY )" export API_SERVER_KEY echo "GATEWAY_TOKEN not set - generated ephemeral API token for this boot." fi fi mkdir -p "$HERMES_HOME"/{cron,sessions,logs,hooks,memories,skills,skins,plans,workspace,home,plugins} mkdir -p "$HERMES_HOME/.local/bin" ln -sfn /opt/hermes/.venv/bin/hermes "$HERMES_HOME/.local/bin/hermes" if [ ! -L "${HOME}/.hermes/plugins" ]; then mkdir -p "${HOME}/.hermes" rm -rf "${HOME}/.hermes/plugins" ln -sfn "$HERMES_HOME/plugins" "${HOME}/.hermes/plugins" fi if [ -n "${HF_TOKEN:-}" ]; then echo "Restoring Hermes state from HF Dataset..." python3 "$APP_DIR/hermes-sync.py" restore || true else echo "HF_TOKEN not set - dataset persistence is disabled." fi CLOUDFLARE_WORKERS_TOKEN="${CLOUDFLARE_WORKERS_TOKEN:-${CLOUDFLARE_API_TOKEN:-}}" export CLOUDFLARE_WORKERS_TOKEN if [ -n "${CLOUDFLARE_WORKERS_TOKEN:-}" ] || [ -n "${CLOUDFLARE_PROXY_URL:-}" ]; then export CLOUDFLARE_PROXY_DEBUG="${CLOUDFLARE_PROXY_DEBUG:-false}" fi if [ -n "${TELEGRAM_USER_IDS:-}" ] && [ -z "${TELEGRAM_ALLOWED_USERS:-}" ]; then export TELEGRAM_ALLOWED_USERS="$TELEGRAM_USER_IDS" elif [ -n "${TELEGRAM_USER_ID:-}" ] && [ -z "${TELEGRAM_ALLOWED_USERS:-}" ]; then export TELEGRAM_ALLOWED_USERS="$TELEGRAM_USER_ID" fi if [ -n "${TELEGRAM_BOT_TOKEN:-}" ] && [ -n "${SPACE_HOST:-}" ] && [ -z "${TELEGRAM_WEBHOOK_URL:-}" ]; then if [ "${TELEGRAM_MODE:-webhook}" != "polling" ]; then export TELEGRAM_WEBHOOK_URL="https://${SPACE_HOST}/telegram" fi fi if [ -n "${TELEGRAM_WEBHOOK_URL:-}" ] && [ -z "${TELEGRAM_WEBHOOK_SECRET:-}" ]; then SECRET_FILE="$HERMES_HOME/.hermes-hf-telegram-webhook-secret" if [ -f "$SECRET_FILE" ]; then export TELEGRAM_WEBHOOK_SECRET TELEGRAM_WEBHOOK_SECRET="$(cat "$SECRET_FILE")" else TELEGRAM_WEBHOOK_SECRET="$(python3 - <<'PY' import secrets print(secrets.token_hex(32)) PY )" printf '%s' "$TELEGRAM_WEBHOOK_SECRET" > "$SECRET_FILE" chmod 600 "$SECRET_FILE" export TELEGRAM_WEBHOOK_SECRET fi fi MODEL_INPUT="${HERMES_MODEL:-${LLM_MODEL:-}}" MODEL_FOR_CONFIG="$MODEL_INPUT" PROVIDER_FOR_CONFIG="${HERMES_INFERENCE_PROVIDER:-auto}" LLM_API_KEY="${LLM_API_KEY:-}" if [ -n "$MODEL_INPUT" ]; then MODEL_PREFIX="${MODEL_INPUT%%/*}" else MODEL_PREFIX="" fi case "$MODEL_PREFIX" in openrouter) [ -n "$LLM_API_KEY" ] && export OPENROUTER_API_KEY="${OPENROUTER_API_KEY:-$LLM_API_KEY}" [ "$PROVIDER_FOR_CONFIG" = "auto" ] && PROVIDER_FOR_CONFIG="openrouter" MODEL_FOR_CONFIG="${MODEL_INPUT#openrouter/}" ;; huggingface|hf) [ -n "$LLM_API_KEY" ] && export HF_TOKEN="${HF_TOKEN:-$LLM_API_KEY}" [ "$PROVIDER_FOR_CONFIG" = "auto" ] && PROVIDER_FOR_CONFIG="huggingface" MODEL_FOR_CONFIG="${MODEL_INPUT#huggingface/}" ;; anthropic) [ -n "$LLM_API_KEY" ] && export ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY:-$LLM_API_KEY}" ;; openai|openai-codex) [ -n "$LLM_API_KEY" ] && export OPENAI_API_KEY="${OPENAI_API_KEY:-$LLM_API_KEY}" ;; google|gemini) [ -n "$LLM_API_KEY" ] && export GOOGLE_API_KEY="${GOOGLE_API_KEY:-$LLM_API_KEY}" GEMINI_API_KEY="${GEMINI_API_KEY:-$LLM_API_KEY}" PROVIDER_FOR_CONFIG="gemini" MODEL_FOR_CONFIG="${MODEL_INPUT#*/}" ;; deepseek) [ -n "$LLM_API_KEY" ] && export DEEPSEEK_API_KEY="${DEEPSEEK_API_KEY:-$LLM_API_KEY}" ;; xai|grok) [ -n "$LLM_API_KEY" ] && export XAI_API_KEY="${XAI_API_KEY:-$LLM_API_KEY}" ;; opencode-zen) [ -n "$LLM_API_KEY" ] && export OPENCODE_ZEN_API_KEY="${OPENCODE_ZEN_API_KEY:-$LLM_API_KEY}" ;; opencode-go) [ -n "$LLM_API_KEY" ] && export OPENCODE_GO_API_KEY="${OPENCODE_GO_API_KEY:-$LLM_API_KEY}" ;; esac if [ -n "${CUSTOM_BASE_URL:-}" ]; then PROVIDER_FOR_CONFIG="${CUSTOM_PROVIDER:-custom}" [ -n "$LLM_API_KEY" ] && export OPENAI_API_KEY="${OPENAI_API_KEY:-$LLM_API_KEY}" fi export MODEL_FOR_CONFIG PROVIDER_FOR_CONFIG export CUSTOM_BASE_URL="${CUSTOM_BASE_URL:-}" export CUSTOM_API_KEY="${CUSTOM_API_KEY:-${LLM_API_KEY:-}}" export CUSTOM_MODEL_CONTEXT_LENGTH="${CUSTOM_MODEL_CONTEXT_LENGTH:-131072}" export CUSTOM_MODEL_MAX_TOKENS="${CUSTOM_MODEL_MAX_TOKENS:-8192}" export TELEGRAM_BASE_URL="${TELEGRAM_BASE_URL:-}" export TELEGRAM_BASE_FILE_URL="${TELEGRAM_BASE_FILE_URL:-}" if [ -n "${CLOUDFLARE_PROXY_URL:-}" ] && [ -z "$TELEGRAM_BASE_URL" ]; then CLOUDFLARE_PROXY_URL="${CLOUDFLARE_PROXY_URL%/}" export TELEGRAM_BASE_URL="${CLOUDFLARE_PROXY_URL}/bot" export TELEGRAM_BASE_FILE_URL="${CLOUDFLARE_PROXY_URL}/file/bot" fi promote_first_pool_key() { local singular_var="$1" local pool_var="$2" local singular_val="${!singular_var:-}" local pool_val="${!pool_var:-}" [ -n "$singular_val" ] && return 0 [ -n "$pool_val" ] || return 0 local first first=$(printf '%s' "$pool_val" | tr ',' '\n' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' | awk 'NF{print; exit}') [ -n "$first" ] || return 0 export "${singular_var}=$first" } promote_first_pool_key "OPENROUTER_API_KEY" "OPENROUTER_API_KEYS" promote_first_pool_key "ANTHROPIC_API_KEY" "ANTHROPIC_API_KEYS" promote_first_pool_key "OPENAI_API_KEY" "OPENAI_API_KEYS" promote_first_pool_key "GOOGLE_API_KEY" "GOOGLE_API_KEYS" promote_first_pool_key "GEMINI_API_KEY" "GEMINI_API_KEYS" promote_first_pool_key "DEEPSEEK_API_KEY" "DEEPSEEK_API_KEYS" promote_first_pool_key "XAI_API_KEY" "XAI_API_KEYS" python3 - <<'PY' import os from pathlib import Path import yaml home = Path(os.environ["HERMES_HOME"]) path = home / "config.yaml" try: config = yaml.safe_load(path.read_text(encoding="utf-8")) or {} except FileNotFoundError: config = {} model_name = os.environ.get("MODEL_FOR_CONFIG", "").strip() provider_name = os.environ.get("PROVIDER_FOR_CONFIG", "").strip() if model_name: model = config.setdefault("model", {}) model["default"] = model_name if provider_name and provider_name != "auto": model["provider"] = provider_name else: model.pop("provider", None) custom_base = os.environ.get("CUSTOM_BASE_URL", "").strip() if custom_base and model_name: model.setdefault("base_url", custom_base.rstrip("/")) if os.environ.get("CUSTOM_API_KEY"): model.setdefault("api_key", os.environ["CUSTOM_API_KEY"]) try: model.setdefault("context_length", int(os.environ.get("CUSTOM_MODEL_CONTEXT_LENGTH", "131072"))) model.setdefault("max_tokens", int(os.environ.get("CUSTOM_MODEL_MAX_TOKENS", "8192"))) except ValueError: pass config.setdefault("terminal", {}).setdefault("cwd", os.environ.get("MESSAGING_CWD", str(home / "workspace"))) config.setdefault("compression", {}).setdefault("enabled", True) config.setdefault("display", {}).setdefault("background_process_notifications", os.environ.get("HERMES_BACKGROUND_NOTIFICATIONS", "result")) config.setdefault("security", {}).setdefault("redact_secrets", True) platforms = config.setdefault("platforms", {}) if os.environ.get("TELEGRAM_BOT_TOKEN"): telegram = platforms.setdefault("telegram", {}) telegram.setdefault("enabled", True) extra = telegram.setdefault("extra", {}) if os.environ.get("TELEGRAM_BASE_URL"): extra.setdefault("base_url", os.environ["TELEGRAM_BASE_URL"]) extra.setdefault("base_file_url", os.environ.get("TELEGRAM_BASE_FILE_URL") or os.environ["TELEGRAM_BASE_URL"]) if os.environ.get("TELEGRAM_ALLOWED_USERS"): config.setdefault("telegram", {}).setdefault("allow_from", [ item.strip() for item in os.environ["TELEGRAM_ALLOWED_USERS"].split(",") if item.strip() ]) path.write_text(yaml.safe_dump(config, sort_keys=False), encoding="utf-8") path.chmod(0o600) PY HERMES_RUNTIME_VERSION="$(/opt/hermes/.venv/bin/hermes --version 2>/dev/null | awk '{print $NF; exit}' || true)" echo "" if [ -n "${HERMES_RUNTIME_VERSION:-}" ]; then echo "Version : ${HERMES_RUNTIME_VERSION}" fi echo "Model : ${MODEL_FOR_CONFIG:-unset}" echo "Provider : ${PROVIDER_FOR_CONFIG:-unset}" if [ -n "${TELEGRAM_BOT_TOKEN:-}" ]; then if [ -n "${TELEGRAM_WEBHOOK_URL:-}" ]; then echo "Telegram : webhook" else echo "Telegram : polling" fi else echo "Telegram : not configured" fi if [ -n "${HF_TOKEN:-}" ]; then echo "Backup : ${BACKUP_DATASET} (every ${SYNC_INTERVAL:-600}s)" else echo "Backup : disabled" fi if [ -n "${CLOUDFLARE_PROXY_URL:-}" ]; then echo "Proxy : ${CLOUDFLARE_PROXY_URL}" fi echo "Routes : /app/ (Hermes UI), /terminal/ (JupyterLab)" echo "Dashboard : http://127.0.0.1:${DASHBOARD_PORT}" echo "Gateway : http://127.0.0.1:${GATEWAY_API_PORT}" echo "" JUPYTER_PID="" start_jupyter() { if [ "${DEV_MODE:-true}" = "false" ]; then echo "JupyterLab disabled (DEV_MODE=false)." return 0 fi if [ -n "${JUPYTER_PID:-}" ] && kill -0 "$JUPYTER_PID" 2>/dev/null; then return 0 fi local token="${JUPYTER_TOKEN:-${API_SERVER_KEY:-}}" if [ -z "$token" ]; then echo "WARNING: No GATEWAY_TOKEN or JUPYTER_TOKEN set - JupyterLab skipped." >&2 return 0 fi export JUPYTER_TOKEN="$token" local VENV_PYTHON="/opt/hermes/.venv/bin/python" if ! "$VENV_PYTHON" -c "import jupyterlab" >/dev/null 2>&1; then echo "WARNING: jupyterlab not installed in venv; skipping terminal." >&2 return 0 fi local root_dir="${JUPYTER_ROOT_DIR:-$HERMES_HOME/workspace}" mkdir -p "$root_dir" ln -sfn "$HERMES_HOME" "$root_dir/hermes-hf" 2>/dev/null || true echo "Starting JupyterLab terminal on port 8888 (root: $root_dir)" "$VENV_PYTHON" -m jupyterlab \ --ip 127.0.0.1 \ --port 8888 \ --no-browser \ --IdentityProvider.token="$JUPYTER_TOKEN" \ --ServerApp.base_url=/terminal/ \ --ServerApp.terminals_enabled=True \ --ServerApp.terminado_settings='{"shell_command":["/bin/bash","-i"]}' \ --ServerApp.allow_origin='*' \ --ServerApp.allow_remote_access=True \ --ServerApp.trust_xheaders=True \ --ServerApp.tornado_settings="{'headers': {'Content-Security-Policy': 'frame-ancestors *'}}" \ --IdentityProvider.cookie_options="{'SameSite': 'None', 'Secure': True}" \ --ServerApp.disable_check_xsrf=True \ --LabApp.news_url=None \ --LabApp.check_for_updates_class=jupyterlab.NeverCheckForUpdate \ --ServerApp.log_level=WARN \ --ServerApp.root_dir="$root_dir" \ >> "$HERMES_HOME/logs/jupyter.log" 2>&1 & JUPYTER_PID=$! export JUPYTER_PID echo "JupyterLab started (PID: $JUPYTER_PID)" } SYNC_LOOP_PID="" DASHBOARD_PID="" graceful_shutdown() { echo "Shutting down Hermes-HF..." if [ -n "${HF_TOKEN:-}" ]; then python3 "$APP_DIR/hermes-sync.py" sync-once || echo "Warning: shutdown sync failed." fi kill $(jobs -p) 2>/dev/null || true exit 0 } trap graceful_shutdown SIGTERM SIGINT if [ ! -f "$STARTUP_FILE" ]; then touch "$STARTUP_FILE" chmod +x "$STARTUP_FILE" echo "Created workspace/startup.sh" fi cat > "$HOME/.bashrc" << 'BASHRC' export PATH="/opt/hermes/.venv/bin:/opt/data/.local/bin:$PATH" export DEBIAN_FRONTEND="${DEBIAN_FRONTEND:-noninteractive}" if [ -z "${PS1:-}" ] || [ "$PS1" = "$ " ]; then export PS1="\u@\h:\w\$ " fi HERMES_HOME="${HERMES_HOME:-/opt/data}" STARTUP_FILE="$HERMES_HOME/workspace/startup.sh" _hm_append() { [ "${HERMES_HF_CAPTURE_DISABLE:-0}" = "1" ] && return 0 local line="$*" mkdir -p "$(dirname "$STARTUP_FILE")" touch "$STARTUP_FILE" chmod +x "$STARTUP_FILE" 2>/dev/null || true grep -qxF "$line" "$STARTUP_FILE" 2>/dev/null || echo "$line" >> "$STARTUP_FILE" } _hm_quote_args() { local quoted=() local arg for arg in "$@"; do printf -v arg '%q' "$arg" quoted+=("$arg") done printf '%s' "${quoted[*]}" } _hm_append_cmd() { local cmd="$1" shift local args args=$(_hm_quote_args "$@") if [ -n "$args" ]; then _hm_append "$cmd $args" else _hm_append "$cmd" fi } _hm_args_without_flags() { local out=() for arg in "$@"; do case "$arg" in ''|-|--*|-*) ;; *) out+=("$arg") ;; esac done printf '%s\n' "${out[@]}" } _hm_has_install_targets() { local item while IFS= read -r item; do [ -n "$item" ] && return 0 done </dev/null 2>&1 && sudo -n apt-get --version >/dev/null 2>&1 } _hm_apt_install() { if [ "$(id -u)" -eq 0 ]; then command apt-get update && command apt-get install -y "$@" elif _hm_can_sudo_apt; then sudo apt-get update && sudo apt-get install -y "$@" else echo "Error: apt install needs root." >&2 return 1 fi } apt-get() { case "${1:-}" in install) shift _hm_apt_install "$@" local rc=$? if [ $rc -eq 0 ]; then _hm_has_install_targets "$@" && _hm_append_cmd "sudo apt-get update && sudo apt-get install -y" "$@" fi return $rc ;; update|*) command apt-get "$@"; return $? ;; esac } apt() { case "${1:-}" in install) shift _hm_apt_install "$@" local rc=$? if [ $rc -eq 0 ]; then _hm_has_install_targets "$@" && _hm_append_cmd "sudo apt-get update && sudo apt-get install -y" "$@" fi return $rc ;; update|*) command apt "$@"; return $? ;; esac } pip() { command pip "$@" local rc=$? if [ $rc -eq 0 ] && [ "${1:-}" = "install" ] \ && ! _hm_has_arg -r "${@:2}" && ! _hm_has_arg --requirement "${@:2}" \ && _hm_has_install_targets "${@:2}"; then _hm_append_cmd "pip install" "${@:2}" fi return $rc } pip3() { command pip3 "$@" local rc=$? if [ $rc -eq 0 ] && [ "${1:-}" = "install" ] \ && ! _hm_has_arg -r "${@:2}" && ! _hm_has_arg --requirement "${@:2}" \ && _hm_has_install_targets "${@:2}"; then _hm_append_cmd "pip install" "${@:2}" fi return $rc } npm() { command npm "$@" local rc=$? if [ $rc -eq 0 ] && { [ "${1:-}" = "install" ] || [ "${1:-}" = "i" ]; } && { [ "${2:-}" = "-g" ] || [ "${2:-}" = "--global" ]; } && _hm_has_install_targets "${@:3}"; then _hm_append_cmd "npm install -g" "${@:3}" fi return $rc } hermes() { command hermes "$@" local rc=$? if [ $rc -eq 0 ] && [ "${1:-}" = "plugins" ] && [ "${2:-}" = "install" ] && _hm_has_install_targets "${@:3}"; then _hm_append_cmd "hermes plugins install" "${@:3}" fi return $rc } BASHRC cat > "$HOME/.profile" << 'PROFILE' [ -n "${BASH_VERSION:-}" ] && [ -f ~/.bashrc ] && . ~/.bashrc PROFILE echo "Shell capture wrappers ready." HM_STARTUP_FAILURES=0 if [ -n "${HERMES_HF_APT_PACKAGES:-}" ]; then echo "Installing apt packages from HERMES_HF_APT_PACKAGES..." read -r -a HM_APT_PACKAGES <<< "$HERMES_HF_APT_PACKAGES" if command -v sudo >/dev/null 2>&1; then if sudo apt-get update && sudo apt-get install -y "${HM_APT_PACKAGES[@]}"; then echo "HERMES_HF_APT_PACKAGES install complete." else HM_STARTUP_FAILURES=$((HM_STARTUP_FAILURES + 1)) echo "ERROR: HERMES_HF_APT_PACKAGES install failed: ${HERMES_HF_APT_PACKAGES}" >&2 fi else HM_STARTUP_FAILURES=$((HM_STARTUP_FAILURES + 1)) echo "ERROR: root/sudo unavailable; HERMES_HF_APT_PACKAGES skipped" >&2 fi fi if [ -n "${HERMES_HF_PIP_PACKAGES:-}" ]; then echo "Installing Python packages from HERMES_HF_PIP_PACKAGES..." read -r -a HM_PIP_PACKAGES <<< "$HERMES_HF_PIP_PACKAGES" if /opt/hermes/.venv/bin/pip install "${HM_PIP_PACKAGES[@]}"; then echo "HERMES_HF_PIP_PACKAGES install complete." else HM_STARTUP_FAILURES=$((HM_STARTUP_FAILURES + 1)) echo "ERROR: HERMES_HF_PIP_PACKAGES install failed: ${HERMES_HF_PIP_PACKAGES}" >&2 fi fi if [ -n "${HERMES_HF_RUN:-}" ]; then local script_file script_file=$(mktemp "/tmp/hermes-hf-startup.XXXXXX.sh") { echo 'export HERMES_HF_CAPTURE_DISABLE=1' echo '[ -f ~/.bashrc ] && . ~/.bashrc' if [[ "$HERMES_HF_RUN" == base64:* ]] || [[ "$HERMES_HF_RUN" == b64:* ]]; then printf '%s' "${HERMES_HF_RUN#*:}" | base64 -d else printf '%s\n' "$HERMES_HF_RUN" fi } > "$script_file" chmod 700 "$script_file" echo "[startup:HERMES_HF_RUN] running script" set +e bash "$script_file" local rc=$? set -e rm -f "$script_file" if [ $rc -eq 0 ]; then echo "[startup:HERMES_HF_RUN] ok" else HM_STARTUP_FAILURES=$((HM_STARTUP_FAILURES + 1)) echo "ERROR: HERMES_HF_RUN script failed (exit ${rc})" >&2 fi fi if [ -s "$STARTUP_FILE" ]; then echo "Running workspace/startup.sh..." set +e HERMES_HF_CAPTURE_DISABLE=1 bash -l "$STARTUP_FILE" set -e echo "Workspace startup script complete." fi if [ "$HM_STARTUP_FAILURES" -gt 0 ]; then echo "Warning: ${HM_STARTUP_FAILURES} startup step(s) failed." >&2 fi node "$APP_DIR/health-server.js" & HEALTH_PID=$! if [ -n "${WEBHOOK_URL:-}" ]; then python3 - <<'PY' >/dev/null 2>&1 & import json, os, urllib.request body = json.dumps({ "event": "restart", "status": "success", "message": "Hermes-HF gateway has started.", "model": os.environ.get("MODEL_FOR_CONFIG", ""), }).encode() req = urllib.request.Request(os.environ["WEBHOOK_URL"], data=body, method="POST", headers={"Content-Type": "application/json"}) urllib.request.urlopen(req, timeout=10).read() PY fi start_dashboard_once() { if [ -n "${DASHBOARD_PID:-}" ] && kill -0 "$DASHBOARD_PID" 2>/dev/null; then return 0 fi echo "Launching Hermes dashboard on 127.0.0.1:${DASHBOARD_PORT}..." (hermes dashboard --host 127.0.0.1 --insecure 2>&1 | tee -a "$HERMES_HOME/logs/dashboard.log") & DASHBOARD_PID=$! } start_background_sync_once() { [ -n "${HF_TOKEN:-}" ] || return 0 if [ -n "${SYNC_LOOP_PID:-}" ] && kill -0 "$SYNC_LOOP_PID" 2>/dev/null; then return 0 fi python3 -u "$APP_DIR/hermes-sync.py" loop & SYNC_LOOP_PID=$! } start_dashboard_once start_jupyter GATEWAY_RESTART_DELAY="${GATEWAY_RESTART_DELAY:-5}" GATEWAY_MAX_RESTARTS="${GATEWAY_MAX_RESTARTS:-0}" GATEWAY_RESTART_COUNT=0 GATEWAY_READY_TIMEOUT="${GATEWAY_READY_TIMEOUT:-120}" while true; do if [ -n "${HEALTH_PID:-}" ] && ! kill -0 "$HEALTH_PID" 2>/dev/null; then echo "Warning: health-server exited; restarting..." node "$APP_DIR/health-server.js" & HEALTH_PID=$! fi if [ -n "${DASHBOARD_PID:-}" ] && ! kill -0 "$DASHBOARD_PID" 2>/dev/null; then echo "Warning: Hermes dashboard exited; restarting..." start_dashboard_once fi if [ "${DEV_MODE:-true}" != "false" ] && [ -n "${JUPYTER_PID:-}" ] && ! kill -0 "$JUPYTER_PID" 2>/dev/null; then echo "Warning: JupyterLab exited; restarting..." unset JUPYTER_PID start_jupyter fi echo "Waiting for Hermes gateway (managed by s6)..." ready=false for ((i=0; i "/dev/tcp/127.0.0.1/${GATEWAY_API_PORT}") 2>/dev/null; then ready=true break fi sleep 1 done if [ "$ready" != "true" ]; then echo "WARNING: Gateway API not ready after ${GATEWAY_READY_TIMEOUT}s (s6 may still be starting)" else echo "Gateway API ready on port ${GATEWAY_API_PORT}" fi start_background_sync_once echo "Hermes-HF running. Press Ctrl+C to stop." tail -f /dev/null & wait $! done