somratpro commited on
Commit
51ec4bc
Β·
1 Parent(s): b4ed131

feat: add WhatsApp pairing guardian, improve DNS resolution, and implement workspace sync status reporting

Browse files
Files changed (10) hide show
  1. .env.example +3 -7
  2. Dockerfile +8 -3
  3. README.md +81 -51
  4. dns-fix.js +102 -13
  5. health-server.js +467 -19
  6. iframe-fix.cjs +45 -0
  7. keep-alive.sh +2 -2
  8. start.sh +35 -3
  9. wa-guardian.js +126 -0
  10. workspace-sync.py +45 -2
.env.example CHANGED
@@ -132,7 +132,7 @@ GATEWAY_TOKEN=your_gateway_token_here
132
  # If set, users can log in with this password instead of the token
133
  # OPENCLAW_PASSWORD=your_password_here
134
 
135
- # ── OPTIONAL: Telegram Integration ──
136
  # Get bot token from: https://t.me/BotFather
137
  TELEGRAM_BOT_TOKEN=your_bot_token_here
138
 
@@ -161,12 +161,8 @@ KEEP_ALIVE_INTERVAL=300
161
  # Workspace auto-sync interval (seconds). Default: 600.
162
  SYNC_INTERVAL=600
163
 
164
- # ── OPTIONAL: Advanced ──
165
- # Pin OpenClaw version. Default: latest
166
- OPENCLAW_VERSION=latest
167
-
168
- # Health endpoint port. Default: 7861
169
- HEALTH_PORT=7861
170
 
171
  # Trusted proxies (comma-separated IPs)
172
  # Fixes "Proxy headers detected from untrusted address" behind reverse proxies
 
132
  # If set, users can log in with this password instead of the token
133
  # OPENCLAW_PASSWORD=your_password_here
134
 
135
+ # ── OPTIONAL: Chat Integrations ──
136
  # Get bot token from: https://t.me/BotFather
137
  TELEGRAM_BOT_TOKEN=your_bot_token_here
138
 
 
161
  # Workspace auto-sync interval (seconds). Default: 600.
162
  SYNC_INTERVAL=600
163
 
164
+ # Webhooks: Standard POST notifications for lifecycle events
165
+ # WEBHOOK_URL=https://your-webhook-endpoint.com/log
 
 
 
 
166
 
167
  # Trusted proxies (comma-separated IPs)
168
  # Fixes "Proxy headers detected from untrusted address" behind reverse proxies
Dockerfile CHANGED
@@ -4,10 +4,12 @@
4
  # Multi-stage build: uses pre-built OpenClaw image for fast builds
5
 
6
  # ── Stage 1: Pull pre-built OpenClaw ──
7
- FROM ghcr.io/openclaw/openclaw:latest AS openclaw
 
8
 
9
  # ── Stage 2: Runtime ──
10
  FROM node:22-slim
 
11
 
12
  # Install system dependencies
13
  RUN apt-get update && apt-get install -y \
@@ -30,24 +32,27 @@ COPY --from=openclaw --chown=1000:1000 /app /home/node/.openclaw/openclaw-app
30
 
31
  # Symlink openclaw CLI so it's available globally
32
  RUN ln -s /home/node/.openclaw/openclaw-app/openclaw.mjs /usr/local/bin/openclaw 2>/dev/null || \
33
- npm install -g openclaw@latest
34
 
35
  # Copy HuggingClaw files
36
  COPY --chown=1000:1000 dns-fix.js /opt/dns-fix.js
37
  COPY --chown=1000:1000 health-server.js /home/node/app/health-server.js
 
38
  COPY --chown=1000:1000 start.sh /home/node/app/start.sh
39
  COPY --chown=1000:1000 keep-alive.sh /home/node/app/keep-alive.sh
 
40
  COPY --chown=1000:1000 workspace-sync.py /home/node/app/workspace-sync.py
41
  RUN chmod +x /home/node/app/start.sh /home/node/app/keep-alive.sh
42
 
43
  USER node
44
 
45
  ENV HOME=/home/node \
 
46
  PATH=/home/node/.local/bin:/usr/local/bin:$PATH \
47
  NODE_OPTIONS="--require /opt/dns-fix.js"
48
 
49
  WORKDIR /home/node/app
50
 
51
- EXPOSE 7860
52
 
53
  CMD ["/home/node/app/start.sh"]
 
4
  # Multi-stage build: uses pre-built OpenClaw image for fast builds
5
 
6
  # ── Stage 1: Pull pre-built OpenClaw ──
7
+ ARG OPENCLAW_VERSION=latest
8
+ FROM ghcr.io/openclaw/openclaw:${OPENCLAW_VERSION} AS openclaw
9
 
10
  # ── Stage 2: Runtime ──
11
  FROM node:22-slim
12
+ ARG OPENCLAW_VERSION=latest
13
 
14
  # Install system dependencies
15
  RUN apt-get update && apt-get install -y \
 
32
 
33
  # Symlink openclaw CLI so it's available globally
34
  RUN ln -s /home/node/.openclaw/openclaw-app/openclaw.mjs /usr/local/bin/openclaw 2>/dev/null || \
35
+ npm install -g openclaw@${OPENCLAW_VERSION}
36
 
37
  # Copy HuggingClaw files
38
  COPY --chown=1000:1000 dns-fix.js /opt/dns-fix.js
39
  COPY --chown=1000:1000 health-server.js /home/node/app/health-server.js
40
+ COPY --chown=1000:1000 iframe-fix.cjs /home/node/app/iframe-fix.cjs
41
  COPY --chown=1000:1000 start.sh /home/node/app/start.sh
42
  COPY --chown=1000:1000 keep-alive.sh /home/node/app/keep-alive.sh
43
+ COPY --chown=1000:1000 wa-guardian.js /home/node/app/wa-guardian.js
44
  COPY --chown=1000:1000 workspace-sync.py /home/node/app/workspace-sync.py
45
  RUN chmod +x /home/node/app/start.sh /home/node/app/keep-alive.sh
46
 
47
  USER node
48
 
49
  ENV HOME=/home/node \
50
+ OPENCLAW_VERSION=${OPENCLAW_VERSION} \
51
  PATH=/home/node/.local/bin:/usr/local/bin:$PATH \
52
  NODE_OPTIONS="--require /opt/dns-fix.js"
53
 
54
  WORKDIR /home/node/app
55
 
56
+ EXPOSE 7861
57
 
58
  CMD ["/home/node/app/start.sh"]
README.md CHANGED
@@ -4,7 +4,8 @@ emoji: 🦞
4
  colorFrom: blue
5
  colorTo: purple
6
  sdk: docker
7
- app_port: 7860
 
8
  pinned: true
9
  license: mit
10
  ---
@@ -15,7 +16,7 @@ license: mit
15
  [![HF Space](https://img.shields.io/badge/πŸ€—%20HuggingFace-Space-blue?style=flat-square)](https://huggingface.co/spaces)
16
  [![OpenClaw](https://img.shields.io/badge/OpenClaw-Gateway-red?style=flat-square)](https://github.com/openclaw/openclaw)
17
 
18
- **Your always-on AI assistant β€” free, no server needed.** HuggingClaw runs [OpenClaw](https://openclaw.ai) on HuggingFace Spaces, giving you a 24/7 AI chat assistant Telegram. It works with *any* large language model (LLM) – Claude, ChatGPT, Gemini, etc. – and even supports custom models via [OpenRouter](https://openrouter.ai). Deploy in minutes on the free HF Spaces tier (2 vCPU, 16GB RAM, 50GB) with automatic workspace backup to a HuggingFace Dataset so your chat history and settings persist across restarts.
19
 
20
  ## Table of Contents
21
 
@@ -23,7 +24,10 @@ license: mit
23
  - [πŸŽ₯ Video Tutorial](#-video-tutorial)
24
  - [πŸš€ Quick Start](#-quick-start)
25
  - [πŸ“± Telegram Setup *(Optional)*](#-telegram-setup-optional)
 
26
  - [πŸ’Ύ Workspace Backup *(Optional)*](#-workspace-backup-optional)
 
 
27
  - [βš™οΈ Full Configuration Reference](#-full-configuration-reference)
28
  - [πŸ€– LLM Providers](#-llm-providers)
29
  - [πŸ’» Local Development](#-local-development)
@@ -42,7 +46,9 @@ license: mit
42
  - 🐳 **Fast Builds:** Uses a pre-built OpenClaw Docker image to deploy in minutes.
43
  - πŸ’Ύ **Workspace Backup:** Chats and settings sync to a private HF Dataset via the `huggingface_hub` (Git fallback), preserving data automatically.
44
  - πŸ’“ **Always-On:** Built-in keep-alive pings prevent the HF Space from sleeping, so the assistant is always online.
45
- - πŸ‘₯ **Multi-User Telegram:** Configure one or more user IDs to control who can message the bot.
 
 
46
  - πŸ” **Flexible Auth:** Secure the Control UI with either a gateway token or password.
47
  - 🏠 **100% HF-Native:** Runs entirely on HuggingFace’s free infrastructure (2 vCPU, 16GB RAM).
48
 
@@ -69,6 +75,8 @@ Navigate to your new Space's **Settings**, scroll down to the **Variables and se
69
  > [!TIP]
70
  > HuggingClaw is completely flexible! You only need these three secrets to get started. You can set other secrets later.
71
 
 
 
72
  ### Step 3: Deploy & Run
73
 
74
  That's it! The Space will build the container and start up automatically. You can monitor the build process in the **Logs** tab.
@@ -86,6 +94,14 @@ To chat via Telegram:
86
 
87
  After restarting, the bot should appear online on Telegram.
88
 
 
 
 
 
 
 
 
 
89
  ## πŸ’Ύ Workspace Backup *(Optional)*
90
 
91
  For persistent chat history and configuration:
@@ -95,73 +111,87 @@ For persistent chat history and configuration:
95
 
96
  Optionally set `BACKUP_DATASET_NAME` (default: `huggingclaw-backup`) to choose the HF Dataset name. On first run, HuggingClaw will create (or use) the private Dataset repo `HF_USERNAME/SPACE-backup`, then restore your workspace on startup and sync changes every 10 minutes. The workspace is also saved on graceful shutdown. This ensures your data survives restarts.
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  ## βš™οΈ Full Configuration Reference
99
 
100
- See `.env.example` for complete settings. Key environment variables:
101
 
102
  ### Core
103
 
104
- | Variable | Description |
105
- |----------------|---------------------------------------|
106
- | `LLM_API_KEY` | LLM provider API key (e.g. OpenAI, Anthropic, etc.) |
107
- | `LLM_MODEL` | Model ID (prefix `<provider>/`, auto-detected from prefix) |
108
- | `GATEWAY_TOKEN`| Gateway token for Control UI access (required) |
109
 
110
  ### Background Services
111
 
112
- | Variable | Default | Description |
113
- |----------------------|---------|-----------------------------------|
114
- | `KEEP_ALIVE_INTERVAL`| `300` | Self-ping interval in seconds (0 to disable) |
115
- | `SYNC_INTERVAL` | `600` | Workspace sync interval (sec.) to HF Dataset |
116
 
117
  ### Security
118
 
119
- | Variable | Description |
120
- |---------------------|---------------------------------------------------|
121
- | `OPENCLAW_PASSWORD` | (optional) Enable simple password auth instead of token |
122
- | `TRUSTED_PROXIES` | Comma-separated IPs of HF proxies (for reverse-proxy fixes) |
123
- | `ALLOWED_ORIGINS` | Comma-separated allowed origins for Control UI (e.g. `https://your-space.hf.space`) |
124
 
125
  ### Workspace Backup
126
 
127
- | Variable | Default | Description |
128
- |---------------------|-------------------|------------------------------------------------|
129
- | `HF_USERNAME` | β€” | Your HuggingFace username |
130
- | `HF_TOKEN` | β€” | HF token with write access (for backups) |
131
- | `BACKUP_DATASET_NAME`| `huggingclaw-backup`| Dataset name for backup repo (auto-created) |
132
- | `WORKSPACE_GIT_USER`| `openclaw@example.com` | Git commit email for workspace commits |
133
- | `WORKSPACE_GIT_NAME`| `OpenClaw Bot` | Git commit name for workspace commits |
134
 
135
  ### Advanced
136
 
137
- | Variable | Default | Description |
138
- |---------------------|-----------|-------------------------------------|
139
- | `OPENCLAW_VERSION` | `latest` | Pin a specific OpenClaw version |
140
- | `HEALTH_PORT` | `7861` | Internal health endpoint port |
141
 
142
  ## πŸ€– LLM Providers
143
 
144
  HuggingClaw supports **all providers** from OpenClaw. Set `LLM_MODEL=<provider/model>` and the provider is auto-detected. For example:
145
 
146
- | Provider | Prefix | Example Model | API Key Source |
147
- |---------------|---------------|--------------------------------|-----------------------------------|
148
- | **Anthropic** | `anthropic/` | `anthropic/claude-sonnet-4-6` | [Anthropic Console](https://console.anthropic.com/) |
149
- | **OpenAI** | `openai/` | `openai/gpt-5.4` | [OpenAI Platform](https://platform.openai.com/) |
150
- | **Google** | `google/` | `google/gemini-2.5-flash` | [AI Studio](https://ai.google.dev/) |
151
- | **DeepSeek** | `deepseek/` | `deepseek/deepseek-v3.2` | [DeepSeek](https://platform.deepseek.com) |
152
- | **xAI (Grok)**| `xai/` | `xai/grok-4` | [xAI](https://console.x.ai) |
153
- | **Mistral** | `mistral/` | `mistral/mistral-large-latest` | [Mistral Console](https://console.mistral.ai) |
154
- | **Moonshot** | `moonshot/` | `moonshot/kimi-k2.5` | [Moonshot](https://platform.moonshot.cn) |
155
- | **Cohere** | `cohere/` | `cohere/command-a` | [Cohere Dashboard](https://dashboard.cohere.com) |
156
- | **Groq** | `groq/` | `groq/mixtral-8x7b-32768` | [Groq](https://console.groq.com) |
157
- | **MiniMax** | `minimax/` | `minimax/minimax-m2.7` | [MiniMax](https://platform.minimax.io) |
158
- | **NVIDIA** | `nvidia/` | `nvidia/nemotron-3-super-120b-a12b` | [NVIDIA API](https://api.nvidia.com) |
159
- | **Z.ai (GLM)**| `zai/` | `zai/glm-5` | [Z.ai](https://z.ai) |
160
- | **Volcengine**| `volcengine/` | `volcengine/doubao-seed-1-8-251228` | [Volcengine](https://www.volcengine.com) |
161
- | **HuggingFace**| `huggingface/`| `huggingface/deepseek-ai/DeepSeek-R1` | [HF Tokens](https://huggingface.co/settings/tokens) |
162
- | **OpenCode Zen**| `opencode/` | `opencode/claude-opus-4-6` | [OpenCode.ai](https://opencode.ai/auth) |
163
- | **OpenCode Go**| `opencode-go/`| `opencode-go/kimi-k2.5` | [OpenCode.ai](https://opencode.ai/auth) |
164
- | **Kilo Gateway**| `kilocode/` | `kilocode/anthropic/claude-opus-4.6` | [Kilo.ai](https://kilo.ai) |
165
 
166
  ### OpenRouter – 200+ Models with One Key
167
 
@@ -197,8 +227,8 @@ cp .env.example .env
197
  **With Docker:**
198
 
199
  ```bash
200
- docker build -t huggingclaw .
201
- docker run -p 7860:7860 --env-file .env huggingclaw
202
  ```
203
 
204
  **Without Docker:**
@@ -260,7 +290,7 @@ HuggingClaw keeps the Space awake without external cron tools:
260
  - **Space keeps sleeping:** Check logs for `Keep-alive` messages. Ensure `KEEP_ALIVE_INTERVAL` isn’t set to `0`.
261
  - **Auth errors / proxy:** If you see reverse-proxy auth errors, add the logged IPs under `TRUSTED_PROXIES` (from logs `remote=x.x.x.x`).
262
  - **UI blocked (CORS):** Set `ALLOWED_ORIGINS=https://your-space-name.hf.space`.
263
- - **Version mismatches:** You can pin a specific OpenClaw version via the `OPENCLAW_VERSION` secret.
264
 
265
  ## πŸ“š Links
266
 
 
4
  colorFrom: blue
5
  colorTo: purple
6
  sdk: docker
7
+ app_port: 7861
8
+ base_path: /dashboard
9
  pinned: true
10
  license: mit
11
  ---
 
16
  [![HF Space](https://img.shields.io/badge/πŸ€—%20HuggingFace-Space-blue?style=flat-square)](https://huggingface.co/spaces)
17
  [![OpenClaw](https://img.shields.io/badge/OpenClaw-Gateway-red?style=flat-square)](https://github.com/openclaw/openclaw)
18
 
19
+ **Your always-on AI assistant β€” free, no server needed.** HuggingClaw runs [OpenClaw](https://openclaw.ai) on HuggingFace Spaces, giving you a 24/7 AI chat assistant on Telegram and WhatsApp. It works with *any* large language model (LLM) – Claude, ChatGPT, Gemini, etc. – and even supports custom models via [OpenRouter](https://openrouter.ai). Deploy in minutes on the free HF Spaces tier (2 vCPU, 16GB RAM, 50GB) with automatic workspace backup to a HuggingFace Dataset so your chat history and settings persist across restarts.
20
 
21
  ## Table of Contents
22
 
 
24
  - [πŸŽ₯ Video Tutorial](#-video-tutorial)
25
  - [πŸš€ Quick Start](#-quick-start)
26
  - [πŸ“± Telegram Setup *(Optional)*](#-telegram-setup-optional)
27
+ - [πŸ’¬ WhatsApp Setup *(Optional)*](#-whatsapp-setup-optional)
28
  - [πŸ’Ύ Workspace Backup *(Optional)*](#-workspace-backup-optional)
29
+ - [πŸ“Š Dashboard & Monitoring](#-dashboard--monitoring)
30
+ - [πŸ”” Webhooks *(Optional)*](#-webhooks-optional)
31
  - [βš™οΈ Full Configuration Reference](#-full-configuration-reference)
32
  - [πŸ€– LLM Providers](#-llm-providers)
33
  - [πŸ’» Local Development](#-local-development)
 
46
  - 🐳 **Fast Builds:** Uses a pre-built OpenClaw Docker image to deploy in minutes.
47
  - πŸ’Ύ **Workspace Backup:** Chats and settings sync to a private HF Dataset via the `huggingface_hub` (Git fallback), preserving data automatically.
48
  - πŸ’“ **Always-On:** Built-in keep-alive pings prevent the HF Space from sleeping, so the assistant is always online.
49
+ - πŸ‘₯ **Multi-User Messaging:** Support for Telegram (multi-user) and WhatsApp (pairing).
50
+ - πŸ“Š **Visual Dashboard:** Beautiful Web UI to monitor uptime, sync status, and active models.
51
+ - πŸ”” **Webhooks:** Get notified on restarts or backup failures via standard webhooks.
52
  - πŸ” **Flexible Auth:** Secure the Control UI with either a gateway token or password.
53
  - 🏠 **100% HF-Native:** Runs entirely on HuggingFace’s free infrastructure (2 vCPU, 16GB RAM).
54
 
 
75
  > [!TIP]
76
  > HuggingClaw is completely flexible! You only need these three secrets to get started. You can set other secrets later.
77
 
78
+ Optional: if you want to pin a specific OpenClaw release instead of `latest`, add `OPENCLAW_VERSION` under **Variables** in your Space settings. For Docker Spaces, HF passes Variables as build args during image build, so this should be a Variable, not a Secret.
79
+
80
  ### Step 3: Deploy & Run
81
 
82
  That's it! The Space will build the container and start up automatically. You can monitor the build process in the **Logs** tab.
 
94
 
95
  After restarting, the bot should appear online on Telegram.
96
 
97
+ ## πŸ’¬ WhatsApp Setup *(Optional)*
98
+
99
+ To use WhatsApp:
100
+
101
+ 1. Visit your Space URL. It opens the dashboard at `/dashboard` by default, then click **Open Control UI**.
102
+ 2. In the Control UI, go to **Channels** β†’ **WhatsApp** β†’ **Login**.
103
+ 3. Scan the QR code with your phone. πŸ“±
104
+
105
  ## πŸ’Ύ Workspace Backup *(Optional)*
106
 
107
  For persistent chat history and configuration:
 
111
 
112
  Optionally set `BACKUP_DATASET_NAME` (default: `huggingclaw-backup`) to choose the HF Dataset name. On first run, HuggingClaw will create (or use) the private Dataset repo `HF_USERNAME/SPACE-backup`, then restore your workspace on startup and sync changes every 10 minutes. The workspace is also saved on graceful shutdown. This ensures your data survives restarts.
113
 
114
+ ## πŸ“Š Dashboard & Monitoring
115
+
116
+ HuggingClaw now features a built-in dashboard at `/dashboard`, served from the same public HF Space URL as the Control UI:
117
+
118
+ - **Uptime Tracking:** Real-time uptime monitoring.
119
+ - **Sync Status:** Visual indicators for workspace backup operations.
120
+ - **Model Info:** See which LLM is currently powering your assistant.
121
+
122
+ ## πŸ”” Webhooks *(Optional)*
123
+
124
+ Get notified when your Space restarts or if a backup fails:
125
+
126
+ - Set `WEBHOOK_URL` to your endpoint (e.g., Make.com, IFTTT, Discord Webhook).
127
+ - HuggingClaw sends a POST JSON payload with event details.
128
+
129
  ## βš™οΈ Full Configuration Reference
130
 
131
+ See `.env.example` for runtime settings. Key configuration values:
132
 
133
  ### Core
134
 
135
+ | Variable | Description |
136
+ |-----------------|-------------------------------------------------------------|
137
+ | `LLM_API_KEY` | LLM provider API key (e.g. OpenAI, Anthropic, etc.) |
138
+ | `LLM_MODEL` | Model ID (prefix `<provider>/`, auto-detected from prefix) |
139
+ | `GATEWAY_TOKEN` | Gateway token for Control UI access (required) |
140
 
141
  ### Background Services
142
 
143
+ | Variable | Default | Description |
144
+ |-----------------------|---------|---------------------------------------------|
145
+ | `KEEP_ALIVE_INTERVAL` | `300` | Self-ping interval in seconds (0 to disable)|
146
+ | `SYNC_INTERVAL` | `600` | Workspace sync interval (sec.) to HF Dataset|
147
 
148
  ### Security
149
 
150
+ | Variable | Description |
151
+ |----------------------|---------------------------------------------------------|
152
+ | `OPENCLAW_PASSWORD` | (optional) Enable simple password auth instead of token |
153
+ | `TRUSTED_PROXIES` | Comma-separated IPs of HF proxies |
154
+ | `ALLOWED_ORIGINS` | Comma-separated allowed origins for Control UI |
155
 
156
  ### Workspace Backup
157
 
158
+ | Variable | Default | Description |
159
+ |-----------------------|----------------------|--------------------------------------|
160
+ | `HF_USERNAME` | β€” | Your HuggingFace username |
161
+ | `HF_TOKEN` | β€” | HF token with write access |
162
+ | `BACKUP_DATASET_NAME` | `huggingclaw-backup` | Dataset name for backup repo |
163
+ | `WORKSPACE_GIT_USER` | `openclaw@example.com`| Git commit email for workspace sync |
164
+ | `WORKSPACE_GIT_NAME` | `OpenClaw Bot` | Git commit name for workspace sync |
165
 
166
  ### Advanced
167
 
168
+ | Variable | Default | Description |
169
+ |--------------------|----------|-------------------------------------|
170
+ | `OPENCLAW_VERSION` | `latest` | Build-time pin for the OpenClaw image tag |
 
171
 
172
  ## πŸ€– LLM Providers
173
 
174
  HuggingClaw supports **all providers** from OpenClaw. Set `LLM_MODEL=<provider/model>` and the provider is auto-detected. For example:
175
 
176
+ | Provider | Prefix | Example Model | API Key Source |
177
+ |------------------|-----------------|---------------------------------------|------------------------------------------------------|
178
+ | **Anthropic** | `anthropic/` | `anthropic/claude-sonnet-4-6` | [Anthropic Console](https://console.anthropic.com/) |
179
+ | **OpenAI** | `openai/` | `openai/gpt-5.4` | [OpenAI Platform](https://platform.openai.com/) |
180
+ | **Google** | `google/` | `google/gemini-2.5-flash` | [AI Studio](https://ai.google.dev/) |
181
+ | **DeepSeek** | `deepseek/` | `deepseek/deepseek-v3.2` | [DeepSeek](https://platform.deepseek.com) |
182
+ | **xAI (Grok)** | `xai/` | `xai/grok-4` | [xAI](https://console.x.ai) |
183
+ | **Mistral** | `mistral/` | `mistral/mistral-large-latest` | [Mistral Console](https://console.mistral.ai) |
184
+ | **Moonshot** | `moonshot/` | `moonshot/kimi-k2.5` | [Moonshot](https://platform.moonshot.cn) |
185
+ | **Cohere** | `cohere/` | `cohere/command-a` | [Cohere Dashboard](https://dashboard.cohere.com) |
186
+ | **Groq** | `groq/` | `groq/mixtral-8x7b-32768` | [Groq](https://console.groq.com) |
187
+ | **MiniMax** | `minimax/` | `minimax/minimax-m2.7` | [MiniMax](https://platform.minimax.io) |
188
+ | **NVIDIA** | `nvidia/` | `nvidia/nemotron-3-super-120b-a12b` | [NVIDIA API](https://api.nvidia.com) |
189
+ | **Z.ai (GLM)** | `zai/` | `zai/glm-5` | [Z.ai](https://z.ai) |
190
+ | **Volcengine** | `volcengine/` | `volcengine/doubao-seed-1-8-251228` | [Volcengine](https://www.volcengine.com) |
191
+ | **HuggingFace** | `huggingface/` | `huggingface/deepseek-ai/DeepSeek-R1` | [HF Tokens](https://huggingface.co/settings/tokens) |
192
+ | **OpenCode Zen** | `opencode/` | `opencode/claude-opus-4-6` | [OpenCode.ai](https://opencode.ai/auth) |
193
+ | **OpenCode Go** | `opencode-go/` | `opencode-go/kimi-k2.5` | [OpenCode.ai](https://opencode.ai/auth) |
194
+ | **Kilo Gateway** | `kilocode/` | `kilocode/anthropic/claude-opus-4.6` | [Kilo.ai](https://kilo.ai) |
195
 
196
  ### OpenRouter – 200+ Models with One Key
197
 
 
227
  **With Docker:**
228
 
229
  ```bash
230
+ docker build --build-arg OPENCLAW_VERSION=latest -t huggingclaw .
231
+ docker run -p 7861:7861 --env-file .env huggingclaw
232
  ```
233
 
234
  **Without Docker:**
 
290
  - **Space keeps sleeping:** Check logs for `Keep-alive` messages. Ensure `KEEP_ALIVE_INTERVAL` isn’t set to `0`.
291
  - **Auth errors / proxy:** If you see reverse-proxy auth errors, add the logged IPs under `TRUSTED_PROXIES` (from logs `remote=x.x.x.x`).
292
  - **UI blocked (CORS):** Set `ALLOWED_ORIGINS=https://your-space-name.hf.space`.
293
+ - **Version mismatches:** Pin a specific OpenClaw build with the `OPENCLAW_VERSION` Variable in HF Spaces, or `--build-arg OPENCLAW_VERSION=...` locally.
294
 
295
  ## πŸ“š Links
296
 
dns-fix.js CHANGED
@@ -1,19 +1,108 @@
1
- // Fix HF Spaces DNS: internal resolver can't resolve discord.com / api.telegram.org
2
- // Override dns.lookup (used by http/https) to use Google/Cloudflare DNS
3
- const dns = require('dns');
4
- const { Resolver } = dns;
5
- const resolver = new Resolver();
6
- resolver.setServers(['8.8.8.8', '1.1.1.1']);
 
 
 
 
 
 
7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  const origLookup = dns.lookup;
9
- dns.lookup = function(hostname, options, callback) {
10
- if (typeof options === 'function') { callback = options; options = { family: 0 }; }
11
- resolver.resolve4(hostname, (err, addresses) => {
12
- if (err || !addresses || !addresses.length) return origLookup.call(dns, hostname, options, callback);
13
- if (options && options.all) {
14
- callback(null, addresses.map(a => ({ address: a, family: 4 })));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  } else {
16
- callback(null, addresses[0], 4);
 
17
  }
18
  });
19
  };
 
1
+ /**
2
+ * DNS fix preload script for HF Spaces.
3
+ *
4
+ * Patches Node.js dns.lookup to:
5
+ * 1. Try system DNS first
6
+ * 2. Fall back to DNS-over-HTTPS (Cloudflare) if system DNS fails
7
+ * (This is needed because HF Spaces intercepts/blocks some domains like
8
+ * WhatsApp web or Telegram API via standard UDP DNS).
9
+ *
10
+ * Loaded via: NODE_OPTIONS="--require /opt/dns-fix.js"
11
+ */
12
+ "use strict";
13
 
14
+ const dns = require("dns");
15
+ const https = require("https");
16
+
17
+ // In-memory cache for runtime DoH resolutions
18
+ const runtimeCache = new Map(); // hostname -> { ip, expiry }
19
+
20
+ // DNS-over-HTTPS resolver
21
+ function dohResolve(hostname, callback) {
22
+ // Check runtime cache
23
+ const cached = runtimeCache.get(hostname);
24
+ if (cached && cached.expiry > Date.now()) {
25
+ return callback(null, cached.ip);
26
+ }
27
+
28
+ const url = `https://1.1.1.1/dns-query?name=${encodeURIComponent(hostname)}&type=A`;
29
+ const req = https.get(
30
+ url,
31
+ { headers: { Accept: "application/dns-json" }, timeout: 15000 },
32
+ (res) => {
33
+ let body = "";
34
+ res.on("data", (c) => (body += c));
35
+ res.on("end", () => {
36
+ try {
37
+ const data = JSON.parse(body);
38
+ const aRecords = (data.Answer || []).filter((a) => a.type === 1);
39
+ if (aRecords.length === 0) {
40
+ return callback(new Error(`DoH: no A record for ${hostname}`));
41
+ }
42
+ const ip = aRecords[0].data;
43
+ const ttl = Math.max((aRecords[0].TTL || 300) * 1000, 60000);
44
+ runtimeCache.set(hostname, { ip, expiry: Date.now() + ttl });
45
+ callback(null, ip);
46
+ } catch (e) {
47
+ callback(new Error(`DoH parse error: ${e.message}`));
48
+ }
49
+ });
50
+ }
51
+ );
52
+ req.on("error", (e) => callback(new Error(`DoH request failed: ${e.message}`)));
53
+ req.on("timeout", () => {
54
+ req.destroy();
55
+ callback(new Error("DoH request timed out"));
56
+ });
57
+ }
58
+
59
+ // Monkey-patch dns.lookup
60
  const origLookup = dns.lookup;
61
+
62
+ dns.lookup = function patchedLookup(hostname, options, callback) {
63
+ // Normalize arguments (options is optional, can be number or object)
64
+ if (typeof options === "function") {
65
+ callback = options;
66
+ options = {};
67
+ }
68
+ if (typeof options === "number") {
69
+ options = { family: options };
70
+ }
71
+ options = options || {};
72
+
73
+ // Skip patching for localhost, IPs, and internal domains
74
+ if (
75
+ !hostname ||
76
+ hostname === "localhost" ||
77
+ hostname === "0.0.0.0" ||
78
+ hostname === "127.0.0.1" ||
79
+ hostname === "::1" ||
80
+ /^\d+\.\d+\.\d+\.\d+$/.test(hostname) ||
81
+ /^::/.test(hostname)
82
+ ) {
83
+ return origLookup.call(dns, hostname, options, callback);
84
+ }
85
+
86
+ // 1) Try system DNS first
87
+ origLookup.call(dns, hostname, options, (err, address, family) => {
88
+ if (!err && address) {
89
+ return callback(null, address, family);
90
+ }
91
+
92
+ // 2) System DNS failed with ENOTFOUND or EAI_AGAIN β€” fall back to DoH
93
+ if (err && (err.code === "ENOTFOUND" || err.code === "EAI_AGAIN")) {
94
+ dohResolve(hostname, (dohErr, ip) => {
95
+ if (dohErr || !ip) {
96
+ return callback(err); // Return original error
97
+ }
98
+ if (options.all) {
99
+ return callback(null, [{ address: ip, family: 4 }]);
100
+ }
101
+ callback(null, ip, 4);
102
+ });
103
  } else {
104
+ // Other DNS errors β€” pass through
105
+ callback(err, address, family);
106
  }
107
  });
108
  };
health-server.js CHANGED
@@ -1,27 +1,475 @@
1
- // Lightweight health endpoint on port 7861
2
- // OpenClaw runs on 7860, this runs alongside it
3
- // Returns 200 OK for keep-alive pings and external monitoring
4
- const http = require('http');
5
 
6
- const PORT = process.env.HEALTH_PORT || 7861;
 
 
7
  const startTime = Date.now();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
  const server = http.createServer((req, res) => {
10
- if (req.url === '/health' || req.url === '/') {
11
- const uptime = Math.floor((Date.now() - startTime) / 1000);
12
- res.writeHead(200, { 'Content-Type': 'application/json' });
13
- res.end(JSON.stringify({
14
- status: 'ok',
15
- uptime: uptime,
16
- uptimeHuman: `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m`,
17
- timestamp: new Date().toISOString()
18
- }));
19
- } else {
20
- res.writeHead(404);
21
- res.end();
 
 
 
22
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  });
24
 
25
- server.listen(PORT, '0.0.0.0', () => {
26
- console.log(`πŸ₯ Health server listening on port ${PORT}`);
 
 
27
  });
 
1
+ // Single public entrypoint for HF Spaces: local dashboard + reverse proxy to OpenClaw.
2
+ const http = require("http");
3
+ const fs = require("fs");
4
+ const net = require("net");
5
 
6
+ const PORT = 7861;
7
+ const GATEWAY_PORT = 7860;
8
+ const GATEWAY_HOST = "127.0.0.1";
9
  const startTime = Date.now();
10
+ const LLM_MODEL = process.env.LLM_MODEL || "Not Set";
11
+ const TELEGRAM_ENABLED = !!process.env.TELEGRAM_BOT_TOKEN;
12
+
13
+ function getPathname(url) {
14
+ try {
15
+ return new URL(url, "http://localhost").pathname;
16
+ } catch {
17
+ return "/";
18
+ }
19
+ }
20
+
21
+ function isDashboardRoute(pathname) {
22
+ return pathname === "/dashboard" || pathname === "/dashboard/";
23
+ }
24
+
25
+ function isLocalRoute(pathname) {
26
+ return (
27
+ pathname === "/health" ||
28
+ pathname === "/status" ||
29
+ isDashboardRoute(pathname)
30
+ );
31
+ }
32
+
33
+ function appendForwarded(existingValue, nextValue) {
34
+ const cleanNext = nextValue || "";
35
+ if (!existingValue) return cleanNext;
36
+ if (Array.isArray(existingValue))
37
+ return `${existingValue.join(", ")}, ${cleanNext}`;
38
+ return `${existingValue}, ${cleanNext}`;
39
+ }
40
+
41
+ function buildProxyHeaders(headers, remoteAddress) {
42
+ return {
43
+ ...headers,
44
+ host: headers.host || `${GATEWAY_HOST}:${GATEWAY_PORT}`,
45
+ "x-forwarded-for": appendForwarded(
46
+ headers["x-forwarded-for"],
47
+ remoteAddress,
48
+ ),
49
+ "x-forwarded-host": headers["x-forwarded-host"] || headers.host || "",
50
+ "x-forwarded-proto": headers["x-forwarded-proto"] || "https",
51
+ };
52
+ }
53
+
54
+ function readSyncStatus() {
55
+ try {
56
+ if (fs.existsSync("/tmp/sync-status.json")) {
57
+ return JSON.parse(fs.readFileSync("/tmp/sync-status.json", "utf8"));
58
+ }
59
+ } catch {}
60
+ return { status: "unknown", message: "No sync data yet" };
61
+ }
62
+
63
+ function renderDashboard() {
64
+ return `
65
+ <!DOCTYPE html>
66
+ <html lang="en">
67
+ <head>
68
+ <meta charset="UTF-8">
69
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
70
+ <title>HuggingClaw Dashboard</title>
71
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600&display=swap" rel="stylesheet">
72
+ <style>
73
+ :root {
74
+ --bg: #0f172a;
75
+ --card-bg: rgba(30, 41, 59, 0.7);
76
+ --accent: linear-gradient(135deg, #3b82f6, #8b5cf6);
77
+ --text: #f8fafc;
78
+ --text-dim: #94a3b8;
79
+ --success: #10b981;
80
+ --error: #ef4444;
81
+ --warning: #f59e0b;
82
+ }
83
+
84
+ * { box-sizing: border-box; margin: 0; padding: 0; }
85
+
86
+ body {
87
+ font-family: 'Outfit', sans-serif;
88
+ background-color: var(--bg);
89
+ color: var(--text);
90
+ display: flex;
91
+ justify-content: center;
92
+ align-items: center;
93
+ min-height: 100vh;
94
+ overflow: hidden;
95
+ background-image:
96
+ radial-gradient(at 0% 0%, rgba(59, 130, 246, 0.15) 0px, transparent 50%),
97
+ radial-gradient(at 100% 0%, rgba(139, 92, 246, 0.15) 0px, transparent 50%);
98
+ }
99
+
100
+ .dashboard {
101
+ width: 90%;
102
+ max-width: 600px;
103
+ background: var(--card-bg);
104
+ backdrop-filter: blur(12px);
105
+ border: 1px solid rgba(255, 255, 255, 0.1);
106
+ border-radius: 24px;
107
+ padding: 40px;
108
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
109
+ animation: fadeIn 0.8s ease-out;
110
+ }
111
+
112
+ @keyframes fadeIn {
113
+ from { opacity: 0; transform: translateY(20px); }
114
+ to { opacity: 1; transform: translateY(0); }
115
+ }
116
+
117
+ header {
118
+ text-align: center;
119
+ margin-bottom: 40px;
120
+ }
121
+
122
+ h1 {
123
+ font-size: 2.5rem;
124
+ margin-bottom: 8px;
125
+ background: var(--accent);
126
+ -webkit-background-clip: text;
127
+ -webkit-text-fill-color: transparent;
128
+ font-weight: 600;
129
+ }
130
+
131
+ .subtitle {
132
+ color: var(--text-dim);
133
+ font-size: 0.9rem;
134
+ letter-spacing: 1px;
135
+ text-transform: uppercase;
136
+ }
137
+
138
+ .stats-grid {
139
+ display: grid;
140
+ grid-template-columns: repeat(2, 1fr);
141
+ gap: 20px;
142
+ margin-bottom: 30px;
143
+ }
144
+
145
+ .stat-card {
146
+ background: rgba(255, 255, 255, 0.03);
147
+ border: 1px solid rgba(255, 255, 255, 0.05);
148
+ padding: 20px;
149
+ border-radius: 16px;
150
+ transition: transform 0.3s ease, border-color 0.3s ease;
151
+ }
152
+
153
+ .stat-card:hover {
154
+ transform: translateY(-5px);
155
+ border-color: rgba(59, 130, 246, 0.3);
156
+ }
157
+
158
+ .stat-label {
159
+ color: var(--text-dim);
160
+ font-size: 0.75rem;
161
+ text-transform: uppercase;
162
+ margin-bottom: 8px;
163
+ display: block;
164
+ }
165
+
166
+ .stat-value {
167
+ font-size: 1.1rem;
168
+ font-weight: 600;
169
+ word-break: break-all;
170
+ }
171
+
172
+ .stat-btn {
173
+ grid-column: span 2;
174
+ background: var(--accent);
175
+ color: #fff;
176
+ padding: 16px;
177
+ border-radius: 16px;
178
+ text-align: center;
179
+ text-decoration: none;
180
+ font-weight: 600;
181
+ margin-top: 10px;
182
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
183
+ box-shadow: 0 10px 20px -5px rgba(59, 130, 246, 0.4);
184
+ }
185
+
186
+ .stat-btn:hover {
187
+ transform: scale(1.02);
188
+ box-shadow: 0 15px 30px -5px rgba(59, 130, 246, 0.6);
189
+ }
190
+
191
+ .status-badge {
192
+ display: inline-flex;
193
+ align-items: center;
194
+ gap: 6px;
195
+ padding: 4px 12px;
196
+ border-radius: 20px;
197
+ font-size: 0.8rem;
198
+ font-weight: 600;
199
+ }
200
+
201
+ .status-online { background: rgba(16, 185, 129, 0.1); color: var(--success); }
202
+ .status-offline { background: rgba(239, 68, 68, 0.1); color: var(--error); }
203
+ .status-syncing { background: rgba(59, 130, 246, 0.1); color: #3b82f6; }
204
+
205
+ .pulse {
206
+ width: 8px;
207
+ height: 8px;
208
+ border-radius: 50%;
209
+ background: currentColor;
210
+ box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
211
+ animation: pulse 2s infinite;
212
+ }
213
+
214
+ @keyframes pulse {
215
+ 0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); }
216
+ 70% { transform: scale(1); box-shadow: 0 0 0 10px rgba(16, 185, 129, 0); }
217
+ 100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
218
+ }
219
+
220
+ .footer {
221
+ text-align: center;
222
+ color: var(--text-dim);
223
+ font-size: 0.8rem;
224
+ margin-top: 20px;
225
+ }
226
+
227
+ .sync-info {
228
+ background: rgba(255, 255, 255, 0.02);
229
+ padding: 15px;
230
+ border-radius: 12px;
231
+ font-size: 0.85rem;
232
+ color: var(--text-dim);
233
+ margin-top: 10px;
234
+ }
235
+
236
+ #sync-msg { color: var(--text); display: block; margin-top: 4px; }
237
+ </style>
238
+ </head>
239
+ <body>
240
+ <div class="dashboard">
241
+ <header>
242
+ <h1>🦞 HuggingClaw</h1>
243
+ <p class="subtitle">Space Dashboard</p>
244
+ </header>
245
+
246
+ <div class="stats-grid">
247
+ <div class="stat-card">
248
+ <span class="stat-label">Model</span>
249
+ <span class="stat-value" id="model-id">Loading...</span>
250
+ </div>
251
+ <div class="stat-card">
252
+ <span class="stat-label">Uptime</span>
253
+ <span class="stat-value" id="uptime">Loading...</span>
254
+ </div>
255
+ <div class="stat-card">
256
+ <span class="stat-label">WhatsApp</span>
257
+ <span id="wa-status">Loading...</span>
258
+ </div>
259
+ <div class="stat-card">
260
+ <span class="stat-label">Telegram</span>
261
+ <span id="tg-status">Loading...</span>
262
+ </div>
263
+ <a href="/" class="stat-btn">Open Control UI</a>
264
+ </div>
265
+
266
+ <div class="stat-card" style="width: 100%;">
267
+ <span class="stat-label">Workspace Sync Status</span>
268
+ <div id="sync-badge-container"></div>
269
+ <div class="sync-info">
270
+ Last Sync Activity: <span id="sync-time">Never</span>
271
+ <span id="sync-msg">Initializing synchronization...</span>
272
+ </div>
273
+ </div>
274
+
275
+ <div class="footer">
276
+ Live updates every 10s
277
+ </div>
278
+ </div>
279
+
280
+ <script>
281
+ async function updateStats() {
282
+ try {
283
+ const res = await fetch('/status');
284
+ const data = await res.json();
285
+
286
+ document.getElementById('model-id').textContent = data.model;
287
+ document.getElementById('uptime').textContent = data.uptime;
288
+
289
+ document.getElementById('wa-status').innerHTML = data.whatsapp
290
+ ? '<div class="status-badge status-online"><div class="pulse"></div>Active</div>'
291
+ : '<div class="status-badge status-offline">Disabled</div>';
292
+
293
+ document.getElementById('tg-status').innerHTML = data.telegram
294
+ ? '<div class="status-badge status-online"><div class="pulse"></div>Active</div>'
295
+ : '<div class="status-badge status-offline">Disabled</div>';
296
+
297
+ const syncData = data.sync;
298
+ let badgeClass = 'status-offline';
299
+ let pulseHtml = '';
300
+
301
+ if (syncData.status === 'success') {
302
+ badgeClass = 'status-online';
303
+ pulseHtml = '<div class="pulse"></div>';
304
+ } else if (syncData.status === 'syncing') {
305
+ badgeClass = 'status-syncing';
306
+ pulseHtml = '<div class="pulse" style="background:#3b82f6"></div>';
307
+ }
308
+
309
+ document.getElementById('sync-badge-container').innerHTML =
310
+ '<div class="status-badge ' + badgeClass + '">' + pulseHtml + syncData.status.toUpperCase() + '</div>';
311
+
312
+ document.getElementById('sync-time').textContent = syncData.timestamp || 'Never';
313
+ document.getElementById('sync-msg').textContent = syncData.message || 'Waiting for first sync...';
314
+ } catch (e) {
315
+ console.error("Failed to fetch status", e);
316
+ }
317
+ }
318
+
319
+ updateStats();
320
+ setInterval(updateStats, 10000);
321
+ </script>
322
+ </body>
323
+ </html>
324
+ `;
325
+ }
326
+
327
+ function proxyHttp(req, res) {
328
+ const proxyReq = http.request(
329
+ {
330
+ hostname: GATEWAY_HOST,
331
+ port: GATEWAY_PORT,
332
+ method: req.method,
333
+ path: req.url,
334
+ headers: buildProxyHeaders(req.headers, req.socket.remoteAddress),
335
+ },
336
+ (proxyRes) => {
337
+ res.writeHead(proxyRes.statusCode || 502, proxyRes.headers);
338
+ proxyRes.pipe(res);
339
+ },
340
+ );
341
+
342
+ proxyReq.on("error", (error) => {
343
+ res.writeHead(502, { "Content-Type": "application/json" });
344
+ res.end(
345
+ JSON.stringify({
346
+ status: "error",
347
+ message: "Gateway unavailable",
348
+ detail: error.message,
349
+ }),
350
+ );
351
+ });
352
+
353
+ req.pipe(proxyReq);
354
+ }
355
+
356
+ function serializeUpgradeHeaders(req, remoteAddress) {
357
+ const forwardedHeaders = [];
358
+
359
+ for (let i = 0; i < req.rawHeaders.length; i += 2) {
360
+ const name = req.rawHeaders[i];
361
+ const value = req.rawHeaders[i + 1];
362
+ const lower = name.toLowerCase();
363
+
364
+ if (
365
+ lower === "x-forwarded-for" ||
366
+ lower === "x-forwarded-host" ||
367
+ lower === "x-forwarded-proto"
368
+ ) {
369
+ continue;
370
+ }
371
+
372
+ forwardedHeaders.push(`${name}: ${value}`);
373
+ }
374
+
375
+ forwardedHeaders.push(
376
+ `X-Forwarded-For: ${appendForwarded(req.headers["x-forwarded-for"], remoteAddress)}`,
377
+ );
378
+ forwardedHeaders.push(
379
+ `X-Forwarded-Host: ${req.headers["x-forwarded-host"] || req.headers.host || ""}`,
380
+ );
381
+ forwardedHeaders.push(
382
+ `X-Forwarded-Proto: ${req.headers["x-forwarded-proto"] || "https"}`,
383
+ );
384
+
385
+ return forwardedHeaders;
386
+ }
387
+
388
+ function proxyUpgrade(req, socket, head) {
389
+ const proxySocket = net.connect(GATEWAY_PORT, GATEWAY_HOST);
390
+
391
+ proxySocket.on("connect", () => {
392
+ const requestLines = [
393
+ `${req.method} ${req.url} HTTP/${req.httpVersion}`,
394
+ ...serializeUpgradeHeaders(req, req.socket.remoteAddress),
395
+ "",
396
+ "",
397
+ ];
398
+
399
+ proxySocket.write(requestLines.join("\r\n"));
400
+
401
+ if (head && head.length > 0) {
402
+ proxySocket.write(head);
403
+ }
404
+
405
+ socket.pipe(proxySocket).pipe(socket);
406
+ });
407
+
408
+ proxySocket.on("error", () => {
409
+ if (socket.writable) {
410
+ socket.write("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n");
411
+ }
412
+ socket.destroy();
413
+ });
414
+
415
+ socket.on("error", () => {
416
+ proxySocket.destroy();
417
+ });
418
+ }
419
 
420
  const server = http.createServer((req, res) => {
421
+ const pathname = getPathname(req.url || "/");
422
+ const uptime = Math.floor((Date.now() - startTime) / 1000);
423
+ const uptimeHuman = `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m`;
424
+
425
+ if (pathname === "/health") {
426
+ res.writeHead(200, { "Content-Type": "application/json" });
427
+ res.end(
428
+ JSON.stringify({
429
+ status: "ok",
430
+ uptime,
431
+ uptimeHuman,
432
+ timestamp: new Date().toISOString(),
433
+ }),
434
+ );
435
+ return;
436
  }
437
+
438
+ if (pathname === "/status") {
439
+ res.writeHead(200, { "Content-Type": "application/json" });
440
+ res.end(
441
+ JSON.stringify({
442
+ model: LLM_MODEL,
443
+ whatsapp: true,
444
+ telegram: TELEGRAM_ENABLED,
445
+ sync: readSyncStatus(),
446
+ uptime: uptimeHuman,
447
+ }),
448
+ );
449
+ return;
450
+ }
451
+
452
+ if (isDashboardRoute(pathname)) {
453
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
454
+ res.end(renderDashboard());
455
+ return;
456
+ }
457
+
458
+ proxyHttp(req, res);
459
+ });
460
+
461
+ server.on("upgrade", (req, socket, head) => {
462
+ const pathname = getPathname(req.url || "/");
463
+ if (isLocalRoute(pathname)) {
464
+ socket.destroy();
465
+ return;
466
+ }
467
+
468
+ proxyUpgrade(req, socket, head);
469
  });
470
 
471
+ server.listen(PORT, "0.0.0.0", () => {
472
+ console.log(
473
+ `Health server listening on port ${PORT}; proxying gateway traffic to ${GATEWAY_HOST}:${GATEWAY_PORT}`,
474
+ );
475
  });
iframe-fix.cjs ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * iframe-fix.cjs β€” Node.js preload script
3
+ *
4
+ * Intercepts OpenClaw's HTTP server to:
5
+ * 1. Allow iframe embedding (strip X-Frame-Options, fix CSP)
6
+ */
7
+ 'use strict';
8
+
9
+ const http = require('http');
10
+
11
+ console.log('[iframe-fix] Initialized: Allowing iframe embedding for *.hf.space and huggingface.co');
12
+
13
+ const origEmit = http.Server.prototype.emit;
14
+
15
+ http.Server.prototype.emit = function (event, ...args) {
16
+ if (event === 'request') {
17
+ const [, res] = args;
18
+
19
+ // Only intercept on the main OpenClaw server (port 7860)
20
+ const serverPort = this.address && this.address() && this.address().port;
21
+ if (serverPort && serverPort !== 7860) {
22
+ return origEmit.apply(this, [event, ...args]);
23
+ }
24
+
25
+ // Fix iframe embedding β€” must be applied BEFORE any early returns
26
+ const origWriteHead = res.writeHead;
27
+ res.writeHead = function (statusCode, ...whArgs) {
28
+ if (res.getHeader) {
29
+ // Strip X-Frame-Options so it can load in a Hugging Face Space iframe
30
+ res.removeHeader('x-frame-options');
31
+
32
+ // Update Content-Security-Policy if it contains frame-ancestors 'none'
33
+ const csp = res.getHeader('content-security-policy');
34
+ if (csp && typeof csp === 'string') {
35
+ res.setHeader('content-security-policy',
36
+ csp.replace(/frame-ancestors\s+'none'/i,
37
+ "frame-ancestors 'self' https://huggingface.co https://*.hf.space"));
38
+ }
39
+ }
40
+ return origWriteHead.apply(this, [statusCode, ...whArgs]);
41
+ };
42
+ }
43
+
44
+ return origEmit.apply(this, [event, ...args]);
45
+ };
keep-alive.sh CHANGED
@@ -18,8 +18,8 @@ if [ -z "$SPACE_HOST" ]; then
18
  exit 0
19
  fi
20
 
21
- # Ping the Space URL β€” any HTTP response (even 404) counts as activity
22
- PING_URL="https://${SPACE_HOST}"
23
 
24
  echo "πŸ’“ Keep-alive started: pinging ${PING_URL} every ${INTERVAL}s"
25
 
 
18
  exit 0
19
  fi
20
 
21
+ # Ping the health endpoint so we keep the Space warm without touching the gateway
22
+ PING_URL="https://${SPACE_HOST}/health"
23
 
24
  echo "πŸ’“ Keep-alive started: pinging ${PING_URL} every ${INTERVAL}s"
25
 
start.sh CHANGED
@@ -234,21 +234,31 @@ if [ -n "$TELEGRAM_BOT_TOKEN" ]; then
234
  fi
235
  fi
236
 
 
 
 
 
237
  # Write config
238
  echo "$CONFIG_JSON" > "/home/node/.openclaw/openclaw.json"
239
  chmod 600 /home/node/.openclaw/openclaw.json
240
 
 
 
 
 
241
  # ── Startup Summary ──
242
  echo ""
243
  echo " β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”"
244
  echo " β”‚ πŸ“‹ Configuration Summary β”‚"
245
  echo " β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€"
 
246
  printf " β”‚ %-40s β”‚\n" "Model: $LLM_MODEL"
247
  if [ -n "$TELEGRAM_BOT_TOKEN" ]; then
248
  printf " β”‚ %-40s β”‚\n" "Telegram: βœ… enabled"
249
  else
250
  printf " β”‚ %-40s β”‚\n" "Telegram: ❌ not configured"
251
  fi
 
252
  if [ -n "$HF_USERNAME" ] && [ -n "$HF_TOKEN" ]; then
253
  printf " β”‚ %-40s β”‚\n" "Backup: βœ… ${HF_USERNAME}/${BACKUP_DATASET:-huggingclaw-backup}"
254
  else
@@ -262,6 +272,7 @@ fi
262
  if [ -n "$SPACE_HOST" ]; then
263
  printf " β”‚ %-40s β”‚\n" "Keep-alive: βœ… every ${KEEP_ALIVE_INTERVAL:-300}s"
264
  printf " β”‚ %-40s β”‚\n" "Control UI: https://${SPACE_HOST}"
 
265
  else
266
  printf " β”‚ %-40s β”‚\n" "Keep-alive: ⏸️ local mode"
267
  fi
@@ -270,9 +281,20 @@ if [ -n "$HF_USERNAME" ] && [ -n "$HF_TOKEN" ]; then
270
  SYNC_STATUS="βœ… every ${SYNC_INTERVAL:-600}s"
271
  fi
272
  printf " β”‚ %-40s β”‚\n" "Auto-sync: $SYNC_STATUS"
 
 
 
273
  echo " β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜"
274
  echo ""
275
 
 
 
 
 
 
 
 
 
276
  # ── Trap SIGTERM for graceful shutdown ──
277
  graceful_shutdown() {
278
  echo ""
@@ -300,16 +322,26 @@ graceful_shutdown() {
300
  trap graceful_shutdown SIGTERM SIGINT
301
 
302
  # ── Start background services ──
 
 
303
  node /home/node/app/health-server.js &
 
 
 
304
  /home/node/app/keep-alive.sh &
 
305
 
306
- python3 /home/node/app/workspace-sync.py &
 
 
 
 
 
 
307
 
308
  # ── Launch gateway ──
309
  echo "πŸš€ Launching OpenClaw gateway on port 7860..."
310
  echo ""
311
- # Set model via environment for the gateway
312
- export LLM_MODEL="$LLM_MODEL"
313
 
314
 
315
 
 
234
  fi
235
  fi
236
 
237
+ # WhatsApp
238
+ CONFIG_JSON=$(echo "$CONFIG_JSON" | jq '.plugins.entries.whatsapp = {"enabled": true}')
239
+ CONFIG_JSON=$(echo "$CONFIG_JSON" | jq '.channels.whatsapp = {"dmPolicy": "pairing"}')
240
+
241
  # Write config
242
  echo "$CONFIG_JSON" > "/home/node/.openclaw/openclaw.json"
243
  chmod 600 /home/node/.openclaw/openclaw.json
244
 
245
+ # ── Enable Iframe Fix (Security: No Token Redirect) ──
246
+ # This Node.js preload script strips X-Frame-Options to allow HF Space embedding
247
+ export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--require /home/node/app/iframe-fix.cjs"
248
+
249
  # ── Startup Summary ──
250
  echo ""
251
  echo " β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”"
252
  echo " β”‚ πŸ“‹ Configuration Summary β”‚"
253
  echo " β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€"
254
+ printf " β”‚ %-40s β”‚\n" "OpenClaw: $OPENCLAW_VERSION"
255
  printf " β”‚ %-40s β”‚\n" "Model: $LLM_MODEL"
256
  if [ -n "$TELEGRAM_BOT_TOKEN" ]; then
257
  printf " β”‚ %-40s β”‚\n" "Telegram: βœ… enabled"
258
  else
259
  printf " β”‚ %-40s β”‚\n" "Telegram: ❌ not configured"
260
  fi
261
+ printf " β”‚ %-40s β”‚\n" "WhatsApp: βœ… enabled"
262
  if [ -n "$HF_USERNAME" ] && [ -n "$HF_TOKEN" ]; then
263
  printf " β”‚ %-40s β”‚\n" "Backup: βœ… ${HF_USERNAME}/${BACKUP_DATASET:-huggingclaw-backup}"
264
  else
 
272
  if [ -n "$SPACE_HOST" ]; then
273
  printf " β”‚ %-40s β”‚\n" "Keep-alive: βœ… every ${KEEP_ALIVE_INTERVAL:-300}s"
274
  printf " β”‚ %-40s β”‚\n" "Control UI: https://${SPACE_HOST}"
275
+ printf " β”‚ %-40s β”‚\n" "Dashboard: https://${SPACE_HOST}/dashboard"
276
  else
277
  printf " β”‚ %-40s β”‚\n" "Keep-alive: ⏸️ local mode"
278
  fi
 
281
  SYNC_STATUS="βœ… every ${SYNC_INTERVAL:-600}s"
282
  fi
283
  printf " β”‚ %-40s β”‚\n" "Auto-sync: $SYNC_STATUS"
284
+ if [ -n "$WEBHOOK_URL" ]; then
285
+ printf " β”‚ %-40s β”‚\n" "Webhooks: βœ… enabled"
286
+ fi
287
  echo " β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜"
288
  echo ""
289
 
290
+ # ── Trigger Webhook on Restart ──
291
+ if [ -n "$WEBHOOK_URL" ]; then
292
+ echo "πŸ”” Sending restart webhook..."
293
+ curl -s -X POST "$WEBHOOK_URL" \
294
+ -H "Content-Type: application/json" \
295
+ -d '{"event":"restart", "status":"success", "message":"HuggingClaw gateway has started/restarted.", "model": "'"$LLM_MODEL"'"}' >/dev/null 2>&1 &
296
+ fi
297
+
298
  # ── Trap SIGTERM for graceful shutdown ──
299
  graceful_shutdown() {
300
  echo ""
 
322
  trap graceful_shutdown SIGTERM SIGINT
323
 
324
  # ── Start background services ──
325
+ export LLM_MODEL="$LLM_MODEL"
326
+ # 10. Start Health Server & Dashboard
327
  node /home/node/app/health-server.js &
328
+ HEALTH_PID=$!
329
+
330
+ # 11. Start HF keep-alive
331
  /home/node/app/keep-alive.sh &
332
+ KEEP_ALIVE_PID=$!
333
 
334
+ # 12. Start WhatsApp Guardian (Automates pairing)
335
+ node /home/node/app/wa-guardian.js &
336
+ GUARDIAN_PID=$!
337
+ echo "πŸ›‘οΈ WhatsApp Guardian started (PID: $GUARDIAN_PID)"
338
+
339
+ # 13. Start Workspace Sync
340
+ python3 -u /home/node/app/workspace-sync.py &
341
 
342
  # ── Launch gateway ──
343
  echo "πŸš€ Launching OpenClaw gateway on port 7860..."
344
  echo ""
 
 
345
 
346
 
347
 
wa-guardian.js ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * HuggingClaw WhatsApp Guardian
3
+ *
4
+ * Automates the WhatsApp pairing process on HuggingFace Spaces.
5
+ * Handles the "515 Restart" by monitoring the channel status and
6
+ * re-applying the configuration after a successful scan.
7
+ */
8
+ "use strict";
9
+
10
+ const { WebSocket } = require('/home/node/.openclaw/openclaw-app/node_modules/ws');
11
+ const { randomUUID } = require('node:crypto');
12
+
13
+ const GATEWAY_URL = "ws://127.0.0.1:7860";
14
+ const GATEWAY_TOKEN = process.env.GATEWAY_TOKEN || "huggingclaw";
15
+ const CHECK_INTERVAL = 5000;
16
+ const WAIT_TIMEOUT = 120000;
17
+
18
+ let isWaiting = false;
19
+ let hasShownWaitMessage = false;
20
+
21
+ async function createConnection() {
22
+ return new Promise((resolve, reject) => {
23
+ const ws = new WebSocket(GATEWAY_URL);
24
+ let resolved = false;
25
+
26
+ ws.on("message", (data) => {
27
+ const msg = JSON.parse(data.toString());
28
+
29
+ if (msg.type === "event" && msg.event === "connect.challenge") {
30
+ ws.send(JSON.stringify({
31
+ type: "req",
32
+ id: randomUUID(),
33
+ method: "connect",
34
+ params: {
35
+ auth: { token: GATEWAY_TOKEN },
36
+ client: { id: "wa-guardian", platform: "linux", mode: "backend", version: "1.0.0" },
37
+ scopes: ["operator.admin", "operator.pairing", "operator.read", "operator.write"]
38
+ }
39
+ }));
40
+ return;
41
+ }
42
+
43
+ if (!resolved && msg.type === "res" && msg.ok) {
44
+ resolved = true;
45
+ resolve(ws);
46
+ }
47
+ });
48
+
49
+ ws.on("error", (e) => { if (!resolved) reject(e); });
50
+ setTimeout(() => { if (!resolved) { ws.close(); reject(new Error("Timeout")); } }, 10000);
51
+ });
52
+ }
53
+
54
+ async function callRpc(ws, method, params) {
55
+ return new Promise((resolve, reject) => {
56
+ const id = randomUUID();
57
+ const handler = (data) => {
58
+ const msg = JSON.parse(data.toString());
59
+ if (msg.id === id) {
60
+ ws.removeListener("message", handler);
61
+ resolve(msg);
62
+ }
63
+ };
64
+ ws.on("message", handler);
65
+ ws.send(JSON.stringify({ type: "req", id, method, params }));
66
+ setTimeout(() => { ws.removeListener("message", handler); reject(new Error("RPC Timeout")); }, WAIT_TIMEOUT + 5000);
67
+ });
68
+ }
69
+
70
+ async function checkStatus() {
71
+ if (isWaiting) return;
72
+
73
+ let ws;
74
+ try {
75
+ ws = await createConnection();
76
+
77
+ // Check if WhatsApp channel exists and its status
78
+ const statusRes = await callRpc(ws, "channels.status", {});
79
+ const channels = (statusRes.payload || statusRes.result)?.channels || {};
80
+ const wa = channels.whatsapp;
81
+
82
+ if (!wa) {
83
+ ws.close();
84
+ return;
85
+ }
86
+
87
+ // If connected, we are good
88
+ if (wa.connected) {
89
+ ws.close();
90
+ return;
91
+ }
92
+
93
+ // If "Ready to pair", we wait for the scan
94
+ isWaiting = true;
95
+ if (!hasShownWaitMessage) {
96
+ console.log("\n[guardian] πŸ“± WhatsApp pairing in progress. Please scan the QR code in the Control UI.");
97
+ hasShownWaitMessage = true;
98
+ }
99
+
100
+ console.log("[guardian] Waiting for pairing completion...");
101
+ const waitRes = await callRpc(ws, "web.login.wait", { timeoutMs: WAIT_TIMEOUT });
102
+ const result = waitRes.payload || waitRes.result;
103
+
104
+ if (result && (result.connected || (result.message && result.message.includes("515")))) {
105
+ console.log("[guardian] βœ… Pairing completed! Saving session and restarting gateway...");
106
+ hasShownWaitMessage = false;
107
+
108
+ // Auto-reapply config to finalize pairing
109
+ const getRes = await callRpc(ws, "config.get", {});
110
+ if (getRes.ok) {
111
+ await callRpc(ws, "config.apply", { raw: getRes.payload.raw, baseHash: getRes.payload.hash });
112
+ console.log("[guardian] Configuration re-applied.");
113
+ }
114
+ }
115
+
116
+ } catch (e) {
117
+ // Normal timeout or gateway starting up
118
+ } finally {
119
+ isWaiting = false;
120
+ if (ws) ws.close();
121
+ }
122
+ }
123
+
124
+ console.log("[guardian] βš”οΈ WhatsApp Guardian active. Monitoring pairing status...");
125
+ setInterval(checkStatus, CHECK_INTERVAL);
126
+ setTimeout(checkStatus, 10000);
workspace-sync.py CHANGED
@@ -19,6 +19,7 @@ INTERVAL = int(os.environ.get("SYNC_INTERVAL", "600"))
19
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
20
  HF_USERNAME = os.environ.get("HF_USERNAME", "")
21
  BACKUP_DATASET = os.environ.get("BACKUP_DATASET_NAME", "huggingclaw-backup")
 
22
 
23
  running = True
24
 
@@ -42,6 +43,37 @@ def has_changes():
42
  except Exception:
43
  return False
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
  def sync_with_hf_hub():
47
  """Sync workspace using huggingface_hub library."""
@@ -128,21 +160,32 @@ def main():
128
  continue
129
 
130
  ts = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
 
 
131
 
132
  if use_hf_hub:
133
  if sync_with_hf_hub():
134
  print(f"πŸ”„ Workspace sync (hf_hub): pushed changes ({ts})")
 
135
  else:
136
  # Fallback to git
137
  if sync_with_git():
138
  print(f"πŸ”„ Workspace sync (git fallback): pushed changes ({ts})")
 
139
  else:
140
- print(f"πŸ”„ Workspace sync: failed ({ts}), will retry")
 
 
 
141
  else:
142
  if sync_with_git():
143
  print(f"πŸ”„ Workspace sync (git): pushed changes ({ts})")
 
144
  else:
145
- print(f"πŸ”„ Workspace sync: push failed ({ts}), will retry")
 
 
 
146
 
147
 
148
  if __name__ == "__main__":
 
19
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
20
  HF_USERNAME = os.environ.get("HF_USERNAME", "")
21
  BACKUP_DATASET = os.environ.get("BACKUP_DATASET_NAME", "huggingclaw-backup")
22
+ WEBHOOK_URL = os.environ.get("WEBHOOK_URL", "")
23
 
24
  running = True
25
 
 
43
  except Exception:
44
  return False
45
 
46
+ def write_sync_status(status, message=""):
47
+ """Write sync status to file for the health server dashboard."""
48
+ try:
49
+ import json
50
+ data = {
51
+ "status": status,
52
+ "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
53
+ "message": message
54
+ }
55
+ with open("/tmp/sync-status.json", "w") as f:
56
+ json.dump(data, f)
57
+ except Exception as e:
58
+ print(f" ⚠️ Could not write sync status: {e}")
59
+
60
+ def trigger_webhook(event, status, message):
61
+ """Trigger webhook notification."""
62
+ if not WEBHOOK_URL:
63
+ return
64
+ try:
65
+ import urllib.request
66
+ import json
67
+ data = json.dumps({
68
+ "event": event,
69
+ "status": status,
70
+ "message": message,
71
+ "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
72
+ }).encode('utf-8')
73
+ req = urllib.request.Request(WEBHOOK_URL, data=data, headers={'Content-Type': 'application/json'})
74
+ urllib.request.urlopen(req, timeout=10)
75
+ except Exception as e:
76
+ print(f" ⚠️ Webhook delivery failed: {e}")
77
 
78
  def sync_with_hf_hub():
79
  """Sync workspace using huggingface_hub library."""
 
160
  continue
161
 
162
  ts = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
163
+
164
+ write_sync_status("syncing", f"Starting sync at {ts}")
165
 
166
  if use_hf_hub:
167
  if sync_with_hf_hub():
168
  print(f"πŸ”„ Workspace sync (hf_hub): pushed changes ({ts})")
169
+ write_sync_status("success", "Successfully pushed to HF Hub")
170
  else:
171
  # Fallback to git
172
  if sync_with_git():
173
  print(f"πŸ”„ Workspace sync (git fallback): pushed changes ({ts})")
174
+ write_sync_status("success", "Successfully pushed via git fallback")
175
  else:
176
+ msg = f"Workspace sync: failed ({ts}), will retry"
177
+ print(f"πŸ”„ {msg}")
178
+ write_sync_status("error", msg)
179
+ trigger_webhook("sync", "error", msg)
180
  else:
181
  if sync_with_git():
182
  print(f"πŸ”„ Workspace sync (git): pushed changes ({ts})")
183
+ write_sync_status("success", "Successfully pushed via git")
184
  else:
185
+ msg = f"Workspace sync: push failed ({ts}), will retry"
186
+ print(f"πŸ”„ {msg}")
187
+ write_sync_status("error", msg)
188
+ trigger_webhook("sync", "error", msg)
189
 
190
 
191
  if __name__ == "__main__":