Spaces:
Running
Running
fix: correct spelling of 'HuggingMes' across multiple files
Browse files- .env.example +3 -3
- CHANGELOG.md +1 -1
- CONTRIBUTING.md +1 -1
- Dockerfile +12 -12
- README.md +10 -10
- SECURITY.md +1 -1
- cloudflare-keepalive-setup.py +4 -4
- cloudflare-proxy-setup.py +4 -4
- docker-compose.yml +4 -4
- health-server.js +27 -14
- hermes-sync.py +5 -5
- start.sh +8 -8
.env.example
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
# ============================================================================
|
| 2 |
-
#
|
| 3 |
# Configuration Reference
|
| 4 |
# ============================================================================
|
| 5 |
|
|
@@ -36,8 +36,8 @@ GATEWAY_TOKEN=your_gateway_token_here
|
|
| 36 |
# HF token with write access for private Dataset backup
|
| 37 |
HF_TOKEN=hf_your_token_here
|
| 38 |
|
| 39 |
-
# Dataset name (default:
|
| 40 |
-
# BACKUP_DATASET_NAME=
|
| 41 |
|
| 42 |
# Backup interval in seconds (default: 180)
|
| 43 |
SYNC_INTERVAL=180
|
|
|
|
| 1 |
# ============================================================================
|
| 2 |
+
# HuggingMes - Hermes Agent on Hugging Face Spaces
|
| 3 |
# Configuration Reference
|
| 4 |
# ============================================================================
|
| 5 |
|
|
|
|
| 36 |
# HF token with write access for private Dataset backup
|
| 37 |
HF_TOKEN=hf_your_token_here
|
| 38 |
|
| 39 |
+
# Dataset name (default: huggingmes-backup)
|
| 40 |
+
# BACKUP_DATASET_NAME=huggingmes-backup
|
| 41 |
|
| 42 |
# Backup interval in seconds (default: 180)
|
| 43 |
SYNC_INTERVAL=180
|
CHANGELOG.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
|
| 3 |
## 0.1.0 - 2026-05-03
|
| 4 |
|
| 5 |
-
- Initial
|
| 6 |
- Added HF Space dashboard, `/health`, `/status`, `/v1/*` proxy, and Telegram webhook proxy.
|
| 7 |
- Added Cloudflare Worker setup for Telegram Bot API base URL proxying.
|
| 8 |
- Added private HF Dataset backup and restore for Hermes state.
|
|
|
|
| 2 |
|
| 3 |
## 0.1.0 - 2026-05-03
|
| 4 |
|
| 5 |
+
- Initial HuggingMes Docker Space wrapper for Nous Research Hermes Agent.
|
| 6 |
- Added HF Space dashboard, `/health`, `/status`, `/v1/*` proxy, and Telegram webhook proxy.
|
| 7 |
- Added Cloudflare Worker setup for Telegram Bot API base URL proxying.
|
| 8 |
- Added private HF Dataset backup and restore for Hermes state.
|
CONTRIBUTING.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
# Contributing
|
| 2 |
|
| 3 |
-
Thanks for improving
|
| 4 |
|
| 5 |
## Local Checks
|
| 6 |
|
|
|
|
| 1 |
# Contributing
|
| 2 |
|
| 3 |
+
Thanks for improving HuggingMes.
|
| 4 |
|
| 5 |
## Local Checks
|
| 6 |
|
Dockerfile
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
#
|
| 2 |
|
| 3 |
ARG HERMES_AGENT_VERSION=latest
|
| 4 |
FROM nousresearch/hermes-agent:${HERMES_AGENT_VERSION}
|
|
@@ -13,20 +13,20 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
| 13 |
&& rm -rf /var/lib/apt/lists/* \
|
| 14 |
&& uv pip install --python /opt/hermes/.venv/bin/python --no-cache-dir huggingface_hub
|
| 15 |
|
| 16 |
-
COPY --chown=hermes:hermes start.sh /opt/
|
| 17 |
-
COPY --chown=hermes:hermes health-server.js /opt/
|
| 18 |
-
COPY --chown=hermes:hermes hermes-sync.py /opt/
|
| 19 |
-
COPY --chown=hermes:hermes cloudflare-proxy-setup.py /opt/
|
| 20 |
-
COPY --chown=hermes:hermes cloudflare-keepalive-setup.py /opt/
|
| 21 |
|
| 22 |
RUN chmod +x \
|
| 23 |
-
/opt/
|
| 24 |
-
/opt/
|
| 25 |
-
/opt/
|
| 26 |
-
/opt/
|
| 27 |
|
| 28 |
ENV HERMES_HOME=/opt/data \
|
| 29 |
-
|
| 30 |
HERMES_AGENT_VERSION=${HERMES_AGENT_VERSION} \
|
| 31 |
PYTHONUNBUFFERED=1
|
| 32 |
|
|
@@ -35,4 +35,4 @@ EXPOSE 7861
|
|
| 35 |
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s \
|
| 36 |
CMD curl -fsS http://localhost:7861/health || exit 1
|
| 37 |
|
| 38 |
-
CMD ["/opt/
|
|
|
|
| 1 |
+
# HuggingMes - Hermes Agent Gateway for Hugging Face Spaces
|
| 2 |
|
| 3 |
ARG HERMES_AGENT_VERSION=latest
|
| 4 |
FROM nousresearch/hermes-agent:${HERMES_AGENT_VERSION}
|
|
|
|
| 13 |
&& rm -rf /var/lib/apt/lists/* \
|
| 14 |
&& uv pip install --python /opt/hermes/.venv/bin/python --no-cache-dir huggingface_hub
|
| 15 |
|
| 16 |
+
COPY --chown=hermes:hermes start.sh /opt/huggingmes/start.sh
|
| 17 |
+
COPY --chown=hermes:hermes health-server.js /opt/huggingmes/health-server.js
|
| 18 |
+
COPY --chown=hermes:hermes hermes-sync.py /opt/huggingmes/hermes-sync.py
|
| 19 |
+
COPY --chown=hermes:hermes cloudflare-proxy-setup.py /opt/huggingmes/cloudflare-proxy-setup.py
|
| 20 |
+
COPY --chown=hermes:hermes cloudflare-keepalive-setup.py /opt/huggingmes/cloudflare-keepalive-setup.py
|
| 21 |
|
| 22 |
RUN chmod +x \
|
| 23 |
+
/opt/huggingmes/start.sh \
|
| 24 |
+
/opt/huggingmes/hermes-sync.py \
|
| 25 |
+
/opt/huggingmes/cloudflare-proxy-setup.py \
|
| 26 |
+
/opt/huggingmes/cloudflare-keepalive-setup.py
|
| 27 |
|
| 28 |
ENV HERMES_HOME=/opt/data \
|
| 29 |
+
HUGGINGMES_APP_DIR=/opt/huggingmes \
|
| 30 |
HERMES_AGENT_VERSION=${HERMES_AGENT_VERSION} \
|
| 31 |
PYTHONUNBUFFERED=1
|
| 32 |
|
|
|
|
| 35 |
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s \
|
| 36 |
CMD curl -fsS http://localhost:7861/health || exit 1
|
| 37 |
|
| 38 |
+
CMD ["/opt/huggingmes/start.sh"]
|
README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: indigo
|
| 6 |
sdk: docker
|
|
@@ -30,7 +30,7 @@ secrets:
|
|
| 30 |
[](https://huggingface.co/spaces)
|
| 31 |
[](https://github.com/NousResearch/hermes-agent)
|
| 32 |
|
| 33 |
-
**Self-hosted Hermes AI agent gateway β free, no server needed.**
|
| 34 |
|
| 35 |
## Table of Contents
|
| 36 |
|
|
@@ -62,7 +62,7 @@ secrets:
|
|
| 62 |
|
| 63 |
### Step 1: Duplicate this Space
|
| 64 |
|
| 65 |
-
[](https://huggingface.co/spaces/somratpro/
|
| 66 |
|
| 67 |
### Step 2: Add Your Secrets
|
| 68 |
|
|
@@ -74,18 +74,18 @@ Navigate to your new Space's **Settings β Variables and secrets**, and add the
|
|
| 74 |
|
| 75 |
### Step 3: Access Your Dashboard
|
| 76 |
|
| 77 |
-
Once the build is complete, visit your Space's public URL. You will see the
|
| 78 |
|
| 79 |
## π Access Control
|
| 80 |
|
| 81 |
-
Hermes' built-in dashboard is local-first.
|
| 82 |
|
| 83 |
- **Dashboard:** Opening `/app/` requires your `GATEWAY_TOKEN`.
|
| 84 |
- **API:** Routes under `/v1/*` (OpenAI-compatible) require `Authorization: Bearer <GATEWAY_TOKEN>`.
|
| 85 |
|
| 86 |
## π€ LLM Providers
|
| 87 |
|
| 88 |
-
|
| 89 |
|
| 90 |
| Provider | Prefix | Example `LLM_MODEL` |
|
| 91 |
| :--- | :--- | :--- |
|
|
@@ -105,16 +105,16 @@ To use Hermes via Telegram:
|
|
| 105 |
|
| 106 |
## π Cloudflare Proxy
|
| 107 |
|
| 108 |
-
HuggingFace Spaces often block outbound connections to external APIs.
|
| 109 |
|
| 110 |
1. Add `CLOUDFLARE_WORKERS_TOKEN` as a Space secret.
|
| 111 |
2. Restart the Space.
|
| 112 |
|
| 113 |
-
|
| 114 |
|
| 115 |
## πΎ Backup & Persistence
|
| 116 |
|
| 117 |
-
Set `HF_TOKEN` with **Write** access to enable backup.
|
| 118 |
|
| 119 |
## π Staying Alive *(Recommended on Free HF Spaces)*
|
| 120 |
|
|
|
|
| 1 |
---
|
| 2 |
+
title: HuggingMes
|
| 3 |
+
emoji: β
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: indigo
|
| 6 |
sdk: docker
|
|
|
|
| 30 |
[](https://huggingface.co/spaces)
|
| 31 |
[](https://github.com/NousResearch/hermes-agent)
|
| 32 |
|
| 33 |
+
**Self-hosted Hermes AI agent gateway β free, no server needed.** HuggingMes runs [Nous Research Hermes Agent](https://github.com/NousResearch/hermes-agent) on HuggingFace Spaces, providing a 24/7 personal AI assistant. It includes a premium management dashboard, automatic persistent backup to HF Datasets, and built-in connectivity fixes to bypass platform restrictions. Deploy in minutes on the free HF Spaces tier with full data persistence.
|
| 34 |
|
| 35 |
## Table of Contents
|
| 36 |
|
|
|
|
| 62 |
|
| 63 |
### Step 1: Duplicate this Space
|
| 64 |
|
| 65 |
+
[](https://huggingface.co/spaces/somratpro/HuggingMes?duplicate=true)
|
| 66 |
|
| 67 |
### Step 2: Add Your Secrets
|
| 68 |
|
|
|
|
| 74 |
|
| 75 |
### Step 3: Access Your Dashboard
|
| 76 |
|
| 77 |
+
Once the build is complete, visit your Space's public URL. You will see the HuggingMes management dashboard. Click **Open Hermes UI** and enter your `GATEWAY_TOKEN` to access the agent interface.
|
| 78 |
|
| 79 |
## π Access Control
|
| 80 |
|
| 81 |
+
Hermes' built-in dashboard is local-first. HuggingMes adds a secure wrapper:
|
| 82 |
|
| 83 |
- **Dashboard:** Opening `/app/` requires your `GATEWAY_TOKEN`.
|
| 84 |
- **API:** Routes under `/v1/*` (OpenAI-compatible) require `Authorization: Bearer <GATEWAY_TOKEN>`.
|
| 85 |
|
| 86 |
## π€ LLM Providers
|
| 87 |
|
| 88 |
+
HuggingMes automatically maps your `LLM_MODEL` and `LLM_API_KEY` to the correct Hermes configuration.
|
| 89 |
|
| 90 |
| Provider | Prefix | Example `LLM_MODEL` |
|
| 91 |
| :--- | :--- | :--- |
|
|
|
|
| 105 |
|
| 106 |
## π Cloudflare Proxy
|
| 107 |
|
| 108 |
+
HuggingFace Spaces often block outbound connections to external APIs. HuggingMes handles this automatically:
|
| 109 |
|
| 110 |
1. Add `CLOUDFLARE_WORKERS_TOKEN` as a Space secret.
|
| 111 |
2. Restart the Space.
|
| 112 |
|
| 113 |
+
HuggingMes will auto-provision a Worker proxy for Telegram and other restricted traffic, and set up a keep-awake cron.
|
| 114 |
|
| 115 |
## πΎ Backup & Persistence
|
| 116 |
|
| 117 |
+
Set `HF_TOKEN` with **Write** access to enable backup. HuggingMes syncs all agent data to a private Dataset named `huggingmes-backup` every 180 seconds.
|
| 118 |
|
| 119 |
## π Staying Alive *(Recommended on Free HF Spaces)*
|
| 120 |
|
SECURITY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
# Security
|
| 2 |
|
| 3 |
-
|
| 4 |
|
| 5 |
## Required Hardening
|
| 6 |
|
|
|
|
| 1 |
# Security
|
| 2 |
|
| 3 |
+
HuggingMes runs a full agent gateway with tool access. Treat the Space and its secrets like a server.
|
| 4 |
|
| 5 |
## Required Hardening
|
| 6 |
|
cloudflare-keepalive-setup.py
CHANGED
|
@@ -12,7 +12,7 @@ import urllib.request
|
|
| 12 |
from pathlib import Path
|
| 13 |
|
| 14 |
API_BASE = "https://api.cloudflare.com/client/v4"
|
| 15 |
-
KEEPALIVE_STATUS_FILE = Path("/tmp/
|
| 16 |
|
| 17 |
|
| 18 |
def cf_request(method: str, path: str, token: str, body: bytes | None = None, content_type: str = "application/json"):
|
|
@@ -33,7 +33,7 @@ def cf_request(method: str, path: str, token: str, body: bytes | None = None, co
|
|
| 33 |
def slugify(value: str) -> str:
|
| 34 |
cleaned = re.sub(r"[^a-z0-9-]+", "-", value.lower()).strip("-")
|
| 35 |
cleaned = re.sub(r"-{2,}", "-", cleaned)
|
| 36 |
-
return (cleaned or "
|
| 37 |
|
| 38 |
|
| 39 |
def get_space_host() -> str:
|
|
@@ -56,7 +56,7 @@ def derive_keepalive_worker_name() -> str:
|
|
| 56 |
space_host = get_space_host()
|
| 57 |
if space_host:
|
| 58 |
return slugify(f"{space_host.replace('.hf.space', '')}-keepalive")
|
| 59 |
-
return "
|
| 60 |
|
| 61 |
|
| 62 |
def render_keepalive_worker(target_url: str) -> str:
|
|
@@ -76,7 +76,7 @@ async function ping(source) {{
|
|
| 76 |
const response = await fetch(TARGET_URL, {{
|
| 77 |
method: "GET",
|
| 78 |
headers: {{
|
| 79 |
-
"user-agent": "
|
| 80 |
"cache-control": "no-cache"
|
| 81 |
}},
|
| 82 |
cf: {{ cacheTtl: 0, cacheEverything: false }}
|
|
|
|
| 12 |
from pathlib import Path
|
| 13 |
|
| 14 |
API_BASE = "https://api.cloudflare.com/client/v4"
|
| 15 |
+
KEEPALIVE_STATUS_FILE = Path("/tmp/huggingmes-cloudflare-keepalive-status.json")
|
| 16 |
|
| 17 |
|
| 18 |
def cf_request(method: str, path: str, token: str, body: bytes | None = None, content_type: str = "application/json"):
|
|
|
|
| 33 |
def slugify(value: str) -> str:
|
| 34 |
cleaned = re.sub(r"[^a-z0-9-]+", "-", value.lower()).strip("-")
|
| 35 |
cleaned = re.sub(r"-{2,}", "-", cleaned)
|
| 36 |
+
return (cleaned or "huggingmes-proxy")[:63].rstrip("-")
|
| 37 |
|
| 38 |
|
| 39 |
def get_space_host() -> str:
|
|
|
|
| 56 |
space_host = get_space_host()
|
| 57 |
if space_host:
|
| 58 |
return slugify(f"{space_host.replace('.hf.space', '')}-keepalive")
|
| 59 |
+
return "huggingmes-keepalive"
|
| 60 |
|
| 61 |
|
| 62 |
def render_keepalive_worker(target_url: str) -> str:
|
|
|
|
| 76 |
const response = await fetch(TARGET_URL, {{
|
| 77 |
method: "GET",
|
| 78 |
headers: {{
|
| 79 |
+
"user-agent": "HuggingMes Cloudflare KeepAlive",
|
| 80 |
"cache-control": "no-cache"
|
| 81 |
}},
|
| 82 |
cf: {{ cacheTtl: 0, cacheEverything: false }}
|
cloudflare-proxy-setup.py
CHANGED
|
@@ -13,8 +13,8 @@ import urllib.request
|
|
| 13 |
from pathlib import Path
|
| 14 |
|
| 15 |
API_BASE = "https://api.cloudflare.com/client/v4"
|
| 16 |
-
ENV_FILE = Path("/tmp/
|
| 17 |
-
ENV_FILE = Path("/tmp/
|
| 18 |
DEFAULT_ALLOWED = [
|
| 19 |
"api.telegram.org",
|
| 20 |
"discord.com",
|
|
@@ -52,7 +52,7 @@ def cf_request(method: str, path: str, token: str, body: bytes | None = None, co
|
|
| 52 |
def slugify(value: str) -> str:
|
| 53 |
cleaned = re.sub(r"[^a-z0-9-]+", "-", value.lower()).strip("-")
|
| 54 |
cleaned = re.sub(r"-{2,}", "-", cleaned)
|
| 55 |
-
return (cleaned or "
|
| 56 |
|
| 57 |
|
| 58 |
def derive_worker_name() -> str:
|
|
@@ -62,7 +62,7 @@ def derive_worker_name() -> str:
|
|
| 62 |
space_host = os.environ.get("SPACE_HOST", "").strip()
|
| 63 |
if space_host:
|
| 64 |
return slugify(f"{space_host.replace('.hf.space', '')}-proxy")
|
| 65 |
-
return "
|
| 66 |
|
| 67 |
|
| 68 |
def render_worker(secret_value: str, allowed_targets: list[str], allow_proxy_all: bool) -> str:
|
|
|
|
| 13 |
from pathlib import Path
|
| 14 |
|
| 15 |
API_BASE = "https://api.cloudflare.com/client/v4"
|
| 16 |
+
ENV_FILE = Path("/tmp/huggingmes-cloudflare-proxy.env")
|
| 17 |
+
ENV_FILE = Path("/tmp/huggingmes-cloudflare-proxy.env")
|
| 18 |
DEFAULT_ALLOWED = [
|
| 19 |
"api.telegram.org",
|
| 20 |
"discord.com",
|
|
|
|
| 52 |
def slugify(value: str) -> str:
|
| 53 |
cleaned = re.sub(r"[^a-z0-9-]+", "-", value.lower()).strip("-")
|
| 54 |
cleaned = re.sub(r"-{2,}", "-", cleaned)
|
| 55 |
+
return (cleaned or "huggingmes-proxy")[:63].rstrip("-")
|
| 56 |
|
| 57 |
|
| 58 |
def derive_worker_name() -> str:
|
|
|
|
| 62 |
space_host = os.environ.get("SPACE_HOST", "").strip()
|
| 63 |
if space_host:
|
| 64 |
return slugify(f"{space_host.replace('.hf.space', '')}-proxy")
|
| 65 |
+
return "huggingmes-proxy"
|
| 66 |
|
| 67 |
|
| 68 |
def render_worker(secret_value: str, allowed_targets: list[str], allow_proxy_all: bool) -> str:
|
docker-compose.yml
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
services:
|
| 2 |
-
|
| 3 |
build:
|
| 4 |
context: .
|
| 5 |
args:
|
| 6 |
HERMES_AGENT_VERSION: ${HERMES_AGENT_VERSION:-latest}
|
| 7 |
-
image:
|
| 8 |
ports:
|
| 9 |
- "7861:7861"
|
| 10 |
environment:
|
|
@@ -16,7 +16,7 @@ services:
|
|
| 16 |
HF_TOKEN: ${HF_TOKEN:-}
|
| 17 |
SPACE_HOST: ${SPACE_HOST:-localhost:7861}
|
| 18 |
volumes:
|
| 19 |
-
-
|
| 20 |
|
| 21 |
volumes:
|
| 22 |
-
|
|
|
|
| 1 |
services:
|
| 2 |
+
huggingmes:
|
| 3 |
build:
|
| 4 |
context: .
|
| 5 |
args:
|
| 6 |
HERMES_AGENT_VERSION: ${HERMES_AGENT_VERSION:-latest}
|
| 7 |
+
image: huggingmes:local
|
| 8 |
ports:
|
| 9 |
- "7861:7861"
|
| 10 |
environment:
|
|
|
|
| 16 |
HF_TOKEN: ${HF_TOKEN:-}
|
| 17 |
SPACE_HOST: ${SPACE_HOST:-localhost:7861}
|
| 18 |
volumes:
|
| 19 |
+
- huggingmes-data:/opt/data
|
| 20 |
|
| 21 |
volumes:
|
| 22 |
+
huggingmes-data:
|
health-server.js
CHANGED
|
@@ -14,11 +14,11 @@ const startTime = Date.now();
|
|
| 14 |
const API_SERVER_KEY = process.env.API_SERVER_KEY || "";
|
| 15 |
const APP_BASE = "/app";
|
| 16 |
const LOGIN_PATH = "/login";
|
| 17 |
-
const SESSION_COOKIE = "
|
| 18 |
|
| 19 |
-
const SYNC_STATUS_FILE = "/tmp/
|
| 20 |
const CLOUDFLARE_KEEPALIVE_STATUS_FILE =
|
| 21 |
-
"/tmp/
|
| 22 |
|
| 23 |
function canConnect(port, host = GATEWAY_HOST, timeoutMs = 600) {
|
| 24 |
return new Promise((resolve) => {
|
|
@@ -54,7 +54,7 @@ function expectedSessionValue() {
|
|
| 54 |
if (!API_SERVER_KEY) return "";
|
| 55 |
return crypto
|
| 56 |
.createHmac("sha256", API_SERVER_KEY)
|
| 57 |
-
.update("
|
| 58 |
.digest("hex");
|
| 59 |
}
|
| 60 |
|
|
@@ -122,7 +122,7 @@ function renderLoginPage(nextPath, errorMessage = "") {
|
|
| 122 |
<head>
|
| 123 |
<meta charset="utf-8" />
|
| 124 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 125 |
-
<title>
|
| 126 |
<style>
|
| 127 |
:root { color-scheme: dark; --bg:#10141f; --panel:#171d2b; --line:#293246; --text:#f4f7fb; --muted:#9aa7bd; --bad:#ef4444; --accent:#38bdf8; }
|
| 128 |
* { box-sizing:border-box; }
|
|
@@ -138,7 +138,7 @@ function renderLoginPage(nextPath, errorMessage = "") {
|
|
| 138 |
</head>
|
| 139 |
<body>
|
| 140 |
<main>
|
| 141 |
-
<h1>Open
|
| 142 |
<p>Enter the <code>GATEWAY_TOKEN</code> from your Space secrets.</p>
|
| 143 |
${errorHtml}
|
| 144 |
<form method="post" action="${LOGIN_PATH}">
|
|
@@ -410,7 +410,9 @@ function renderDashboard(data) {
|
|
| 410 |
const telegramDetail = data.telegram.configured
|
| 411 |
? `${data.telegram.webhook ? "Webhook" : "Polling"}${data.telegram.proxy ? " via CF proxy" : ""}`
|
| 412 |
: "Not configured";
|
| 413 |
-
const backupDetail = data.backup?.message
|
|
|
|
|
|
|
| 414 |
const keepAliveDetail = keepaliveConfigured
|
| 415 |
? `Pinging <code>${escapeHtml(data.keepalive.targetUrl || "/health")}</code>`
|
| 416 |
: process.env.CLOUDFLARE_WORKERS_TOKEN
|
|
@@ -421,8 +423,13 @@ function renderDashboard(data) {
|
|
| 421 |
const tiles = [
|
| 422 |
renderTile({
|
| 423 |
title: "Gateway",
|
| 424 |
-
value: toneBadge(
|
| 425 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 426 |
tone: data.gateway ? "ok" : "off",
|
| 427 |
meta: data.authConfigured ? "Protected" : "Unprotected",
|
| 428 |
}),
|
|
@@ -440,7 +447,10 @@ function renderDashboard(data) {
|
|
| 440 |
}),
|
| 441 |
renderTile({
|
| 442 |
title: "Telegram",
|
| 443 |
-
value: toneBadge(
|
|
|
|
|
|
|
|
|
|
| 444 |
detail: telegramDetail,
|
| 445 |
tone: telegramTone,
|
| 446 |
}),
|
|
@@ -452,7 +462,10 @@ function renderDashboard(data) {
|
|
| 452 |
}),
|
| 453 |
renderTile({
|
| 454 |
title: "Keep Awake",
|
| 455 |
-
value: toneBadge(
|
|
|
|
|
|
|
|
|
|
| 456 |
detail: keepAliveDetail,
|
| 457 |
tone: keepAliveTone,
|
| 458 |
}),
|
|
@@ -463,7 +476,7 @@ function renderDashboard(data) {
|
|
| 463 |
<head>
|
| 464 |
<meta charset="utf-8" />
|
| 465 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 466 |
-
<title>
|
| 467 |
<style>
|
| 468 |
:root { color-scheme: dark; --bg:#08080f; --panel:#12111b; --panel2:#151421; --line:#26243a; --text:#f6f4ff; --muted:#7f7a9e; --soft:#b8b3d7; --good:#22c55e; --warn:#f5c542; --bad:#fb7185; --accent:#6557df; --accent2:#7c6cf2; }
|
| 469 |
* { box-sizing:border-box; }
|
|
@@ -508,7 +521,7 @@ function renderDashboard(data) {
|
|
| 508 |
<body>
|
| 509 |
<main>
|
| 510 |
<header>
|
| 511 |
-
<h1>
|
| 512 |
<div class="subtitle">Self-hosted - Hermes Agent</div>
|
| 513 |
</header>
|
| 514 |
<a class="hero-action" href="${APP_BASE}/" target="_blank" rel="noopener noreferrer">Open Hermes Agent -></a>
|
|
@@ -641,5 +654,5 @@ const server = http.createServer(async (req, res) => {
|
|
| 641 |
});
|
| 642 |
|
| 643 |
server.listen(PORT, "0.0.0.0", () => {
|
| 644 |
-
console.log(`
|
| 645 |
});
|
|
|
|
| 14 |
const API_SERVER_KEY = process.env.API_SERVER_KEY || "";
|
| 15 |
const APP_BASE = "/app";
|
| 16 |
const LOGIN_PATH = "/login";
|
| 17 |
+
const SESSION_COOKIE = "huggingmes_session";
|
| 18 |
|
| 19 |
+
const SYNC_STATUS_FILE = "/tmp/huggingmes-sync-status.json";
|
| 20 |
const CLOUDFLARE_KEEPALIVE_STATUS_FILE =
|
| 21 |
+
"/tmp/huggingmes-cloudflare-keepalive-status.json";
|
| 22 |
|
| 23 |
function canConnect(port, host = GATEWAY_HOST, timeoutMs = 600) {
|
| 24 |
return new Promise((resolve) => {
|
|
|
|
| 54 |
if (!API_SERVER_KEY) return "";
|
| 55 |
return crypto
|
| 56 |
.createHmac("sha256", API_SERVER_KEY)
|
| 57 |
+
.update("huggingmes-session-v1")
|
| 58 |
.digest("hex");
|
| 59 |
}
|
| 60 |
|
|
|
|
| 122 |
<head>
|
| 123 |
<meta charset="utf-8" />
|
| 124 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 125 |
+
<title>HuggingMes Login</title>
|
| 126 |
<style>
|
| 127 |
:root { color-scheme: dark; --bg:#10141f; --panel:#171d2b; --line:#293246; --text:#f4f7fb; --muted:#9aa7bd; --bad:#ef4444; --accent:#38bdf8; }
|
| 128 |
* { box-sizing:border-box; }
|
|
|
|
| 138 |
</head>
|
| 139 |
<body>
|
| 140 |
<main>
|
| 141 |
+
<h1>Open HuggingMes</h1>
|
| 142 |
<p>Enter the <code>GATEWAY_TOKEN</code> from your Space secrets.</p>
|
| 143 |
${errorHtml}
|
| 144 |
<form method="post" action="${LOGIN_PATH}">
|
|
|
|
| 410 |
const telegramDetail = data.telegram.configured
|
| 411 |
? `${data.telegram.webhook ? "Webhook" : "Polling"}${data.telegram.proxy ? " via CF proxy" : ""}`
|
| 412 |
: "Not configured";
|
| 413 |
+
const backupDetail = data.backup?.message
|
| 414 |
+
? escapeHtml(data.backup.message)
|
| 415 |
+
: "No status yet";
|
| 416 |
const keepAliveDetail = keepaliveConfigured
|
| 417 |
? `Pinging <code>${escapeHtml(data.keepalive.targetUrl || "/health")}</code>`
|
| 418 |
: process.env.CLOUDFLARE_WORKERS_TOKEN
|
|
|
|
| 423 |
const tiles = [
|
| 424 |
renderTile({
|
| 425 |
title: "Gateway",
|
| 426 |
+
value: toneBadge(
|
| 427 |
+
data.gateway ? "Online" : "Offline",
|
| 428 |
+
data.gateway ? "ok" : "off",
|
| 429 |
+
),
|
| 430 |
+
detail: data.gateway
|
| 431 |
+
? `API on port ${data.ports.gateway}`
|
| 432 |
+
: `Unreachable`,
|
| 433 |
tone: data.gateway ? "ok" : "off",
|
| 434 |
meta: data.authConfigured ? "Protected" : "Unprotected",
|
| 435 |
}),
|
|
|
|
| 447 |
}),
|
| 448 |
renderTile({
|
| 449 |
title: "Telegram",
|
| 450 |
+
value: toneBadge(
|
| 451 |
+
data.telegram.configured ? "Configured" : "Disabled",
|
| 452 |
+
telegramTone,
|
| 453 |
+
),
|
| 454 |
detail: telegramDetail,
|
| 455 |
tone: telegramTone,
|
| 456 |
}),
|
|
|
|
| 462 |
}),
|
| 463 |
renderTile({
|
| 464 |
title: "Keep Awake",
|
| 465 |
+
value: toneBadge(
|
| 466 |
+
keepaliveConfigured ? "CF Cron" : keepaliveStatus.toUpperCase(),
|
| 467 |
+
keepAliveTone,
|
| 468 |
+
),
|
| 469 |
detail: keepAliveDetail,
|
| 470 |
tone: keepAliveTone,
|
| 471 |
}),
|
|
|
|
| 476 |
<head>
|
| 477 |
<meta charset="utf-8" />
|
| 478 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 479 |
+
<title>HuggingMes</title>
|
| 480 |
<style>
|
| 481 |
:root { color-scheme: dark; --bg:#08080f; --panel:#12111b; --panel2:#151421; --line:#26243a; --text:#f6f4ff; --muted:#7f7a9e; --soft:#b8b3d7; --good:#22c55e; --warn:#f5c542; --bad:#fb7185; --accent:#6557df; --accent2:#7c6cf2; }
|
| 482 |
* { box-sizing:border-box; }
|
|
|
|
| 521 |
<body>
|
| 522 |
<main>
|
| 523 |
<header>
|
| 524 |
+
<h1>HuggingMes</h1>
|
| 525 |
<div class="subtitle">Self-hosted - Hermes Agent</div>
|
| 526 |
</header>
|
| 527 |
<a class="hero-action" href="${APP_BASE}/" target="_blank" rel="noopener noreferrer">Open Hermes Agent -></a>
|
|
|
|
| 654 |
});
|
| 655 |
|
| 656 |
server.listen(PORT, "0.0.0.0", () => {
|
| 657 |
+
console.log(`HuggingMes dashboard listening on 0.0.0.0:${PORT}`);
|
| 658 |
});
|
hermes-sync.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
|
| 4 |
import hashlib
|
| 5 |
import json
|
|
@@ -22,13 +22,13 @@ from huggingface_hub.errors import HfHubHTTPError, RepositoryNotFoundError
|
|
| 22 |
logging.getLogger("huggingface_hub").setLevel(logging.ERROR)
|
| 23 |
|
| 24 |
HERMES_HOME = Path(os.environ.get("HERMES_HOME", "/opt/data"))
|
| 25 |
-
STATUS_FILE = Path("/tmp/
|
| 26 |
INTERVAL = int(os.environ.get("SYNC_INTERVAL", "180"))
|
| 27 |
INITIAL_DELAY = int(os.environ.get("SYNC_START_DELAY", "10"))
|
| 28 |
HF_TOKEN = os.environ.get("HF_TOKEN", "").strip()
|
| 29 |
HF_USERNAME = os.environ.get("HF_USERNAME", "").strip()
|
| 30 |
SPACE_AUTHOR_NAME = os.environ.get("SPACE_AUTHOR_NAME", "").strip()
|
| 31 |
-
BACKUP_DATASET_NAME = os.environ.get("BACKUP_DATASET_NAME", "
|
| 32 |
INCLUDE_ENV = os.environ.get("SYNC_INCLUDE_ENV", "").strip().lower() in {"1", "true", "yes"}
|
| 33 |
MAX_FILE_SIZE_BYTES = int(os.environ.get("SYNC_MAX_FILE_BYTES", str(50 * 1024 * 1024)))
|
| 34 |
|
|
@@ -142,7 +142,7 @@ def fingerprint_dir(root: Path) -> str:
|
|
| 142 |
|
| 143 |
|
| 144 |
def create_snapshot_dir(source_root: Path) -> Path:
|
| 145 |
-
staging_root = Path(tempfile.mkdtemp(prefix="
|
| 146 |
for path in sorted(source_root.rglob("*")):
|
| 147 |
rel = path.relative_to(source_root)
|
| 148 |
rel_posix = rel.as_posix()
|
|
@@ -237,7 +237,7 @@ def sync_once(last_fingerprint: str | None = None, last_marker: tuple[int, int,
|
|
| 237 |
repo_id=repo_id,
|
| 238 |
repo_type="dataset",
|
| 239 |
token=HF_TOKEN,
|
| 240 |
-
commit_message=f"
|
| 241 |
ignore_patterns=[".git/*", ".git"],
|
| 242 |
)
|
| 243 |
finally:
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
+
"""HuggingMes Hermes state backup via Hugging Face Datasets."""
|
| 3 |
|
| 4 |
import hashlib
|
| 5 |
import json
|
|
|
|
| 22 |
logging.getLogger("huggingface_hub").setLevel(logging.ERROR)
|
| 23 |
|
| 24 |
HERMES_HOME = Path(os.environ.get("HERMES_HOME", "/opt/data"))
|
| 25 |
+
STATUS_FILE = Path("/tmp/huggingmes-sync-status.json")
|
| 26 |
INTERVAL = int(os.environ.get("SYNC_INTERVAL", "180"))
|
| 27 |
INITIAL_DELAY = int(os.environ.get("SYNC_START_DELAY", "10"))
|
| 28 |
HF_TOKEN = os.environ.get("HF_TOKEN", "").strip()
|
| 29 |
HF_USERNAME = os.environ.get("HF_USERNAME", "").strip()
|
| 30 |
SPACE_AUTHOR_NAME = os.environ.get("SPACE_AUTHOR_NAME", "").strip()
|
| 31 |
+
BACKUP_DATASET_NAME = os.environ.get("BACKUP_DATASET_NAME", "huggingmes-backup").strip()
|
| 32 |
INCLUDE_ENV = os.environ.get("SYNC_INCLUDE_ENV", "").strip().lower() in {"1", "true", "yes"}
|
| 33 |
MAX_FILE_SIZE_BYTES = int(os.environ.get("SYNC_MAX_FILE_BYTES", str(50 * 1024 * 1024)))
|
| 34 |
|
|
|
|
| 142 |
|
| 143 |
|
| 144 |
def create_snapshot_dir(source_root: Path) -> Path:
|
| 145 |
+
staging_root = Path(tempfile.mkdtemp(prefix="huggingmes-sync-"))
|
| 146 |
for path in sorted(source_root.rglob("*")):
|
| 147 |
rel = path.relative_to(source_root)
|
| 148 |
rel_posix = rel.as_posix()
|
|
|
|
| 237 |
repo_id=repo_id,
|
| 238 |
repo_type="dataset",
|
| 239 |
token=HF_TOKEN,
|
| 240 |
+
commit_message=f"HuggingMes sync {time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}",
|
| 241 |
ignore_patterns=[".git/*", ".git"],
|
| 242 |
)
|
| 243 |
finally:
|
start.sh
CHANGED
|
@@ -4,19 +4,19 @@ set -euo pipefail
|
|
| 4 |
umask 0077
|
| 5 |
|
| 6 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 7 |
-
#
|
| 8 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 9 |
|
| 10 |
# ββ Startup Banner ββ
|
| 11 |
-
APP_DIR="${
|
| 12 |
HERMES_HOME="${HERMES_HOME:-/opt/data}"
|
| 13 |
PUBLIC_PORT="${PORT:-7861}"
|
| 14 |
GATEWAY_API_PORT="${API_SERVER_PORT:-8642}"
|
| 15 |
DASHBOARD_PORT="${DASHBOARD_PORT:-9119}"
|
| 16 |
TELEGRAM_WEBHOOK_PORT="${TELEGRAM_WEBHOOK_PORT:-8765}"
|
| 17 |
SYNC_INTERVAL="${SYNC_INTERVAL:-180}"
|
| 18 |
-
BACKUP_DATASET="${BACKUP_DATASET_NAME:-
|
| 19 |
-
CF_PROXY_ENV_FILE="/tmp/
|
| 20 |
|
| 21 |
export HERMES_HOME
|
| 22 |
export API_SERVER_ENABLED="${API_SERVER_ENABLED:-true}"
|
|
@@ -27,7 +27,7 @@ export TELEGRAM_WEBHOOK_PORT
|
|
| 27 |
|
| 28 |
echo ""
|
| 29 |
echo " ββββββββββββββββββββββββββββββββββββββββββββ"
|
| 30 |
-
echo " β π¬
|
| 31 |
echo " ββββββββββββββββββββββββββββββββββββββββββββ"
|
| 32 |
echo ""
|
| 33 |
|
|
@@ -85,7 +85,7 @@ if [ -n "${TELEGRAM_BOT_TOKEN:-}" ] && [ -n "${SPACE_HOST:-}" ] && [ -z "${TELEG
|
|
| 85 |
fi
|
| 86 |
|
| 87 |
if [ -n "${TELEGRAM_WEBHOOK_URL:-}" ] && [ -z "${TELEGRAM_WEBHOOK_SECRET:-}" ]; then
|
| 88 |
-
SECRET_FILE="$HERMES_HOME/.
|
| 89 |
if [ -f "$SECRET_FILE" ]; then
|
| 90 |
export TELEGRAM_WEBHOOK_SECRET
|
| 91 |
TELEGRAM_WEBHOOK_SECRET="$(cat "$SECRET_FILE")"
|
|
@@ -274,7 +274,7 @@ echo ""
|
|
| 274 |
|
| 275 |
# ββ Trap SIGTERM for graceful shutdown ββ
|
| 276 |
graceful_shutdown() {
|
| 277 |
-
echo "Shutting down
|
| 278 |
if [ -n "${HF_TOKEN:-}" ]; then
|
| 279 |
python3 "$APP_DIR/hermes-sync.py" sync-once || echo "Warning: shutdown sync failed."
|
| 280 |
fi
|
|
@@ -293,7 +293,7 @@ import json, os, urllib.request
|
|
| 293 |
body = json.dumps({
|
| 294 |
"event": "restart",
|
| 295 |
"status": "success",
|
| 296 |
-
"message": "
|
| 297 |
"model": os.environ.get("MODEL_FOR_CONFIG", ""),
|
| 298 |
}).encode()
|
| 299 |
req = urllib.request.Request(os.environ["WEBHOOK_URL"], data=body, method="POST", headers={"Content-Type": "application/json"})
|
|
|
|
| 4 |
umask 0077
|
| 5 |
|
| 6 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 7 |
+
# HuggingMes β Hermes Gateway for HF Spaces
|
| 8 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 9 |
|
| 10 |
# ββ Startup Banner ββ
|
| 11 |
+
APP_DIR="${HUGGINGMES_APP_DIR:-/opt/huggingmes}"
|
| 12 |
HERMES_HOME="${HERMES_HOME:-/opt/data}"
|
| 13 |
PUBLIC_PORT="${PORT:-7861}"
|
| 14 |
GATEWAY_API_PORT="${API_SERVER_PORT:-8642}"
|
| 15 |
DASHBOARD_PORT="${DASHBOARD_PORT:-9119}"
|
| 16 |
TELEGRAM_WEBHOOK_PORT="${TELEGRAM_WEBHOOK_PORT:-8765}"
|
| 17 |
SYNC_INTERVAL="${SYNC_INTERVAL:-180}"
|
| 18 |
+
BACKUP_DATASET="${BACKUP_DATASET_NAME:-huggingmes-backup}"
|
| 19 |
+
CF_PROXY_ENV_FILE="/tmp/huggingmes-cloudflare-proxy.env"
|
| 20 |
|
| 21 |
export HERMES_HOME
|
| 22 |
export API_SERVER_ENABLED="${API_SERVER_ENABLED:-true}"
|
|
|
|
| 27 |
|
| 28 |
echo ""
|
| 29 |
echo " ββββββββββββββββββββββββββββββββββββββββββββ"
|
| 30 |
+
echo " β π¬ HuggingMes Hermes Gateway β"
|
| 31 |
echo " ββββββββββββββββββββββββββββββββββββββββββββ"
|
| 32 |
echo ""
|
| 33 |
|
|
|
|
| 85 |
fi
|
| 86 |
|
| 87 |
if [ -n "${TELEGRAM_WEBHOOK_URL:-}" ] && [ -z "${TELEGRAM_WEBHOOK_SECRET:-}" ]; then
|
| 88 |
+
SECRET_FILE="$HERMES_HOME/.huggingmes-telegram-webhook-secret"
|
| 89 |
if [ -f "$SECRET_FILE" ]; then
|
| 90 |
export TELEGRAM_WEBHOOK_SECRET
|
| 91 |
TELEGRAM_WEBHOOK_SECRET="$(cat "$SECRET_FILE")"
|
|
|
|
| 274 |
|
| 275 |
# ββ Trap SIGTERM for graceful shutdown ββ
|
| 276 |
graceful_shutdown() {
|
| 277 |
+
echo "Shutting down HuggingMes..."
|
| 278 |
if [ -n "${HF_TOKEN:-}" ]; then
|
| 279 |
python3 "$APP_DIR/hermes-sync.py" sync-once || echo "Warning: shutdown sync failed."
|
| 280 |
fi
|
|
|
|
| 293 |
body = json.dumps({
|
| 294 |
"event": "restart",
|
| 295 |
"status": "success",
|
| 296 |
+
"message": "HuggingMes Hermes gateway has started.",
|
| 297 |
"model": os.environ.get("MODEL_FOR_CONFIG", ""),
|
| 298 |
}).encode()
|
| 299 |
req = urllib.request.Request(os.environ["WEBHOOK_URL"], data=body, method="POST", headers={"Content-Type": "application/json"})
|