HuggingMes / start.sh
Viktor AI
feat: update Odysseus fallback model list to all 121 NVIDIA models
68bdcf5
Raw
History Blame Contribute Delete
39.2 kB
#!/bin/bash
set -euo pipefail
umask 0077
# ════════════════════════════════════════════════════════════════
# HuggingMes β€” Hermes Gateway for HF Spaces
# ════════════════════════════════════════════════════════════════
# ── Startup Banner ──
APP_DIR="${HUGGINGMES_APP_DIR:-/opt/huggingmes}"
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:-huggingmes-backup}"
CF_PROXY_ENV_FILE="/tmp/huggingmes-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 " β•‘ πŸͺ½ HuggingMes Hermes Gateway β•‘"
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 an ephemeral API token for this boot."
fi
fi
# ── Setup directories ──
mkdir -p "$HERMES_HOME"/{cron,sessions,logs,hooks,memories,skills,skins,plans,workspace,home,plugins}
# Expose hermes CLI in ~/.local/bin so login shells (terminal backend) find it.
# Base image PATH includes /opt/data/.local/bin but hermes lives in the venv.
mkdir -p "$HERMES_HOME/.local/bin"
ln -sfn /opt/hermes/.venv/bin/hermes "$HERMES_HOME/.local/bin/hermes"
# Redirect Hermes plugin dir into volume so plugins survive container restarts
if [ ! -L "${HOME}/.hermes/plugins" ]; then
mkdir -p "${HOME}/.hermes"
rm -rf "${HOME}/.hermes/plugins"
ln -sfn "$HERMES_HOME/plugins" "${HOME}/.hermes/plugins"
fi
# ── Odysseus AI Provider Plugin ──
if [ -n "${ODYSSEUS_API_KEY:-}" ]; then
ODYSSEUS_PLUGIN_DIR="$HERMES_HOME/plugins/model-providers/odysseus"
mkdir -p "$ODYSSEUS_PLUGIN_DIR"
ODYSSEUS_BASE="${ODYSSEUS_BASE_URL:-https://ayoub5550-odysseusai.hf.space/v1}"
cat > "$ODYSSEUS_PLUGIN_DIR/plugin.yaml" << 'PLUGINYAML'
name: odysseus
kind: model-provider
version: 1.0.0
description: Odysseus AI β€” intelligent inference with web search and memory
author: ayoub5550
PLUGINYAML
cat > "$ODYSSEUS_PLUGIN_DIR/__init__.py" << PLUGINPY
"""Odysseus AI provider profile.
Odysseus AI is an intelligent inference gateway that adds web search (SearXNG)
and memory (ChromaDB) to LLM requests. It exposes an OpenAI-compatible API
and proxies to upstream model providers (NVIDIA NIM, etc.).
"""
from providers import register_provider
from providers.base import ProviderProfile
odysseus = ProviderProfile(
name="odysseus",
aliases=("odysseus-ai", "odysseusai"),
env_vars=("ODYSSEUS_API_KEY",),
display_name="Odysseus AI",
description="Odysseus AI β€” inference with web search & memory",
base_url="${ODYSSEUS_BASE}",
default_max_tokens=8192,
supports_vision=False,
)
register_provider(odysseus)
PLUGINPY
echo "[odysseus] Provider plugin installed at $ODYSSEUS_PLUGIN_DIR"
fi
# ── Restore workspace/state from HF Dataset ──
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}"
echo "Preparing Cloudflare Telegram proxy..."
python3 "$APP_DIR/cloudflare-proxy-setup.py" || true
if [ -f "$CF_PROXY_ENV_FILE" ]; then
. "$CF_PROXY_ENV_FILE"
fi
fi
if [ -n "${CLOUDFLARE_WORKERS_TOKEN:-}" ]; then
echo "Preparing Cloudflare Keepalive worker..."
python3 "$APP_DIR/cloudflare-keepalive-setup.py" || true
fi
# ── Cloudflare Odysseus Bridge ──
if [ -n "${CLOUDFLARE_WORKERS_TOKEN:-}" ] && [ -n "${ODYSSEUS_API_KEY:-}" ]; then
echo "Deploying Cloudflare Odysseus bridge worker..."
python3 "$APP_DIR/cloudflare-odysseus-bridge.py" || true
ODYSSEUS_BRIDGE_ENV="/tmp/huggingmes-odysseus-bridge.env"
if [ -f "$ODYSSEUS_BRIDGE_ENV" ]; then
. "$ODYSSEUS_BRIDGE_ENV"
fi
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/.huggingmes-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/}"
;;
vercel-ai-gateway|ai-gateway)
[ -n "$LLM_API_KEY" ] && export AI_GATEWAY_API_KEY="${AI_GATEWAY_API_KEY:-$LLM_API_KEY}"
[ "$PROVIDER_FOR_CONFIG" = "auto" ] && PROVIDER_FOR_CONFIG="ai-gateway"
MODEL_FOR_CONFIG="${MODEL_INPUT#*/}"
;;
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#*/}" # strip "google/" or "gemini/" prefix β€” Hermes gemini provider needs bare model name
;;
deepseek)
[ -n "$LLM_API_KEY" ] && export DEEPSEEK_API_KEY="${DEEPSEEK_API_KEY:-$LLM_API_KEY}"
;;
kimi-coding|moonshot)
[ -n "$LLM_API_KEY" ] && export KIMI_API_KEY="${KIMI_API_KEY:-$LLM_API_KEY}"
;;
kimi-coding-cn|moonshot-cn|kimi-cn)
[ -n "$LLM_API_KEY" ] && export KIMI_CN_API_KEY="${KIMI_CN_API_KEY:-$LLM_API_KEY}"
;;
minimax)
[ -n "$LLM_API_KEY" ] && export MINIMAX_API_KEY="${MINIMAX_API_KEY:-$LLM_API_KEY}"
;;
minimax-cn)
[ -n "$LLM_API_KEY" ] && export MINIMAX_CN_API_KEY="${MINIMAX_CN_API_KEY:-$LLM_API_KEY}"
;;
xiaomi)
[ -n "$LLM_API_KEY" ] && export XIAOMI_API_KEY="${XIAOMI_API_KEY:-$LLM_API_KEY}"
;;
zai|z-ai|z.ai|glm)
[ -n "$LLM_API_KEY" ] && export GLM_API_KEY="${GLM_API_KEY:-$LLM_API_KEY}"
;;
arcee|arcee-ai|arceeai)
[ -n "$LLM_API_KEY" ] && export ARCEEAI_API_KEY="${ARCEEAI_API_KEY:-$LLM_API_KEY}"
;;
gmi|gmi-cloud|gmicloud)
[ -n "$LLM_API_KEY" ] && export GMI_API_KEY="${GMI_API_KEY:-$LLM_API_KEY}"
;;
alibaba)
[ -n "$LLM_API_KEY" ] && export DASHSCOPE_API_KEY="${DASHSCOPE_API_KEY:-$LLM_API_KEY}"
;;
alibaba-coding-plan|alibaba_coding)
[ -n "$LLM_API_KEY" ] && export DASHSCOPE_API_KEY="${DASHSCOPE_API_KEY:-$LLM_API_KEY}"
;;
tencent-tokenhub|tencent|tokenhub|tencentmaas)
[ -n "$LLM_API_KEY" ] && export TOKENHUB_API_KEY="${TOKENHUB_API_KEY:-$LLM_API_KEY}"
;;
nvidia)
[ -n "$LLM_API_KEY" ] && export NVIDIA_API_KEY="${NVIDIA_API_KEY:-$LLM_API_KEY}"
;;
xai|grok)
[ -n "$LLM_API_KEY" ] && export XAI_API_KEY="${XAI_API_KEY:-$LLM_API_KEY}"
;;
kilocode)
[ -n "$LLM_API_KEY" ] && export KILOCODE_API_KEY="${KILOCODE_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
# Only override default provider to custom when no explicit provider was requested
if [ "${HERMES_INFERENCE_PROVIDER:-auto}" = "auto" ]; then
PROVIDER_FOR_CONFIG="${CUSTOM_PROVIDER:-custom}"
fi
[ -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
# ── Pool key promotion ──
# Mirror first key from comma-separated pool vars into the singular env var.
# Hermes providers read singular vars; this lets users supply pool keys like
# ANTHROPIC_API_KEYS=key1,key2 and have them picked up automatically.
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 "KIMI_API_KEY" "KIMI_API_KEYS"
promote_first_pool_key "MINIMAX_API_KEY" "MINIMAX_API_KEYS"
promote_first_pool_key "NVIDIA_API_KEY" "NVIDIA_API_KEYS"
promote_first_pool_key "XAI_API_KEY" "XAI_API_KEYS"
promote_first_pool_key "KILOCODE_API_KEY" "KILOCODE_API_KEYS"
promote_first_pool_key "GLM_API_KEY" "GLM_API_KEYS"
promote_first_pool_key "ARCEEAI_API_KEY" "ARCEEAI_API_KEYS"
promote_first_pool_key "DASHSCOPE_API_KEY" "DASHSCOPE_API_KEYS"
promote_first_pool_key "GMI_API_KEY" "GMI_API_KEYS"
promote_first_pool_key "TOKENHUB_API_KEY" "TOKENHUB_API_KEYS"
# ── Build config ──
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 # always from env β€” deploy-time setting
if provider_name and provider_name != "auto":
model["provider"] = provider_name # explicit provider (openrouter, huggingface, custom…)
else:
model.pop("provider", None) # let Hermes infer from model-name prefix
else:
model = config.get("model", {})
print("No LLM_MODEL/HERMES_MODEL set; leaving Hermes model config unchanged.")
custom_base = os.environ.get("CUSTOM_BASE_URL", "").strip()
# Only inject custom base_url into model config when provider IS custom
# (otherwise it overrides the provider-specific URL for nvidia, openrouter, etc.)
if custom_base and model_name and provider_name in ("custom", ""):
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()
])
# ── Supermemory memory provider ──
sm_key = os.environ.get("SUPERMEMORY_API_KEY", "").strip()
if sm_key:
mem = config.setdefault("memory", {})
mem["provider"] = "supermemory"
print("Supermemory memory provider enabled.")
# ── Odysseus AI provider ──
odysseus_key = os.environ.get("ODYSSEUS_API_KEY", "").strip()
if odysseus_key:
odysseus_base = os.environ.get("ODYSSEUS_BRIDGE_URL",
os.environ.get("ODYSSEUS_BASE_URL", "https://ayoub5550-odysseusai.hf.space/v1")).strip().rstrip("/")
providers_cfg = config.setdefault("providers", {})
# Pre-populated model list as fallback; discover_models will refresh live
odysseus_models = {"01-ai/yi-large": {}, "abacusai/dracarys-llama-3.1-70b-instruct": {}, "adept/fuyu-8b": {}, "ai21labs/jamba-1.5-large-instruct": {}, "aisingapore/sea-lion-7b-instruct": {}, "baai/bge-m3": {}, "bigcode/starcoder2-15b": {}, "bytedance/seed-oss-36b-instruct": {}, "databricks/dbrx-instruct": {}, "deepseek-ai/deepseek-coder-6.7b-instruct": {}, "deepseek-ai/deepseek-v4-flash": {}, "deepseek-ai/deepseek-v4-pro": {}, "google/codegemma-1.1-7b": {}, "google/codegemma-7b": {}, "google/deplot": {}, "google/diffusiongemma-26b-a4b-it": {}, "google/gemma-2-2b-it": {}, "google/gemma-2b": {}, "google/gemma-3-12b-it": {}, "google/gemma-3-4b-it": {}, "google/gemma-3n-e2b-it": {}, "google/gemma-3n-e4b-it": {}, "google/gemma-4-31b-it": {}, "google/recurrentgemma-2b": {}, "ibm/granite-3.0-3b-a800m-instruct": {}, "ibm/granite-3.0-8b-instruct": {}, "ibm/granite-34b-code-instruct": {}, "ibm/granite-8b-code-instruct": {}, "meta/codellama-70b": {}, "meta/llama-3.1-70b-instruct": {}, "meta/llama-3.1-8b-instruct": {}, "meta/llama-3.2-11b-vision-instruct": {}, "meta/llama-3.2-1b-instruct": {}, "meta/llama-3.2-3b-instruct": {}, "meta/llama-3.2-90b-vision-instruct": {}, "meta/llama-3.3-70b-instruct": {}, "meta/llama-4-maverick-17b-128e-instruct": {}, "meta/llama-guard-4-12b": {}, "meta/llama2-70b": {}, "microsoft/kosmos-2": {}, "microsoft/phi-3-vision-128k-instruct": {}, "microsoft/phi-3.5-moe-instruct": {}, "microsoft/phi-4-mini-instruct": {}, "microsoft/phi-4-multimodal-instruct": {}, "minimaxai/minimax-m2.7": {}, "minimaxai/minimax-m3": {}, "mistralai/codestral-22b-instruct-v0.1": {}, "mistralai/ministral-14b-instruct-2512": {}, "mistralai/mistral-7b-instruct-v0.3": {}, "mistralai/mistral-large": {}, "mistralai/mistral-large-2-instruct": {}, "mistralai/mistral-large-3-675b-instruct-2512": {}, "mistralai/mistral-medium-3.5-128b": {}, "mistralai/mistral-nemotron": {}, "mistralai/mistral-small-4-119b-2603": {}, "mistralai/mixtral-8x22b-v0.1": {}, "mistralai/mixtral-8x7b-instruct-v0.1": {}, "moonshotai/kimi-k2.6": {}, "nv-mistralai/mistral-nemo-12b-instruct": {}, "nvidia/ai-synthetic-video-detector": {}, "nvidia/cosmos-reason2-8b": {}, "nvidia/embed-qa-4": {}, "nvidia/gliner-pii": {}, "nvidia/ising-calibration-1-35b-a3b": {}, "nvidia/llama-3.1-nemoguard-8b-content-safety": {}, "nvidia/llama-3.1-nemoguard-8b-topic-control": {}, "nvidia/llama-3.1-nemotron-51b-instruct": {}, "nvidia/llama-3.1-nemotron-70b-instruct": {}, "nvidia/llama-3.1-nemotron-nano-8b-v1": {}, "nvidia/llama-3.1-nemotron-nano-vl-8b-v1": {}, "nvidia/llama-3.1-nemotron-safety-guard-8b-v3": {}, "nvidia/llama-3.1-nemotron-ultra-253b-v1": {}, "nvidia/llama-3.2-nemoretriever-1b-vlm-embed-v1": {}, "nvidia/llama-3.2-nv-embedqa-1b-v1": {}, "nvidia/llama-3.3-nemotron-super-49b-v1": {}, "nvidia/llama-3.3-nemotron-super-49b-v1.5": {}, "nvidia/llama-nemotron-embed-1b-v2": {}, "nvidia/llama-nemotron-embed-vl-1b-v2": {}, "nvidia/llama3-chatqa-1.5-70b": {}, "nvidia/mistral-nemo-minitron-8b-8k-instruct": {}, "nvidia/nemoretriever-parse": {}, "nvidia/nemotron-3-content-safety": {}, "nvidia/nemotron-3-nano-30b-a3b": {}, "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning": {}, "nvidia/nemotron-3-super-120b-a12b": {}, "nvidia/nemotron-3-ultra-550b-a55b": {}, "nvidia/nemotron-3.5-content-safety": {}, "nvidia/nemotron-4-340b-instruct": {}, "nvidia/nemotron-4-340b-reward": {}, "nvidia/nemotron-content-safety-reasoning-4b": {}, "nvidia/nemotron-mini-4b-instruct": {}, "nvidia/nemotron-nano-12b-v2-vl": {}, "nvidia/nemotron-nano-3-30b-a3b": {}, "nvidia/nemotron-parse": {}, "nvidia/neva-22b": {}, "nvidia/nv-embed-v1": {}, "nvidia/nv-embedcode-7b-v1": {}, "nvidia/nv-embedqa-e5-v5": {}, "nvidia/nv-embedqa-mistral-7b-v2": {}, "nvidia/nvclip": {}, "nvidia/nvidia-nemotron-nano-9b-v2": {}, "nvidia/riva-translate-4b-instruct": {}, "nvidia/riva-translate-4b-instruct-v1.1": {}, "nvidia/vila": {}, "openai/gpt-oss-120b": {}, "openai/gpt-oss-20b": {}, "qwen/qwen3-next-80b-a3b-instruct": {}, "qwen/qwen3.5-122b-a10b": {}, "qwen/qwen3.5-397b-a17b": {}, "sarvamai/sarvam-m": {}, "snowflake/arctic-embed-l": {}, "stepfun-ai/step-3.5-flash": {}, "stepfun-ai/step-3.7-flash": {}, "stockmark/stockmark-2-100b-instruct": {}, "upstage/solar-10.7b-instruct": {}, "writer/palmyra-creative-122b": {}, "writer/palmyra-fin-70b-32k": {}, "writer/palmyra-med-70b": {}, "writer/palmyra-med-70b-32k": {}, "z-ai/glm-5.1": {}, "zyphra/zamba2-7b-instruct": {}}
providers_cfg["odysseus"] = {
"name": "Odysseus AI",
"base_url": odysseus_base,
"api_key": odysseus_key,
"key_env": "ODYSSEUS_API_KEY",
"models": odysseus_models,
"discover_models": True,
}
print(f"Odysseus AI provider configured with {len(odysseus_models)} fallback models.")
path.write_text(yaml.safe_dump(config, sort_keys=False), encoding="utf-8")
path.chmod(0o600)
# Write supermemory.json config if API key is present
if sm_key:
import json
sm_cfg_path = home / "supermemory.json"
try:
sm_cfg = json.loads(sm_cfg_path.read_text(encoding="utf-8"))
except (FileNotFoundError, json.JSONDecodeError):
sm_cfg = {}
sm_cfg.setdefault("container_tag", "hermes-{identity}")
sm_cfg.setdefault("enable_custom_container_tags", True)
sm_cfg.setdefault("custom_containers", ["work", "personal", "projects"])
sm_cfg.setdefault("custom_container_instructions",
"Use 'work' for job/coding context, 'personal' for life/hobbies, "
"'projects' for specific project knowledge.")
sm_cfg_path.write_text(json.dumps(sm_cfg, indent=2, ensure_ascii=False), encoding="utf-8")
sm_cfg_path.chmod(0o600)
print(f"Supermemory config written to {sm_cfg_path}")
PY
# ── Himalaya Email CLI Setup ──
if [ -n "${GMAIL_APP_PASSWORD:-}" ] && [ -n "${GMAIL_ADDRESS:-}" ]; then
echo "[himalaya] Setting up Gmail integration..."
HIMALAYA_BIN="$HOME/.local/bin/himalaya"
if [ ! -x "$HIMALAYA_BIN" ]; then
echo "[himalaya] Installing himalaya CLI..."
mkdir -p "$HOME/.local/bin"
curl -sSL https://raw.githubusercontent.com/pimalaya/himalaya/master/install.sh \
| PREFIX="$HOME/.local" sh 2>/dev/null || {
echo "[himalaya] WARNING: install failed, trying fallback..." >&2
# Fallback: download latest release directly
HIMALAYA_URL="https://github.com/pimalaya/himalaya/releases/latest/download/himalaya-x86_64-linux.tar.gz"
curl -sSL "$HIMALAYA_URL" | tar xz -C "$HOME/.local/bin/" 2>/dev/null || \
echo "[himalaya] ERROR: Could not install himalaya" >&2
}
fi
if [ -x "$HIMALAYA_BIN" ]; then
HIMALAYA_CFG="$HOME/.config/himalaya/config.toml"
mkdir -p "$(dirname "$HIMALAYA_CFG")"
cat > "$HIMALAYA_CFG" << HIMCFG
[accounts.gmail]
email = "${GMAIL_ADDRESS}"
display-name = "Hermes AI"
default = true
backend.type = "imap"
backend.host = "imap.gmail.com"
backend.port = 993
backend.encryption.type = "tls"
backend.login = "${GMAIL_ADDRESS}"
backend.auth.type = "password"
backend.auth.cmd = "printenv GMAIL_APP_PASSWORD"
message.send.backend.type = "smtp"
message.send.backend.host = "smtp.gmail.com"
message.send.backend.port = 587
message.send.backend.encryption.type = "start-tls"
message.send.backend.login = "${GMAIL_ADDRESS}"
message.send.backend.auth.type = "password"
message.send.backend.auth.cmd = "printenv GMAIL_APP_PASSWORD"
folder.aliases.inbox = "INBOX"
folder.aliases.sent = "[Gmail]/&BigGMQZKBi8- &BkUGMQYzBkQ-"
folder.aliases.drafts = "[Gmail]/&BkUGMwZIBi8GUQYnBio-"
folder.aliases.trash = "[Gmail]/&BicGRAZFBkcGQAZFBkQGJwYq-"
HIMCFG
chmod 600 "$HIMALAYA_CFG"
echo "[himalaya] Gmail configured for ${GMAIL_ADDRESS}"
fi
fi
# ── YouTube CLI Setup ──
if [ -n "${YOUTUBE_REFRESH_TOKEN:-}" ]; then
echo "[youtube] Setting up YouTube CLI tool..."
YT_TOOL="/usr/local/bin/youtube"
cp /app/youtube_tool.py "$YT_TOOL" 2>/dev/null || cp "$(dirname "$0")/youtube_tool.py" "$YT_TOOL" 2>/dev/null || true
if [ -f "$YT_TOOL" ]; then
chmod +x "$YT_TOOL"
echo "[youtube] YouTube CLI ready. Commands: youtube channel | list | upload | search | delete"
fi
fi
# ── Startup Summary ──
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 ""
# ── JupyterLab terminal (on by default when GATEWAY_TOKEN is set) ──
JUPYTER_PID=""
start_jupyter() {
if [ "${DEV_MODE:-true}" = "false" ]; then
echo "JupyterLab disabled (DEV_MODE=false)."
return 0
fi
# Guard: skip if already running
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 (terminal would be unauthenticated)." >&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/HuggingMes" 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)"
}
# ── Trap SIGTERM for graceful shutdown ──
SYNC_LOOP_PID=""
DASHBOARD_PID=""
graceful_shutdown() {
echo "Shutting down HuggingMes..."
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
# ── Shell capture wrappers ──
# Written to ~/.bashrc so terminal installs are recorded in workspace/startup.sh
# and replayed on next boot β€” packages survive Space restarts.
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() {
[ "${HUGGINGMES_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 <<EOF
$(_hm_args_without_flags "$@")
EOF
return 1
}
_hm_has_arg() {
local needle="$1"
shift
for arg in "$@"; do
[ "$arg" = "$needle" ] && return 0
done
return 1
}
_hm_can_sudo_apt() {
command -v sudo >/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)
if [ "$(id -u)" -eq 0 ]; then command apt-get "$@"
elif _hm_can_sudo_apt; then sudo apt-get "$@"
else command apt-get "$@"; fi
return $?
;;
*) 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)
if [ "$(id -u)" -eq 0 ]; then command apt "$@"
elif _hm_can_sudo_apt; then sudo apt "$@"
else command apt "$@"; fi
return $?
;;
*) 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
}
uv() {
command uv "$@"
local rc=$?
if [ $rc -eq 0 ] && [ "${1:-}" = "pip" ] && [ "${2:-}" = "install" ] \
&& ! _hm_has_arg -r "${@:3}" && ! _hm_has_arg --requirements "${@:3}" \
&& _hm_has_install_targets "${@:3}"; then
_hm_append_cmd "uv pip install" "${@:3}"
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."
# ── Optional package installs from HF Variables/Secrets ──
HM_STARTUP_FAILURES=0
if [ -n "${HUGGINGMES_APT_PACKAGES:-}" ]; then
echo "Installing apt packages from HUGGINGMES_APT_PACKAGES..."
read -r -a HM_APT_PACKAGES <<< "$HUGGINGMES_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 "HUGGINGMES_APT_PACKAGES install complete."
else
HM_STARTUP_FAILURES=$((HM_STARTUP_FAILURES + 1))
echo "ERROR: HUGGINGMES_APT_PACKAGES install failed: ${HUGGINGMES_APT_PACKAGES}" >&2
fi
elif [ "$(id -u)" -eq 0 ]; then
if apt-get update && apt-get install -y "${HM_APT_PACKAGES[@]}"; then
echo "HUGGINGMES_APT_PACKAGES install complete."
else
HM_STARTUP_FAILURES=$((HM_STARTUP_FAILURES + 1))
echo "ERROR: HUGGINGMES_APT_PACKAGES install failed: ${HUGGINGMES_APT_PACKAGES}" >&2
fi
else
HM_STARTUP_FAILURES=$((HM_STARTUP_FAILURES + 1))
echo "ERROR: root/sudo unavailable; HUGGINGMES_APT_PACKAGES skipped" >&2
fi
fi
if [ -n "${HUGGINGMES_PIP_PACKAGES:-}" ]; then
echo "Installing Python packages from HUGGINGMES_PIP_PACKAGES..."
read -r -a HM_PIP_PACKAGES <<< "$HUGGINGMES_PIP_PACKAGES"
if /opt/hermes/.venv/bin/pip install "${HM_PIP_PACKAGES[@]}"; then
echo "HUGGINGMES_PIP_PACKAGES install complete."
else
HM_STARTUP_FAILURES=$((HM_STARTUP_FAILURES + 1))
echo "ERROR: HUGGINGMES_PIP_PACKAGES install failed: ${HUGGINGMES_PIP_PACKAGES}" >&2
fi
fi
if [ -n "${HUGGINGMES_NPM_PACKAGES:-}" ]; then
echo "Installing npm packages from HUGGINGMES_NPM_PACKAGES..."
read -r -a HM_NPM_PACKAGES <<< "$HUGGINGMES_NPM_PACKAGES"
if npm install -g "${HM_NPM_PACKAGES[@]}"; then
echo "HUGGINGMES_NPM_PACKAGES install complete."
else
HM_STARTUP_FAILURES=$((HM_STARTUP_FAILURES + 1))
echo "ERROR: HUGGINGMES_NPM_PACKAGES install failed: ${HUGGINGMES_NPM_PACKAGES}" >&2
fi
fi
# ── Arbitrary startup script (HUGGINGMES_RUN) ──
# Supports plain bash or base64-encoded scripts (prefix with base64: or b64:).
# Example: HUGGINGMES_RUN="pip install pandas && npm install -g typescript"
# Example: HUGGINGMES_RUN="base64:$(base64 -w0 setup.sh)"
hm_run_startup_auto() {
local payload="$1"
[ -n "$payload" ] || return 0
local script_file
script_file=$(mktemp "/tmp/huggingmes-startup.XXXXXX.sh")
{
echo 'export HUGGINGMES_CAPTURE_DISABLE=1'
echo '[ -f ~/.bashrc ] && . ~/.bashrc'
if [[ "$payload" == base64:* ]] || [[ "$payload" == b64:* ]]; then
printf '%s' "${payload#*:}" | base64 -d
else
printf '%s\n' "$payload"
fi
} > "$script_file"
chmod 700 "$script_file"
echo "[startup:HUGGINGMES_RUN] running script"
set +e
bash "$script_file"
local rc=$?
set -e
rm -f "$script_file"
if [ $rc -eq 0 ]; then
echo "[startup:HUGGINGMES_RUN] ok"
else
HM_STARTUP_FAILURES=$((HM_STARTUP_FAILURES + 1))
echo "ERROR: HUGGINGMES_RUN script failed (exit ${rc})" >&2
fi
}
if [ -n "${HUGGINGMES_RUN:-}" ]; then
hm_run_startup_auto "$HUGGINGMES_RUN"
fi
# ── Run workspace startup script ──
# Replays install commands recorded by the shell wrappers from previous sessions.
if [ -s "$STARTUP_FILE" ]; then
echo "Running workspace/startup.sh..."
set +e
HUGGINGMES_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. Check logs above." >&2
fi
# ── Start background services ──
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": "HuggingMes Hermes 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
# ── Launch dashboard once (restarts if it dies) ──
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 sync loop once β€” survives gateway restarts ──
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 loop ──
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
# Monitor health-server β€” restart if it died unexpectedly
if [ -n "${HEALTH_PID:-}" ] && ! kill -0 "$HEALTH_PID" 2>/dev/null; then
echo "Warning: health-server exited (PID $HEALTH_PID dead); restarting..."
node "$APP_DIR/health-server.js" &
HEALTH_PID=$!
echo "Health server restarted (PID: $HEALTH_PID)"
fi
# Monitor Hermes dashboard β€” restart if it died unexpectedly
if [ -n "${DASHBOARD_PID:-}" ] && ! kill -0 "$DASHBOARD_PID" 2>/dev/null; then
echo "Warning: Hermes dashboard exited; restarting..."
start_dashboard_once
fi
# Monitor JupyterLab β€” restart if it died unexpectedly
if [ "${DEV_MODE:-true}" != "false" ] && [ -n "${JUPYTER_PID:-}" ] && ! kill -0 "$JUPYTER_PID" 2>/dev/null; then
echo "Warning: JupyterLab exited (PID $JUPYTER_PID dead); restarting..."
unset JUPYTER_PID
start_jupyter
fi
echo "Launching Hermes gateway..."
(hermes gateway run 2>&1 | tee -a "$HERMES_HOME/logs/gateway.log") &
GATEWAY_PID=$!
ready=false
for ((i=0; i<GATEWAY_READY_TIMEOUT; i++)); do
if (echo > "/dev/tcp/127.0.0.1/${GATEWAY_API_PORT}") 2>/dev/null; then
ready=true
break
fi
if ! kill -0 "$GATEWAY_PID" 2>/dev/null; then
break
fi
sleep 1
done
if [ "$ready" != "true" ]; then
echo ""
echo "Hermes gateway failed to expose the API health port. Last 40 log lines:"
echo "----------------------------------------"
tail -40 "$HERMES_HOME/logs/gateway.log" || true
exit 1
fi
# Start sync loop (only once β€” shared across all gateway restarts)
start_background_sync_once
set +e
wait "$GATEWAY_PID"
GATEWAY_EXIT_CODE=$?
set -e
# Sync state before restart
if [ -n "${HF_TOKEN:-}" ]; then
echo "Gateway exited β€” syncing state before restart..."
python3 "$APP_DIR/hermes-sync.py" sync-once || echo "Warning: sync failed."
fi
GATEWAY_RESTART_COUNT=$((GATEWAY_RESTART_COUNT + 1))
if [ "$GATEWAY_MAX_RESTARTS" != "0" ] && [ "$GATEWAY_RESTART_COUNT" -ge "$GATEWAY_MAX_RESTARTS" ]; then
echo "Gateway exited (code ${GATEWAY_EXIT_CODE}); restart limit (${GATEWAY_MAX_RESTARTS}) reached."
exit "$GATEWAY_EXIT_CODE"
fi
echo "Gateway exited (code ${GATEWAY_EXIT_CODE}); restarting in ${GATEWAY_RESTART_DELAY}s..."
sleep "$GATEWAY_RESTART_DELAY"
done