CoolFace
Apppublic

hsbatakurki/HuggingMes

sourceHugging Facemitupdated 4mo agoView on Hugging Face
0likes
start.sh811 linesDownload Raw Back to root
1#!/bin/bash2set -euo pipefail3 4umask 00775 6# ════════════════════════════════════════════════════════════════7# HuggingMes — Hermes Gateway for HF Spaces8# ════════════════════════════════════════════════════════════════9 10# ── Startup Banner ──11APP_DIR="${HUGGINGMES_APP_DIR:-/opt/huggingmes}"12HERMES_HOME="${HERMES_HOME:-/opt/data}"13PUBLIC_PORT="${PORT:-7861}"14GATEWAY_API_PORT="${API_SERVER_PORT:-8642}"15DASHBOARD_PORT="${DASHBOARD_PORT:-9119}"16TELEGRAM_WEBHOOK_PORT="${TELEGRAM_WEBHOOK_PORT:-8765}"17SYNC_INTERVAL="${SYNC_INTERVAL:-600}"18BACKUP_DATASET="${BACKUP_DATASET_NAME:-huggingmes-backup}"19CF_PROXY_ENV_FILE="/tmp/huggingmes-cloudflare-proxy.env"20STARTUP_FILE="$HERMES_HOME/workspace/startup.sh"21 22export HERMES_HOME23export API_SERVER_ENABLED="${API_SERVER_ENABLED:-true}"24export API_SERVER_HOST="${API_SERVER_HOST:-127.0.0.1}"25export API_SERVER_PORT="$GATEWAY_API_PORT"26export GATEWAY_HEALTH_URL="${GATEWAY_HEALTH_URL:-http://127.0.0.1:${GATEWAY_API_PORT}}"27export TELEGRAM_WEBHOOK_PORT28 29echo ""30echo "  ╔══════════════════════════════════════════╗"31echo "  ║        🪽 HuggingMes Hermes Gateway      ║"32echo "  ╚══════════════════════════════════════════╝"33echo ""34 35if [ -z "${API_SERVER_KEY:-}" ]; then36  if [ -n "${GATEWAY_TOKEN:-}" ]; then37    export API_SERVER_KEY="$GATEWAY_TOKEN"38  else39    API_SERVER_KEY="$(python3 - <<'PY'40import secrets41print(secrets.token_urlsafe(32))42PY43)"44    export API_SERVER_KEY45    echo "GATEWAY_TOKEN not set - generated an ephemeral API token for this boot."46  fi47fi48 49# ── Setup directories ──50mkdir -p "$HERMES_HOME"/{cron,sessions,logs,hooks,memories,skills,skins,plans,workspace,home,plugins}51 52# Expose hermes CLI in ~/.local/bin so login shells (terminal backend) find it.53# Base image PATH includes /opt/data/.local/bin but hermes lives in the venv.54mkdir -p "$HERMES_HOME/.local/bin"55ln -sfn /opt/hermes/.venv/bin/hermes "$HERMES_HOME/.local/bin/hermes"56 57# Redirect Hermes plugin dir into volume so plugins survive container restarts58if [ ! -L "${HOME}/.hermes/plugins" ]; then59  mkdir -p "${HOME}/.hermes"60  rm -rf "${HOME}/.hermes/plugins"61  ln -sfn "$HERMES_HOME/plugins" "${HOME}/.hermes/plugins"62fi63 64# ── Restore workspace/state from HF Dataset ──65if [ -n "${HF_TOKEN:-}" ]; then66  echo "Restoring Hermes state from HF Dataset..."67  python3 "$APP_DIR/hermes-sync.py" restore || true68else69  echo "HF_TOKEN not set - dataset persistence is disabled."70fi71 72CLOUDFLARE_WORKERS_TOKEN="${CLOUDFLARE_WORKERS_TOKEN:-${CLOUDFLARE_API_TOKEN:-}}"73export CLOUDFLARE_WORKERS_TOKEN74if [ -n "${CLOUDFLARE_WORKERS_TOKEN:-}" ] || [ -n "${CLOUDFLARE_PROXY_URL:-}" ]; then75  export CLOUDFLARE_PROXY_DEBUG="${CLOUDFLARE_PROXY_DEBUG:-false}"76  echo "Preparing Cloudflare Telegram proxy..."77  python3 "$APP_DIR/cloudflare-proxy-setup.py" || true78  if [ -f "$CF_PROXY_ENV_FILE" ]; then79    . "$CF_PROXY_ENV_FILE"80  fi81fi82 83if [ -n "${CLOUDFLARE_WORKERS_TOKEN:-}" ]; then84  echo "Preparing Cloudflare Keepalive worker..."85  python3 "$APP_DIR/cloudflare-keepalive-setup.py" || true86fi87 88if [ -n "${TELEGRAM_USER_IDS:-}" ] && [ -z "${TELEGRAM_ALLOWED_USERS:-}" ]; then89  export TELEGRAM_ALLOWED_USERS="$TELEGRAM_USER_IDS"90elif [ -n "${TELEGRAM_USER_ID:-}" ] && [ -z "${TELEGRAM_ALLOWED_USERS:-}" ]; then91  export TELEGRAM_ALLOWED_USERS="$TELEGRAM_USER_ID"92fi93 94if [ -n "${TELEGRAM_BOT_TOKEN:-}" ] && [ -n "${SPACE_HOST:-}" ] && [ -z "${TELEGRAM_WEBHOOK_URL:-}" ]; then95  if [ "${TELEGRAM_MODE:-webhook}" != "polling" ]; then96    export TELEGRAM_WEBHOOK_URL="https://${SPACE_HOST}/telegram"97  fi98fi99 100if [ -n "${TELEGRAM_WEBHOOK_URL:-}" ] && [ -z "${TELEGRAM_WEBHOOK_SECRET:-}" ]; then101  SECRET_FILE="$HERMES_HOME/.huggingmes-telegram-webhook-secret"102  if [ -f "$SECRET_FILE" ]; then103    export TELEGRAM_WEBHOOK_SECRET104    TELEGRAM_WEBHOOK_SECRET="$(cat "$SECRET_FILE")"105  else106    TELEGRAM_WEBHOOK_SECRET="$(python3 - <<'PY'107import secrets108print(secrets.token_hex(32))109PY110)"111    printf '%s' "$TELEGRAM_WEBHOOK_SECRET" > "$SECRET_FILE"112    chmod 600 "$SECRET_FILE"113    export TELEGRAM_WEBHOOK_SECRET114  fi115fi116 117MODEL_INPUT="${HERMES_MODEL:-${LLM_MODEL:-}}"118MODEL_FOR_CONFIG="$MODEL_INPUT"119PROVIDER_FOR_CONFIG="${HERMES_INFERENCE_PROVIDER:-auto}"120LLM_API_KEY="${LLM_API_KEY:-}"121 122if [ -n "$MODEL_INPUT" ]; then123  MODEL_PREFIX="${MODEL_INPUT%%/*}"124else125  MODEL_PREFIX=""126fi127 128case "$MODEL_PREFIX" in129  openrouter)130    [ -n "$LLM_API_KEY" ] && export OPENROUTER_API_KEY="${OPENROUTER_API_KEY:-$LLM_API_KEY}"131    [ "$PROVIDER_FOR_CONFIG" = "auto" ] && PROVIDER_FOR_CONFIG="openrouter"132    MODEL_FOR_CONFIG="${MODEL_INPUT#openrouter/}"133    ;;134  huggingface|hf)135    [ -n "$LLM_API_KEY" ] && export HF_TOKEN="${HF_TOKEN:-$LLM_API_KEY}"136    [ "$PROVIDER_FOR_CONFIG" = "auto" ] && PROVIDER_FOR_CONFIG="huggingface"137    MODEL_FOR_CONFIG="${MODEL_INPUT#huggingface/}"138    ;;139  vercel-ai-gateway|ai-gateway)140    [ -n "$LLM_API_KEY" ] && export AI_GATEWAY_API_KEY="${AI_GATEWAY_API_KEY:-$LLM_API_KEY}"141    [ "$PROVIDER_FOR_CONFIG" = "auto" ] && PROVIDER_FOR_CONFIG="ai-gateway"142    MODEL_FOR_CONFIG="${MODEL_INPUT#*/}"143    ;;144  anthropic)145    [ -n "$LLM_API_KEY" ] && export ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY:-$LLM_API_KEY}"146    ;;147  openai|openai-codex)148    [ -n "$LLM_API_KEY" ] && export OPENAI_API_KEY="${OPENAI_API_KEY:-$LLM_API_KEY}"149    ;;150  google|gemini)151    [ -n "$LLM_API_KEY" ] && export GOOGLE_API_KEY="${GOOGLE_API_KEY:-$LLM_API_KEY}" GEMINI_API_KEY="${GEMINI_API_KEY:-$LLM_API_KEY}"152    PROVIDER_FOR_CONFIG="gemini"153    MODEL_FOR_CONFIG="${MODEL_INPUT#*/}"   # strip "google/" or "gemini/" prefix — Hermes gemini provider needs bare model name154    ;;155  deepseek)156    [ -n "$LLM_API_KEY" ] && export DEEPSEEK_API_KEY="${DEEPSEEK_API_KEY:-$LLM_API_KEY}"157    ;;158  kimi-coding|moonshot)159    [ -n "$LLM_API_KEY" ] && export KIMI_API_KEY="${KIMI_API_KEY:-$LLM_API_KEY}"160    ;;161  kimi-coding-cn|moonshot-cn|kimi-cn)162    [ -n "$LLM_API_KEY" ] && export KIMI_CN_API_KEY="${KIMI_CN_API_KEY:-$LLM_API_KEY}"163    ;;164  minimax)165    [ -n "$LLM_API_KEY" ] && export MINIMAX_API_KEY="${MINIMAX_API_KEY:-$LLM_API_KEY}"166    ;;167  minimax-cn)168    [ -n "$LLM_API_KEY" ] && export MINIMAX_CN_API_KEY="${MINIMAX_CN_API_KEY:-$LLM_API_KEY}"169    ;;170  xiaomi)171    [ -n "$LLM_API_KEY" ] && export XIAOMI_API_KEY="${XIAOMI_API_KEY:-$LLM_API_KEY}"172    ;;173  zai|z-ai|z.ai|glm)174    [ -n "$LLM_API_KEY" ] && export GLM_API_KEY="${GLM_API_KEY:-$LLM_API_KEY}"175    ;;176  arcee|arcee-ai|arceeai)177    [ -n "$LLM_API_KEY" ] && export ARCEEAI_API_KEY="${ARCEEAI_API_KEY:-$LLM_API_KEY}"178    ;;179  gmi|gmi-cloud|gmicloud)180    [ -n "$LLM_API_KEY" ] && export GMI_API_KEY="${GMI_API_KEY:-$LLM_API_KEY}"181    ;;182  alibaba)183    [ -n "$LLM_API_KEY" ] && export DASHSCOPE_API_KEY="${DASHSCOPE_API_KEY:-$LLM_API_KEY}"184    ;;185  alibaba-coding-plan|alibaba_coding)186    [ -n "$LLM_API_KEY" ] && export DASHSCOPE_API_KEY="${DASHSCOPE_API_KEY:-$LLM_API_KEY}"187    ;;188  tencent-tokenhub|tencent|tokenhub|tencentmaas)189    [ -n "$LLM_API_KEY" ] && export TOKENHUB_API_KEY="${TOKENHUB_API_KEY:-$LLM_API_KEY}"190    ;;191  nvidia)192    [ -n "$LLM_API_KEY" ] && export NVIDIA_API_KEY="${NVIDIA_API_KEY:-$LLM_API_KEY}"193    ;;194  xai|grok)195    [ -n "$LLM_API_KEY" ] && export XAI_API_KEY="${XAI_API_KEY:-$LLM_API_KEY}"196    ;;197  kilocode)198    [ -n "$LLM_API_KEY" ] && export KILOCODE_API_KEY="${KILOCODE_API_KEY:-$LLM_API_KEY}"199    ;;200  opencode-zen)201    [ -n "$LLM_API_KEY" ] && export OPENCODE_ZEN_API_KEY="${OPENCODE_ZEN_API_KEY:-$LLM_API_KEY}"202    ;;203  opencode-go)204    [ -n "$LLM_API_KEY" ] && export OPENCODE_GO_API_KEY="${OPENCODE_GO_API_KEY:-$LLM_API_KEY}"205    ;;206esac207 208if [ -n "${CUSTOM_BASE_URL:-}" ]; then209  PROVIDER_FOR_CONFIG="${CUSTOM_PROVIDER:-custom}"210  [ -n "$LLM_API_KEY" ] && export OPENAI_API_KEY="${OPENAI_API_KEY:-$LLM_API_KEY}"211fi212 213export MODEL_FOR_CONFIG PROVIDER_FOR_CONFIG214export CUSTOM_BASE_URL="${CUSTOM_BASE_URL:-}"215export CUSTOM_API_KEY="${CUSTOM_API_KEY:-${LLM_API_KEY:-}}"216export CUSTOM_MODEL_CONTEXT_LENGTH="${CUSTOM_MODEL_CONTEXT_LENGTH:-131072}"217export CUSTOM_MODEL_MAX_TOKENS="${CUSTOM_MODEL_MAX_TOKENS:-8192}"218export TELEGRAM_BASE_URL="${TELEGRAM_BASE_URL:-}"219export TELEGRAM_BASE_FILE_URL="${TELEGRAM_BASE_FILE_URL:-}"220 221if [ -n "${CLOUDFLARE_PROXY_URL:-}" ] && [ -z "$TELEGRAM_BASE_URL" ]; then222  CLOUDFLARE_PROXY_URL="${CLOUDFLARE_PROXY_URL%/}"223  export TELEGRAM_BASE_URL="${CLOUDFLARE_PROXY_URL}/bot"224  export TELEGRAM_BASE_FILE_URL="${CLOUDFLARE_PROXY_URL}/file/bot"225fi226 227# ── Pool key promotion ──228# Mirror first key from comma-separated pool vars into the singular env var.229# Hermes providers read singular vars; this lets users supply pool keys like230# ANTHROPIC_API_KEYS=key1,key2 and have them picked up automatically.231promote_first_pool_key() {232  local singular_var="$1"233  local pool_var="$2"234  local singular_val="${!singular_var:-}"235  local pool_val="${!pool_var:-}"236  [ -n "$singular_val" ] && return 0237  [ -n "$pool_val" ] || return 0238  local first239  first=$(printf '%s' "$pool_val" | tr ',' '\n' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' | awk 'NF{print; exit}')240  [ -n "$first" ] || return 0241  export "${singular_var}=$first"242}243 244promote_first_pool_key "OPENROUTER_API_KEY"   "OPENROUTER_API_KEYS"245promote_first_pool_key "ANTHROPIC_API_KEY"    "ANTHROPIC_API_KEYS"246promote_first_pool_key "OPENAI_API_KEY"       "OPENAI_API_KEYS"247promote_first_pool_key "GOOGLE_API_KEY"       "GOOGLE_API_KEYS"248promote_first_pool_key "GEMINI_API_KEY"       "GEMINI_API_KEYS"249promote_first_pool_key "DEEPSEEK_API_KEY"     "DEEPSEEK_API_KEYS"250promote_first_pool_key "KIMI_API_KEY"         "KIMI_API_KEYS"251promote_first_pool_key "MINIMAX_API_KEY"      "MINIMAX_API_KEYS"252promote_first_pool_key "NVIDIA_API_KEY"       "NVIDIA_API_KEYS"253promote_first_pool_key "XAI_API_KEY"          "XAI_API_KEYS"254promote_first_pool_key "KILOCODE_API_KEY"     "KILOCODE_API_KEYS"255promote_first_pool_key "GLM_API_KEY"          "GLM_API_KEYS"256promote_first_pool_key "ARCEEAI_API_KEY"      "ARCEEAI_API_KEYS"257promote_first_pool_key "DASHSCOPE_API_KEY"    "DASHSCOPE_API_KEYS"258promote_first_pool_key "GMI_API_KEY"          "GMI_API_KEYS"259promote_first_pool_key "TOKENHUB_API_KEY"     "TOKENHUB_API_KEYS"260 261# ── Build config ──262python3 - <<'PY'263import os264from pathlib import Path265 266import yaml267 268home = Path(os.environ["HERMES_HOME"])269path = home / "config.yaml"270try:271    config = yaml.safe_load(path.read_text(encoding="utf-8")) or {}272except FileNotFoundError:273    config = {}274 275model_name = os.environ.get("MODEL_FOR_CONFIG", "").strip()276provider_name = os.environ.get("PROVIDER_FOR_CONFIG", "").strip()277 278if model_name:279    model = config.setdefault("model", {})280    model["default"] = model_name                          # always from env — deploy-time setting281    if provider_name and provider_name != "auto":282        model["provider"] = provider_name                  # explicit provider (openrouter, huggingface, custom…)283    else:284        model.pop("provider", None)                        # let Hermes infer from model-name prefix285else:286    model = config.get("model", {})287    print("No LLM_MODEL/HERMES_MODEL set; leaving Hermes model config unchanged.")288 289custom_base = os.environ.get("CUSTOM_BASE_URL", "").strip()290if custom_base and model_name:291    model.setdefault("base_url", custom_base.rstrip("/"))292    if os.environ.get("CUSTOM_API_KEY"):293        model.setdefault("api_key", os.environ["CUSTOM_API_KEY"])294    try:295        model.setdefault("context_length", int(os.environ.get("CUSTOM_MODEL_CONTEXT_LENGTH", "131072")))296        model.setdefault("max_tokens", int(os.environ.get("CUSTOM_MODEL_MAX_TOKENS", "8192")))297    except ValueError:298        pass299 300config.setdefault("terminal", {}).setdefault("cwd", os.environ.get("MESSAGING_CWD", str(home / "workspace")))301config.setdefault("compression", {}).setdefault("enabled", True)302config.setdefault("display", {}).setdefault("background_process_notifications", os.environ.get("HERMES_BACKGROUND_NOTIFICATIONS", "result"))303config.setdefault("security", {}).setdefault("redact_secrets", True)304 305platforms = config.setdefault("platforms", {})306 307if os.environ.get("TELEGRAM_BOT_TOKEN"):308    telegram = platforms.setdefault("telegram", {})309    telegram.setdefault("enabled", True)310    extra = telegram.setdefault("extra", {})311    if os.environ.get("TELEGRAM_BASE_URL"):312        extra.setdefault("base_url", os.environ["TELEGRAM_BASE_URL"])313        extra.setdefault("base_file_url", os.environ.get("TELEGRAM_BASE_FILE_URL") or os.environ["TELEGRAM_BASE_URL"])314    if os.environ.get("TELEGRAM_ALLOWED_USERS"):315        config.setdefault("telegram", {}).setdefault("allow_from", [316            item.strip()317            for item in os.environ["TELEGRAM_ALLOWED_USERS"].split(",")318            if item.strip()319        ])320 321path.write_text(yaml.safe_dump(config, sort_keys=False), encoding="utf-8")322path.chmod(0o600)323PY324 325# ── Startup Summary ──326HERMES_RUNTIME_VERSION="$(/opt/hermes/.venv/bin/hermes --version 2>/dev/null | awk '{print $NF; exit}' || true)"327echo ""328if [ -n "${HERMES_RUNTIME_VERSION:-}" ]; then329  echo "Version   : ${HERMES_RUNTIME_VERSION}"330fi331echo "Model     : ${MODEL_FOR_CONFIG:-unset}"332echo "Provider  : ${PROVIDER_FOR_CONFIG:-unset}"333if [ -n "${TELEGRAM_BOT_TOKEN:-}" ]; then334  if [ -n "${TELEGRAM_WEBHOOK_URL:-}" ]; then335    echo "Telegram  : webhook"336  else337    echo "Telegram  : polling"338  fi339else340  echo "Telegram  : not configured"341fi342if [ -n "${HF_TOKEN:-}" ]; then343  echo "Backup    : ${BACKUP_DATASET} (every ${SYNC_INTERVAL:-600}s)"344else345  echo "Backup    : disabled"346fi347if [ -n "${CLOUDFLARE_PROXY_URL:-}" ]; then348  echo "Proxy     : ${CLOUDFLARE_PROXY_URL}"349fi350echo "Routes    : /app/ (Hermes UI), /terminal/ (JupyterLab)"351echo "Dashboard : http://127.0.0.1:${DASHBOARD_PORT}"352echo "Gateway   : http://127.0.0.1:${GATEWAY_API_PORT}"353echo ""354 355# ── JupyterLab terminal (on by default when GATEWAY_TOKEN is set) ──356JUPYTER_PID=""357start_jupyter() {358  if [ "${DEV_MODE:-true}" = "false" ]; then359    echo "JupyterLab disabled (DEV_MODE=false)."360    return 0361  fi362  # Guard: skip if already running363  if [ -n "${JUPYTER_PID:-}" ] && kill -0 "$JUPYTER_PID" 2>/dev/null; then364    return 0365  fi366  local token="${JUPYTER_TOKEN:-${API_SERVER_KEY:-}}"367  if [ -z "$token" ]; then368    echo "WARNING: No GATEWAY_TOKEN or JUPYTER_TOKEN set — JupyterLab skipped (terminal would be unauthenticated)." >&2369    return 0370  fi371  export JUPYTER_TOKEN="$token"372  local VENV_PYTHON="/opt/hermes/.venv/bin/python"373  if ! "$VENV_PYTHON" -c "import jupyterlab" >/dev/null 2>&1; then374    echo "WARNING: jupyterlab not installed in venv; skipping terminal." >&2375    return 0376  fi377  local root_dir="${JUPYTER_ROOT_DIR:-$HERMES_HOME/workspace}"378  mkdir -p "$root_dir"379  ln -sfn "$HERMES_HOME" "$root_dir/HuggingMes" 2>/dev/null || true380  echo "Starting JupyterLab terminal on port 8888 (root: $root_dir)"381  "$VENV_PYTHON" -m jupyterlab \382    --ip 127.0.0.1 \383    --port 8888 \384    --no-browser \385    --IdentityProvider.token="$JUPYTER_TOKEN" \386    --ServerApp.base_url=/terminal/ \387    --ServerApp.terminals_enabled=True \388    --ServerApp.terminado_settings='{"shell_command":["/bin/bash","-i"]}' \389    --ServerApp.allow_origin='*' \390    --ServerApp.allow_remote_access=True \391    --ServerApp.trust_xheaders=True \392    --ServerApp.tornado_settings="{'headers': {'Content-Security-Policy': 'frame-ancestors *'}}" \393    --IdentityProvider.cookie_options="{'SameSite': 'None', 'Secure': True}" \394    --ServerApp.disable_check_xsrf=True \395    --LabApp.news_url=None \396    --LabApp.check_for_updates_class=jupyterlab.NeverCheckForUpdate \397    --ServerApp.log_level=WARN \398    --ServerApp.root_dir="$root_dir" \399    >> "$HERMES_HOME/logs/jupyter.log" 2>&1 &400  JUPYTER_PID=$!401  export JUPYTER_PID402  echo "JupyterLab started (PID: $JUPYTER_PID)"403}404 405# ── Trap SIGTERM for graceful shutdown ──406SYNC_LOOP_PID=""407DASHBOARD_PID=""408graceful_shutdown() {409  echo "Shutting down HuggingMes..."410  if [ -n "${HF_TOKEN:-}" ]; then411    python3 "$APP_DIR/hermes-sync.py" sync-once || echo "Warning: shutdown sync failed."412  fi413  kill $(jobs -p) 2>/dev/null || true414  exit 0415}416trap graceful_shutdown SIGTERM SIGINT417 418# ── Shell capture wrappers ──419# Written to ~/.bashrc so terminal installs are recorded in workspace/startup.sh420# and replayed on next boot — packages survive Space restarts.421if [ ! -f "$STARTUP_FILE" ]; then422  touch "$STARTUP_FILE"423  chmod +x "$STARTUP_FILE"424  echo "Created workspace/startup.sh"425fi426cat > "$HOME/.bashrc" << 'BASHRC'427export PATH="/opt/hermes/.venv/bin:/opt/data/.local/bin:$PATH"428export DEBIAN_FRONTEND="${DEBIAN_FRONTEND:-noninteractive}"429if [ -z "${PS1:-}" ] || [ "$PS1" = "$ " ]; then430  export PS1="\u@\h:\w\$ "431fi432 433HERMES_HOME="${HERMES_HOME:-/opt/data}"434STARTUP_FILE="$HERMES_HOME/workspace/startup.sh"435 436_hm_append() {437  [ "${HUGGINGMES_CAPTURE_DISABLE:-0}" = "1" ] && return 0438  local line="$*"439  mkdir -p "$(dirname "$STARTUP_FILE")"440  touch "$STARTUP_FILE"441  chmod +x "$STARTUP_FILE" 2>/dev/null || true442  grep -qxF "$line" "$STARTUP_FILE" 2>/dev/null || echo "$line" >> "$STARTUP_FILE"443}444_hm_quote_args() {445  local quoted=()446  local arg447  for arg in "$@"; do448    printf -v arg '%q' "$arg"449    quoted+=("$arg")450  done451  printf '%s' "${quoted[*]}"452}453_hm_append_cmd() {454  local cmd="$1"455  shift456  local args457  args=$(_hm_quote_args "$@")458  if [ -n "$args" ]; then459    _hm_append "$cmd $args"460  else461    _hm_append "$cmd"462  fi463}464_hm_args_without_flags() {465  local out=()466  for arg in "$@"; do467    case "$arg" in468      ''|-|--*|-*) ;;469      *) out+=("$arg") ;;470    esac471  done472  printf '%s\n' "${out[@]}"473}474_hm_has_install_targets() {475  local item476  while IFS= read -r item; do477    [ -n "$item" ] && return 0478  done <<EOF479$(_hm_args_without_flags "$@")480EOF481  return 1482}483_hm_has_arg() {484  local needle="$1"485  shift486  for arg in "$@"; do487    [ "$arg" = "$needle" ] && return 0488  done489  return 1490}491_hm_can_sudo_apt() {492  command -v sudo >/dev/null 2>&1 && sudo -n apt-get --version >/dev/null 2>&1493}494_hm_apt_install() {495  if [ "$(id -u)" -eq 0 ]; then496    command apt-get update && command apt-get install -y "$@"497  elif _hm_can_sudo_apt; then498    sudo apt-get update && sudo apt-get install -y "$@"499  else500    echo "Error: apt install needs root." >&2501    return 1502  fi503}504apt-get() {505  case "${1:-}" in506    install)507      shift508      _hm_apt_install "$@"509      local rc=$?510      if [ $rc -eq 0 ]; then511        _hm_has_install_targets "$@" && _hm_append_cmd "sudo apt-get update && sudo apt-get install -y" "$@"512      fi513      return $rc514      ;;515    update)516      if [ "$(id -u)" -eq 0 ]; then command apt-get "$@"517      elif _hm_can_sudo_apt; then sudo apt-get "$@"518      else command apt-get "$@"; fi519      return $?520      ;;521    *) command apt-get "$@"; return $? ;;522  esac523}524apt() {525  case "${1:-}" in526    install)527      shift528      _hm_apt_install "$@"529      local rc=$?530      if [ $rc -eq 0 ]; then531        _hm_has_install_targets "$@" && _hm_append_cmd "sudo apt-get update && sudo apt-get install -y" "$@"532      fi533      return $rc534      ;;535    update)536      if [ "$(id -u)" -eq 0 ]; then command apt "$@"537      elif _hm_can_sudo_apt; then sudo apt "$@"538      else command apt "$@"; fi539      return $?540      ;;541    *) command apt "$@"; return $? ;;542  esac543}544pip() {545  command pip "$@"546  local rc=$?547  if [ $rc -eq 0 ] && [ "${1:-}" = "install" ] \548      && ! _hm_has_arg -r "${@:2}" && ! _hm_has_arg --requirement "${@:2}" \549      && _hm_has_install_targets "${@:2}"; then550    _hm_append_cmd "pip install" "${@:2}"551  fi552  return $rc553}554pip3() {555  command pip3 "$@"556  local rc=$?557  if [ $rc -eq 0 ] && [ "${1:-}" = "install" ] \558      && ! _hm_has_arg -r "${@:2}" && ! _hm_has_arg --requirement "${@:2}" \559      && _hm_has_install_targets "${@:2}"; then560    _hm_append_cmd "pip install" "${@:2}"561  fi562  return $rc563}564uv() {565  command uv "$@"566  local rc=$?567  if [ $rc -eq 0 ] && [ "${1:-}" = "pip" ] && [ "${2:-}" = "install" ] \568      && ! _hm_has_arg -r "${@:3}" && ! _hm_has_arg --requirements "${@:3}" \569      && _hm_has_install_targets "${@:3}"; then570    _hm_append_cmd "uv pip install" "${@:3}"571  fi572  return $rc573}574npm() {575  command npm "$@"576  local rc=$?577  if [ $rc -eq 0 ] && { [ "${1:-}" = "install" ] || [ "${1:-}" = "i" ]; } && { [ "${2:-}" = "-g" ] || [ "${2:-}" = "--global" ]; } && _hm_has_install_targets "${@:3}"; then578    _hm_append_cmd "npm install -g" "${@:3}"579  fi580  return $rc581}582hermes() {583  command hermes "$@"584  local rc=$?585  if [ $rc -eq 0 ] && [ "${1:-}" = "plugins" ] && [ "${2:-}" = "install" ] && _hm_has_install_targets "${@:3}"; then586    _hm_append_cmd "hermes plugins install" "${@:3}"587  fi588  return $rc589}590BASHRC591cat > "$HOME/.profile" << 'PROFILE'592[ -n "${BASH_VERSION:-}" ] && [ -f ~/.bashrc ] && . ~/.bashrc593PROFILE594echo "Shell capture wrappers ready."595 596# ── Optional package installs from HF Variables/Secrets ──597HM_STARTUP_FAILURES=0598 599if [ -n "${HUGGINGMES_APT_PACKAGES:-}" ]; then600  echo "Installing apt packages from HUGGINGMES_APT_PACKAGES..."601  read -r -a HM_APT_PACKAGES <<< "$HUGGINGMES_APT_PACKAGES"602  if command -v sudo >/dev/null 2>&1; then603    if sudo apt-get update && sudo apt-get install -y "${HM_APT_PACKAGES[@]}"; then604      echo "HUGGINGMES_APT_PACKAGES install complete."605    else606      HM_STARTUP_FAILURES=$((HM_STARTUP_FAILURES + 1))607      echo "ERROR: HUGGINGMES_APT_PACKAGES install failed: ${HUGGINGMES_APT_PACKAGES}" >&2608    fi609  elif [ "$(id -u)" -eq 0 ]; then610    if apt-get update && apt-get install -y "${HM_APT_PACKAGES[@]}"; then611      echo "HUGGINGMES_APT_PACKAGES install complete."612    else613      HM_STARTUP_FAILURES=$((HM_STARTUP_FAILURES + 1))614      echo "ERROR: HUGGINGMES_APT_PACKAGES install failed: ${HUGGINGMES_APT_PACKAGES}" >&2615    fi616  else617    HM_STARTUP_FAILURES=$((HM_STARTUP_FAILURES + 1))618    echo "ERROR: root/sudo unavailable; HUGGINGMES_APT_PACKAGES skipped" >&2619  fi620fi621 622if [ -n "${HUGGINGMES_PIP_PACKAGES:-}" ]; then623  echo "Installing Python packages from HUGGINGMES_PIP_PACKAGES..."624  read -r -a HM_PIP_PACKAGES <<< "$HUGGINGMES_PIP_PACKAGES"625  if /opt/hermes/.venv/bin/pip install "${HM_PIP_PACKAGES[@]}"; then626    echo "HUGGINGMES_PIP_PACKAGES install complete."627  else628    HM_STARTUP_FAILURES=$((HM_STARTUP_FAILURES + 1))629    echo "ERROR: HUGGINGMES_PIP_PACKAGES install failed: ${HUGGINGMES_PIP_PACKAGES}" >&2630  fi631fi632 633if [ -n "${HUGGINGMES_NPM_PACKAGES:-}" ]; then634  echo "Installing npm packages from HUGGINGMES_NPM_PACKAGES..."635  read -r -a HM_NPM_PACKAGES <<< "$HUGGINGMES_NPM_PACKAGES"636  if npm install -g "${HM_NPM_PACKAGES[@]}"; then637    echo "HUGGINGMES_NPM_PACKAGES install complete."638  else639    HM_STARTUP_FAILURES=$((HM_STARTUP_FAILURES + 1))640    echo "ERROR: HUGGINGMES_NPM_PACKAGES install failed: ${HUGGINGMES_NPM_PACKAGES}" >&2641  fi642fi643 644# ── Arbitrary startup script (HUGGINGMES_RUN) ──645# Supports plain bash or base64-encoded scripts (prefix with base64: or b64:).646# Example: HUGGINGMES_RUN="pip install pandas && npm install -g typescript"647# Example: HUGGINGMES_RUN="base64:$(base64 -w0 setup.sh)"648hm_run_startup_auto() {649  local payload="$1"650  [ -n "$payload" ] || return 0651  local script_file652  script_file=$(mktemp "/tmp/huggingmes-startup.XXXXXX.sh")653  {654    echo 'export HUGGINGMES_CAPTURE_DISABLE=1'655    echo '[ -f ~/.bashrc ] && . ~/.bashrc'656    if [[ "$payload" == base64:* ]] || [[ "$payload" == b64:* ]]; then657      printf '%s' "${payload#*:}" | base64 -d658    else659      printf '%s\n' "$payload"660    fi661  } > "$script_file"662  chmod 700 "$script_file"663  echo "[startup:HUGGINGMES_RUN] running script"664  set +e665  bash "$script_file"666  local rc=$?667  set -e668  rm -f "$script_file"669  if [ $rc -eq 0 ]; then670    echo "[startup:HUGGINGMES_RUN] ok"671  else672    HM_STARTUP_FAILURES=$((HM_STARTUP_FAILURES + 1))673    echo "ERROR: HUGGINGMES_RUN script failed (exit ${rc})" >&2674  fi675}676 677if [ -n "${HUGGINGMES_RUN:-}" ]; then678  hm_run_startup_auto "$HUGGINGMES_RUN"679fi680 681# ── Run workspace startup script ──682# Replays install commands recorded by the shell wrappers from previous sessions.683if [ -s "$STARTUP_FILE" ]; then684  echo "Running workspace/startup.sh..."685  set +e686  HUGGINGMES_CAPTURE_DISABLE=1 bash -l "$STARTUP_FILE"687  set -e688  echo "Workspace startup script complete."689fi690 691if [ "$HM_STARTUP_FAILURES" -gt 0 ]; then692  echo "Warning: ${HM_STARTUP_FAILURES} startup step(s) failed. Check logs above." >&2693fi694 695# ── Start background services ──696node "$APP_DIR/health-server.js" &697HEALTH_PID=$!698 699if [ -n "${WEBHOOK_URL:-}" ]; then700  python3 - <<'PY' >/dev/null 2>&1 &701import json, os, urllib.request702body = json.dumps({703    "event": "restart",704    "status": "success",705    "message": "HuggingMes Hermes gateway has started.",706    "model": os.environ.get("MODEL_FOR_CONFIG", ""),707}).encode()708req = urllib.request.Request(os.environ["WEBHOOK_URL"], data=body, method="POST", headers={"Content-Type": "application/json"})709urllib.request.urlopen(req, timeout=10).read()710PY711fi712 713# ── Launch dashboard once (restarts if it dies) ──714start_dashboard_once() {715  if [ -n "${DASHBOARD_PID:-}" ] && kill -0 "$DASHBOARD_PID" 2>/dev/null; then716    return 0717  fi718  echo "Launching Hermes dashboard on 127.0.0.1:${DASHBOARD_PORT}..."719  (hermes dashboard --host 127.0.0.1 --insecure 2>&1 | tee -a "$HERMES_HOME/logs/dashboard.log") &720  DASHBOARD_PID=$!721}722 723# ── Start sync loop once — survives gateway restarts ──724start_background_sync_once() {725  [ -n "${HF_TOKEN:-}" ] || return 0726  if [ -n "${SYNC_LOOP_PID:-}" ] && kill -0 "$SYNC_LOOP_PID" 2>/dev/null; then727    return 0728  fi729  python3 -u "$APP_DIR/hermes-sync.py" loop &730  SYNC_LOOP_PID=$!731}732 733start_dashboard_once734start_jupyter735 736# ── Gateway restart loop ──737GATEWAY_RESTART_DELAY="${GATEWAY_RESTART_DELAY:-5}"738GATEWAY_MAX_RESTARTS="${GATEWAY_MAX_RESTARTS:-0}"739GATEWAY_RESTART_COUNT=0740GATEWAY_READY_TIMEOUT="${GATEWAY_READY_TIMEOUT:-120}"741 742while true; do743  # Monitor health-server — restart if it died unexpectedly744  if [ -n "${HEALTH_PID:-}" ] && ! kill -0 "$HEALTH_PID" 2>/dev/null; then745    echo "Warning: health-server exited (PID $HEALTH_PID dead); restarting..."746    node "$APP_DIR/health-server.js" &747    HEALTH_PID=$!748    echo "Health server restarted (PID: $HEALTH_PID)"749  fi750 751  # Monitor Hermes dashboard — restart if it died unexpectedly752  if [ -n "${DASHBOARD_PID:-}" ] && ! kill -0 "$DASHBOARD_PID" 2>/dev/null; then753    echo "Warning: Hermes dashboard exited; restarting..."754    start_dashboard_once755  fi756 757  # Monitor JupyterLab — restart if it died unexpectedly758  if [ "${DEV_MODE:-true}" != "false" ] && [ -n "${JUPYTER_PID:-}" ] && ! kill -0 "$JUPYTER_PID" 2>/dev/null; then759    echo "Warning: JupyterLab exited (PID $JUPYTER_PID dead); restarting..."760    unset JUPYTER_PID761    start_jupyter762  fi763 764  echo "Launching Hermes gateway..."765  (hermes gateway run 2>&1 | tee -a "$HERMES_HOME/logs/gateway.log") &766  GATEWAY_PID=$!767 768  ready=false769  for ((i=0; i<GATEWAY_READY_TIMEOUT; i++)); do770    if (echo > "/dev/tcp/127.0.0.1/${GATEWAY_API_PORT}") 2>/dev/null; then771      ready=true772      break773    fi774    if ! kill -0 "$GATEWAY_PID" 2>/dev/null; then775      break776    fi777    sleep 1778  done779 780  if [ "$ready" != "true" ]; then781    echo ""782    echo "Hermes gateway failed to expose the API health port. Last 40 log lines:"783    echo "----------------------------------------"784    tail -40 "$HERMES_HOME/logs/gateway.log" || true785    exit 1786  fi787 788  # Start sync loop (only once — shared across all gateway restarts)789  start_background_sync_once790 791  set +e792  wait "$GATEWAY_PID"793  GATEWAY_EXIT_CODE=$?794  set -e795 796  # Sync state before restart797  if [ -n "${HF_TOKEN:-}" ]; then798    echo "Gateway exited — syncing state before restart..."799    python3 "$APP_DIR/hermes-sync.py" sync-once || echo "Warning: sync failed."800  fi801 802  GATEWAY_RESTART_COUNT=$((GATEWAY_RESTART_COUNT + 1))803  if [ "$GATEWAY_MAX_RESTARTS" != "0" ] && [ "$GATEWAY_RESTART_COUNT" -ge "$GATEWAY_MAX_RESTARTS" ]; then804    echo "Gateway exited (code ${GATEWAY_EXIT_CODE}); restart limit (${GATEWAY_MAX_RESTARTS}) reached."805    exit "$GATEWAY_EXIT_CODE"806  fi807 808  echo "Gateway exited (code ${GATEWAY_EXIT_CODE}); restarting in ${GATEWAY_RESTART_DELAY}s..."809  sleep "$GATEWAY_RESTART_DELAY"810done811