Spaces:
Running
Running
Commit ·
d65efac
1
Parent(s): 0a82210
Merge pull request #31 from anurag162008/codex/find-automatic-package-installation-method-d90k9g
Browse files- .env.example +4 -0
- .gitattributes +34 -0
- .gitignore +4 -7
- CHANGELOG.md +13 -0
- CONTRIBUTING.md +3 -2
- Dockerfile +37 -11
- README.md +50 -12
- SECURITY.md +1 -0
- health-server.js +234 -307
- login.html +59 -0
- openclaw-sync.py +1 -4
- start.sh +131 -13
.env.example
CHANGED
|
@@ -247,6 +247,10 @@ LLM_API_KEY_FALLBACK_ENABLED=true
|
|
| 247 |
# Generate: openssl rand -hex 32
|
| 248 |
GATEWAY_TOKEN=your_gateway_token_here
|
| 249 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
# (Optional) Password auth — simpler alternative to token for casual users
|
| 251 |
# If set, users can log in with this password instead of the token
|
| 252 |
# OPENCLAW_PASSWORD=your_password_here
|
|
|
|
| 247 |
# Generate: openssl rand -hex 32
|
| 248 |
GATEWAY_TOKEN=your_gateway_token_here
|
| 249 |
|
| 250 |
+
# [OPTIONAL] JupyterLab terminal token for /terminal/
|
| 251 |
+
# Defaults to "huggingface" if unset. Set a strong token for private deployments.
|
| 252 |
+
JUPYTER_TOKEN=huggingface
|
| 253 |
+
|
| 254 |
# (Optional) Password auth — simpler alternative to token for casual users
|
| 255 |
# If set, users can log in with this password instead of the token
|
| 256 |
# OPENCLAW_PASSWORD=your_password_here
|
.gitattributes
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
.gitignore
CHANGED
|
@@ -1,21 +1,20 @@
|
|
| 1 |
-
# personal workflows
|
| 2 |
-
.github/workflows/
|
| 3 |
-
.git*
|
| 4 |
# env/secrets
|
| 5 |
.env
|
| 6 |
.env.*
|
|
|
|
| 7 |
*.pem
|
| 8 |
*.key
|
| 9 |
|
| 10 |
# node
|
| 11 |
node_modules/
|
|
|
|
| 12 |
npm-debug.log*
|
| 13 |
yarn-debug.log*
|
| 14 |
pnpm-debug.log*
|
| 15 |
|
| 16 |
# python
|
| 17 |
__pycache__/
|
| 18 |
-
*.
|
| 19 |
.venv/
|
| 20 |
venv/
|
| 21 |
|
|
@@ -33,12 +32,10 @@ build/
|
|
| 33 |
.cache/
|
| 34 |
|
| 35 |
# temp
|
|
|
|
| 36 |
tmp/
|
| 37 |
temp/
|
| 38 |
|
| 39 |
# os junk
|
| 40 |
.DS_Store
|
| 41 |
Thumbs.db
|
| 42 |
-
|
| 43 |
-
# gitignore itself
|
| 44 |
-
.gitignore
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# env/secrets
|
| 2 |
.env
|
| 3 |
.env.*
|
| 4 |
+
!.env.example
|
| 5 |
*.pem
|
| 6 |
*.key
|
| 7 |
|
| 8 |
# node
|
| 9 |
node_modules/
|
| 10 |
+
gui/node_modules/
|
| 11 |
npm-debug.log*
|
| 12 |
yarn-debug.log*
|
| 13 |
pnpm-debug.log*
|
| 14 |
|
| 15 |
# python
|
| 16 |
__pycache__/
|
| 17 |
+
*.py[cod]
|
| 18 |
.venv/
|
| 19 |
venv/
|
| 20 |
|
|
|
|
| 32 |
.cache/
|
| 33 |
|
| 34 |
# temp
|
| 35 |
+
.tmp/
|
| 36 |
tmp/
|
| 37 |
temp/
|
| 38 |
|
| 39 |
# os junk
|
| 40 |
.DS_Store
|
| 41 |
Thumbs.db
|
|
|
|
|
|
|
|
|
CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
| 2 |
|
| 3 |
All notable changes to this project will be documented in this file.
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
## [1.5.0] - 2026-05-13
|
| 6 |
|
| 7 |
### Added
|
|
|
|
| 2 |
|
| 3 |
All notable changes to this project will be documented in this file.
|
| 4 |
|
| 5 |
+
## [1.6.0] - 2026-05-14
|
| 6 |
+
|
| 7 |
+
### Added
|
| 8 |
+
|
| 9 |
+
- **Merged JupyterLab terminal** — added the Hugging Face JupyterLab template login page and `/terminal/` routing alongside the HuggingClaw dashboard and `/app/` OpenClaw Control UI.
|
| 10 |
+
- **HF template parity files** — restored Git LFS defaults and project metadata/docs that were missing from the merged repository.
|
| 11 |
+
|
| 12 |
+
### Fixed
|
| 13 |
+
|
| 14 |
+
- **HF Spaces subpath routing** — normalized internal redirects and WebSocket forwarding so `/app/` and `/terminal/` stay mounted behind the single public Spaces port.
|
| 15 |
+
- **Startup log noise** — removed the stale disabled `plugins.entries.acpx` config entry, switched Jupyter token/cookie options to `IdentityProvider.*`, and printed `/app/` with the trailing slash.
|
| 16 |
+
- **Private Space navigation** — dashboard buttons now stay in-frame and startup logs print relative routes, avoiding raw `.hf.space` links that can show Hugging Face's outer 404 page on private Spaces.
|
| 17 |
+
|
| 18 |
## [1.5.0] - 2026-05-13
|
| 19 |
|
| 20 |
### Added
|
CONTRIBUTING.md
CHANGED
|
@@ -17,7 +17,7 @@ Thanks for your interest in contributing! 🦞
|
|
| 17 |
1. Fork the repo
|
| 18 |
2. Create a feature branch: `git checkout -b feature/my-feature`
|
| 19 |
3. Make your changes
|
| 20 |
-
4. Test locally with Docker: `docker build -t huggingclaw . && docker run -p
|
| 21 |
5. Commit with a clear message
|
| 22 |
6. Push and open a PR
|
| 23 |
|
|
@@ -27,6 +27,7 @@ Thanks for your interest in contributing! 🦞
|
|
| 27 |
- No unnecessary dependencies
|
| 28 |
|
| 29 |
### Testing
|
|
|
|
| 30 |
- Test with at least one LLM provider (Anthropic, OpenAI, or Google)
|
| 31 |
- Test with and without Telegram enabled
|
| 32 |
- Test with and without workspace backup enabled
|
|
@@ -38,7 +39,7 @@ Thanks for your interest in contributing! 🦞
|
|
| 38 |
cp .env.example .env
|
| 39 |
# Fill in your values
|
| 40 |
docker build -t huggingclaw .
|
| 41 |
-
docker run -p
|
| 42 |
```
|
| 43 |
|
| 44 |
## Questions?
|
|
|
|
| 17 |
1. Fork the repo
|
| 18 |
2. Create a feature branch: `git checkout -b feature/my-feature`
|
| 19 |
3. Make your changes
|
| 20 |
+
4. Test locally with Docker: `docker build -t huggingclaw . && docker run -p 7861:7861 --env-file .env huggingclaw`
|
| 21 |
5. Commit with a clear message
|
| 22 |
6. Push and open a PR
|
| 23 |
|
|
|
|
| 27 |
- No unnecessary dependencies
|
| 28 |
|
| 29 |
### Testing
|
| 30 |
+
- If touching routing or Docker startup, verify both `/app/` (OpenClaw UI) and `/terminal/` (JupyterLab terminal).
|
| 31 |
- Test with at least one LLM provider (Anthropic, OpenAI, or Google)
|
| 32 |
- Test with and without Telegram enabled
|
| 33 |
- Test with and without workspace backup enabled
|
|
|
|
| 39 |
cp .env.example .env
|
| 40 |
# Fill in your values
|
| 41 |
docker build -t huggingclaw .
|
| 42 |
+
docker run -p 7861:7861 --env-file .env huggingclaw
|
| 43 |
```
|
| 44 |
|
| 45 |
## Questions?
|
Dockerfile
CHANGED
|
@@ -1,7 +1,11 @@
|
|
| 1 |
# ════════════════════════════════════════════════════════════════
|
| 2 |
-
# 🦞 HuggingClaw
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
# ════════════════════════════════════════════════════════════════
|
| 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
|
|
@@ -10,8 +14,10 @@ FROM ghcr.io/openclaw/openclaw:${OPENCLAW_VERSION} AS openclaw
|
|
| 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 \
|
| 16 |
git \
|
| 17 |
sudo \
|
|
@@ -43,11 +49,17 @@ RUN apt-get update && apt-get install -y \
|
|
| 43 |
xfonts-scalable \
|
| 44 |
--no-install-recommends && \
|
| 45 |
pip3 install --no-cache-dir --break-system-packages huggingface_hub && \
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
rm -rf /var/lib/apt/lists/*
|
| 47 |
|
| 48 |
# Reuse existing node user (UID 1000). Allow passwordless package-manager
|
| 49 |
-
# commands only so runtime apt installs can be replayed after HF Space restarts
|
| 50 |
-
# without granting unrestricted sudo access.
|
| 51 |
RUN mkdir -p /home/node/app /home/node/.openclaw && \
|
| 52 |
chown -R 1000:1000 /home/node && \
|
| 53 |
printf '%s\n' \
|
|
@@ -60,8 +72,7 @@ RUN mkdir -p /home/node/app /home/node/.openclaw && \
|
|
| 60 |
# Copy pre-built OpenClaw (skips npm install entirely — much faster!)
|
| 61 |
COPY --from=openclaw --chown=1000:1000 /app /home/node/.openclaw/openclaw-app
|
| 62 |
|
| 63 |
-
# Add Playwright in an isolated sidecar node_modules
|
| 64 |
-
# bundled OpenClaw app dependency tree.
|
| 65 |
RUN mkdir -p /home/node/browser-deps && \
|
| 66 |
cd /home/node/browser-deps && \
|
| 67 |
npm init -y && \
|
|
@@ -75,14 +86,30 @@ RUN ln -s /home/node/.openclaw/openclaw-app/openclaw.mjs /usr/local/bin/openclaw
|
|
| 75 |
COPY --chown=1000:1000 cloudflare-proxy.js /opt/cloudflare-proxy.js
|
| 76 |
COPY --chown=1000:1000 cloudflare-proxy-setup.py /home/node/app/cloudflare-proxy-setup.py
|
| 77 |
COPY --chown=1000:1000 health-server.js /home/node/app/health-server.js
|
|
|
|
| 78 |
COPY --chown=1000:1000 iframe-fix.cjs /home/node/app/iframe-fix.cjs
|
| 79 |
COPY --chown=1000:1000 start.sh /home/node/app/start.sh
|
| 80 |
COPY --chown=1000:1000 wa-guardian.js /home/node/app/wa-guardian.js
|
| 81 |
COPY --chown=1000:1000 cloudflare-keepalive-setup.py /home/node/app/cloudflare-keepalive-setup.py
|
| 82 |
COPY --chown=1000:1000 openclaw-sync.py /home/node/app/openclaw-sync.py
|
| 83 |
-
RUN chmod +x /home/node/app/start.sh /home/node/app/cloudflare-proxy-setup.py /home/node/app/cloudflare-keepalive-setup.py /home/node/app/openclaw-sync.py
|
| 84 |
COPY --chown=1000:1000 multi-provider-key-rotator.cjs /home/node/app/multi-provider-key-rotator.cjs
|
| 85 |
-
RUN
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
USER node
|
| 88 |
|
|
@@ -94,10 +121,9 @@ ENV HOME=/home/node \
|
|
| 94 |
|
| 95 |
WORKDIR /home/node/app
|
| 96 |
|
|
|
|
| 97 |
EXPOSE 7861
|
| 98 |
|
| 99 |
-
# health-server.js exposes /health on 7861 and proxies to the gateway on 7860.
|
| 100 |
-
# 90s start period covers OpenClaw's plugin install + gateway boot on cold start.
|
| 101 |
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s \
|
| 102 |
CMD curl -fsS http://localhost:7861/health || exit 1
|
| 103 |
|
|
|
|
| 1 |
# ════════════════════════════════════════════════════════════════
|
| 2 |
+
# 🦞 HuggingClaw + 💻 JupyterLab Terminal
|
| 3 |
+
# ════════════════════════════════════════════════════════════════
|
| 4 |
+
# Port 7861 (exposed): Dashboard + reverse proxy
|
| 5 |
+
# / → HuggingClaw dashboard
|
| 6 |
+
# /app/ → OpenClaw gateway (internal :7860)
|
| 7 |
+
# /terminal/ → JupyterLab terminal (internal :8888)
|
| 8 |
# ════════════════════════════════════════════════════════════════
|
|
|
|
| 9 |
|
| 10 |
# ── Stage 1: Pull pre-built OpenClaw ──
|
| 11 |
ARG OPENCLAW_VERSION=latest
|
|
|
|
| 14 |
# ── Stage 2: Runtime ──
|
| 15 |
FROM node:22-slim
|
| 16 |
ARG OPENCLAW_VERSION=latest
|
| 17 |
+
ARG DEV_MODE=false
|
| 18 |
+
ENV DEV_MODE=${DEV_MODE}
|
| 19 |
|
| 20 |
+
# Install system dependencies (+ optional JupyterLab deps in DEV_MODE)
|
| 21 |
RUN apt-get update && apt-get install -y \
|
| 22 |
git \
|
| 23 |
sudo \
|
|
|
|
| 49 |
xfonts-scalable \
|
| 50 |
--no-install-recommends && \
|
| 51 |
pip3 install --no-cache-dir --break-system-packages huggingface_hub && \
|
| 52 |
+
case "$(printf '%s' "${DEV_MODE}" | tr '[:upper:]' '[:lower:]')" in \
|
| 53 |
+
true|1|yes|on) \
|
| 54 |
+
pip3 install --no-cache-dir --break-system-packages \
|
| 55 |
+
jupyterlab==4.5.7 \
|
| 56 |
+
tornado==6.5.5 \
|
| 57 |
+
ipywidgets==8.1.8 ;; \
|
| 58 |
+
esac && \
|
| 59 |
rm -rf /var/lib/apt/lists/*
|
| 60 |
|
| 61 |
# Reuse existing node user (UID 1000). Allow passwordless package-manager
|
| 62 |
+
# commands only so runtime apt installs can be replayed after HF Space restarts.
|
|
|
|
| 63 |
RUN mkdir -p /home/node/app /home/node/.openclaw && \
|
| 64 |
chown -R 1000:1000 /home/node && \
|
| 65 |
printf '%s\n' \
|
|
|
|
| 72 |
# Copy pre-built OpenClaw (skips npm install entirely — much faster!)
|
| 73 |
COPY --from=openclaw --chown=1000:1000 /app /home/node/.openclaw/openclaw-app
|
| 74 |
|
| 75 |
+
# Add Playwright in an isolated sidecar node_modules
|
|
|
|
| 76 |
RUN mkdir -p /home/node/browser-deps && \
|
| 77 |
cd /home/node/browser-deps && \
|
| 78 |
npm init -y && \
|
|
|
|
| 86 |
COPY --chown=1000:1000 cloudflare-proxy.js /opt/cloudflare-proxy.js
|
| 87 |
COPY --chown=1000:1000 cloudflare-proxy-setup.py /home/node/app/cloudflare-proxy-setup.py
|
| 88 |
COPY --chown=1000:1000 health-server.js /home/node/app/health-server.js
|
| 89 |
+
COPY --chown=1000:1000 login.html /home/node/app/login.html
|
| 90 |
COPY --chown=1000:1000 iframe-fix.cjs /home/node/app/iframe-fix.cjs
|
| 91 |
COPY --chown=1000:1000 start.sh /home/node/app/start.sh
|
| 92 |
COPY --chown=1000:1000 wa-guardian.js /home/node/app/wa-guardian.js
|
| 93 |
COPY --chown=1000:1000 cloudflare-keepalive-setup.py /home/node/app/cloudflare-keepalive-setup.py
|
| 94 |
COPY --chown=1000:1000 openclaw-sync.py /home/node/app/openclaw-sync.py
|
|
|
|
| 95 |
COPY --chown=1000:1000 multi-provider-key-rotator.cjs /home/node/app/multi-provider-key-rotator.cjs
|
| 96 |
+
RUN printf '%s\n' \
|
| 97 |
+
'#!/usr/bin/env bash' \
|
| 98 |
+
'set -euo pipefail' \
|
| 99 |
+
'case "$(printf "%s" "${DEV_MODE}" | tr "[:upper:]" "[:lower:]")" in' \
|
| 100 |
+
' true|1|yes|on)' \
|
| 101 |
+
" python3 -c \"from pathlib import Path; import shutil, jupyter_server; template_dir = Path(jupyter_server.__file__).parent / 'templates'; template_dir.mkdir(parents=True, exist_ok=True); shutil.copyfile('/home/node/app/login.html', template_dir / 'login.html')\"" \
|
| 102 |
+
' ;;' \
|
| 103 |
+
'esac' \
|
| 104 |
+
> /tmp/setup-jupyter-template.sh && \
|
| 105 |
+
chmod +x /tmp/setup-jupyter-template.sh && \
|
| 106 |
+
/tmp/setup-jupyter-template.sh && \
|
| 107 |
+
rm -f /tmp/setup-jupyter-template.sh
|
| 108 |
+
RUN chmod +x /home/node/app/start.sh \
|
| 109 |
+
/home/node/app/cloudflare-proxy-setup.py \
|
| 110 |
+
/home/node/app/cloudflare-keepalive-setup.py \
|
| 111 |
+
/home/node/app/openclaw-sync.py \
|
| 112 |
+
/home/node/app/multi-provider-key-rotator.cjs
|
| 113 |
|
| 114 |
USER node
|
| 115 |
|
|
|
|
| 121 |
|
| 122 |
WORKDIR /home/node/app
|
| 123 |
|
| 124 |
+
# 7861 = public entrypoint (dashboard + proxy for both OpenClaw and JupyterLab)
|
| 125 |
EXPOSE 7861
|
| 126 |
|
|
|
|
|
|
|
| 127 |
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s \
|
| 128 |
CMD curl -fsS http://localhost:7861/health || exit 1
|
| 129 |
|
README.md
CHANGED
|
@@ -1,12 +1,17 @@
|
|
| 1 |
---
|
| 2 |
title: HuggingClaw
|
| 3 |
emoji: 🦞
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
app_port: 7861
|
| 8 |
-
pinned:
|
| 9 |
license: mit
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
secrets:
|
| 11 |
- name: LLM_API_KEY
|
| 12 |
description: "Your LLM provider API key (e.g. Anthropic, OpenAI, Google, OpenRouter)."
|
|
@@ -14,6 +19,8 @@ secrets:
|
|
| 14 |
description: "Model ID to use, e.g. google/gemini-2.5-flash or openai/gpt-4o."
|
| 15 |
- name: GATEWAY_TOKEN
|
| 16 |
description: "Strong token to secure your OpenClaw Control UI (generate: openssl rand -hex 32)."
|
|
|
|
|
|
|
| 17 |
- name: CLOUDFLARE_WORKERS_TOKEN
|
| 18 |
description: "Cloudflare API token — auto-creates a Worker proxy and KeepAlive monitor."
|
| 19 |
- name: TELEGRAM_ALLOWED_USERS
|
|
@@ -32,7 +39,7 @@ secrets:
|
|
| 32 |
[](https://huggingface.co/spaces)
|
| 33 |
[](https://github.com/openclaw/openclaw)
|
| 34 |
|
| 35 |
-
**Your always-on AI assistant — free, no server needed.**
|
| 36 |
|
| 37 |
## Table of Contents
|
| 38 |
|
|
@@ -49,6 +56,8 @@ secrets:
|
|
| 49 |
- [🤖 LLM Providers](#-llm-providers)
|
| 50 |
- [💻 Local Development](#-local-development)
|
| 51 |
- [🔗 CLI Access](#-cli-access)
|
|
|
|
|
|
|
| 52 |
- [🏗️ Architecture](#-architecture)
|
| 53 |
- [💓 Staying Alive](#-staying-alive)
|
| 54 |
- [🐛 Troubleshooting](#-troubleshooting)
|
|
@@ -69,6 +78,7 @@ secrets:
|
|
| 69 |
- 📊 **Visual Dashboard:** Beautiful Web UI to monitor uptime, sync status, and active models.
|
| 70 |
- 🔔 **Webhooks:** Get notified on restarts or backup failures via standard webhooks.
|
| 71 |
- 🔐 **Flexible Auth:** Secure the Control UI with either a gateway token or password.
|
|
|
|
| 72 |
- 🏠 **100% HF-Native:** Runs entirely on HuggingFace’s free infrastructure (2 vCPU, 16GB RAM).
|
| 73 |
|
| 74 |
## 🎥 Video Tutorial
|
|
@@ -94,7 +104,7 @@ Navigate to your new Space's **Settings**, scroll down to the **Variables and se
|
|
| 94 |
> [!TIP]
|
| 95 |
> HuggingClaw is completely flexible! You only need these three secrets to get started. You can set other secrets later.
|
| 96 |
|
| 97 |
-
Optional:
|
| 98 |
|
| 99 |
### Step 3: Deploy & Run
|
| 100 |
|
|
@@ -348,6 +358,30 @@ openclaw channels login --gateway https://YOUR_SPACE_NAME.hf.space
|
|
| 348 |
# When prompted, enter your GATEWAY_TOKEN
|
| 349 |
```
|
| 350 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
## 🏗️ Architecture
|
| 352 |
|
| 353 |
HuggingClaw uses a multi-layered approach to ensure stability and persistence on Hugging Face's ephemeral infrastructure.
|
|
@@ -355,8 +389,9 @@ HuggingClaw uses a multi-layered approach to ensure stability and persistence on
|
|
| 355 |
<details>
|
| 356 |
<summary><b>Click to view technical details</b></summary>
|
| 357 |
|
| 358 |
-
- **Dashboard (`/`)**: Management, monitoring, and keep-alive tools.
|
| 359 |
-
- **Control UI (`/
|
|
|
|
| 360 |
- **Health Check (`/health`)**: Endpoint for uptime monitoring and readiness probes.
|
| 361 |
- **Sync Engine**: Python background process managing HF Dataset persistence.
|
| 362 |
- **Transparent Proxy**: Interceptor for requests to blocked domains (Telegram, etc.).
|
|
@@ -367,12 +402,15 @@ HuggingClaw uses a multi-layered approach to ensure stability and persistence on
|
|
| 367 |
2. Resolve backup namespace and restore workspace from HF Dataset.
|
| 368 |
3. Generate `openclaw.json` configuration.
|
| 369 |
4. Launch background tasks (auto-sync, channel helpers).
|
| 370 |
-
5. Start OpenClaw gateway
|
| 371 |
|
| 372 |
</details>
|
| 373 |
|
| 374 |
## 🐛 Troubleshooting
|
| 375 |
|
|
|
|
|
|
|
|
|
|
| 376 |
- **Missing secrets:** Ensure `LLM_API_KEY`, `LLM_MODEL`, and `GATEWAY_TOKEN` are set in your Space **Settings → Secrets**.
|
| 377 |
- **Telegram bot issues:** Verify your `TELEGRAM_BOT_TOKEN`. Check Space logs for lines like `📱 Enabling Telegram`.
|
| 378 |
- **Backup restore failing:** Make sure `HF_TOKEN` is valid and has write access to your HF account dataset. Set `HF_USERNAME` only if auto-detection is not available in your environment.
|
|
@@ -397,9 +435,9 @@ Similar projects by [@somratpro](https://github.com/somratpro) — all free, one
|
|
| 397 |
|
| 398 |
## 📚 Links
|
| 399 |
|
| 400 |
-
- [OpenClaw Docs](https://docs.openclaw.ai)
|
| 401 |
-
- [OpenClaw GitHub](https://github.com/openclaw/openclaw)
|
| 402 |
-
- [HuggingFace Spaces Docs](https://huggingface.co/docs/hub/spaces)
|
| 403 |
|
| 404 |
## ❤️ Support
|
| 405 |
|
|
@@ -422,4 +460,4 @@ Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for gui
|
|
| 422 |
|
| 423 |
MIT — see [LICENSE](LICENSE) for details.
|
| 424 |
|
| 425 |
-
*Made with ❤️ by [@somratpro](https://github.com/somratpro) for the OpenClaw community.*
|
|
|
|
| 1 |
---
|
| 2 |
title: HuggingClaw
|
| 3 |
emoji: 🦞
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: blue
|
| 6 |
sdk: docker
|
| 7 |
app_port: 7861
|
| 8 |
+
pinned: false
|
| 9 |
license: mit
|
| 10 |
+
tags:
|
| 11 |
+
- openclaw
|
| 12 |
+
- jupyterlab
|
| 13 |
+
- terminal
|
| 14 |
+
- llm-gateway
|
| 15 |
secrets:
|
| 16 |
- name: LLM_API_KEY
|
| 17 |
description: "Your LLM provider API key (e.g. Anthropic, OpenAI, Google, OpenRouter)."
|
|
|
|
| 19 |
description: "Model ID to use, e.g. google/gemini-2.5-flash or openai/gpt-4o."
|
| 20 |
- name: GATEWAY_TOKEN
|
| 21 |
description: "Strong token to secure your OpenClaw Control UI (generate: openssl rand -hex 32)."
|
| 22 |
+
- name: JUPYTER_TOKEN
|
| 23 |
+
description: "Optional strong token for the JupyterLab terminal at /terminal/ (defaults to huggingface)."
|
| 24 |
- name: CLOUDFLARE_WORKERS_TOKEN
|
| 25 |
description: "Cloudflare API token — auto-creates a Worker proxy and KeepAlive monitor."
|
| 26 |
- name: TELEGRAM_ALLOWED_USERS
|
|
|
|
| 39 |
[](https://huggingface.co/spaces)
|
| 40 |
[](https://github.com/openclaw/openclaw)
|
| 41 |
|
| 42 |
+
**Your always-on AI assistant — free, no server needed.** This merged Space runs [OpenClaw](https://openclaw.ai) plus a Hugging Face-style JupyterLab terminal on one HF Spaces port, 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.
|
| 43 |
|
| 44 |
## Table of Contents
|
| 45 |
|
|
|
|
| 56 |
- [🤖 LLM Providers](#-llm-providers)
|
| 57 |
- [💻 Local Development](#-local-development)
|
| 58 |
- [🔗 CLI Access](#-cli-access)
|
| 59 |
+
- [💻 JupyterLab Terminal](#-jupyterlab-terminal)
|
| 60 |
+
- [🔍 Merge Comparison](#-merge-comparison)
|
| 61 |
- [🏗️ Architecture](#-architecture)
|
| 62 |
- [💓 Staying Alive](#-staying-alive)
|
| 63 |
- [🐛 Troubleshooting](#-troubleshooting)
|
|
|
|
| 78 |
- 📊 **Visual Dashboard:** Beautiful Web UI to monitor uptime, sync status, and active models.
|
| 79 |
- 🔔 **Webhooks:** Get notified on restarts or backup failures via standard webhooks.
|
| 80 |
- 🔐 **Flexible Auth:** Secure the Control UI with either a gateway token or password.
|
| 81 |
+
- 💻 **Optional Dev Terminal:** JupyterLab is available at `/terminal/` only when `DEV_MODE=true` (disabled by default).
|
| 82 |
- 🏠 **100% HF-Native:** Runs entirely on HuggingFace’s free infrastructure (2 vCPU, 16GB RAM).
|
| 83 |
|
| 84 |
## 🎥 Video Tutorial
|
|
|
|
| 104 |
> [!TIP]
|
| 105 |
> HuggingClaw is completely flexible! You only need these three secrets to get started. You can set other secrets later.
|
| 106 |
|
| 107 |
+
Optional: set `DEV_MODE=true` (Variable) to enable JupyterLab support and install Jupyter dependencies at build time. You can also set `JUPYTER_TOKEN` as a Secret to replace the default terminal token (`huggingface`). 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 these should be Variables, not Secrets (except tokens).
|
| 108 |
|
| 109 |
### Step 3: Deploy & Run
|
| 110 |
|
|
|
|
| 358 |
# When prompted, enter your GATEWAY_TOKEN
|
| 359 |
```
|
| 360 |
|
| 361 |
+
## 💻 JupyterLab Terminal
|
| 362 |
+
|
| 363 |
+
The merged Space includes the Hugging Face JupyterLab template behavior inside the same container:
|
| 364 |
+
|
| 365 |
+
| Path | Service | Internal Port | Notes |
|
| 366 |
+
| :--- | :--- | :--- | :--- |
|
| 367 |
+
| `/` | HuggingClaw dashboard | `7861` | Public HF Spaces entrypoint |
|
| 368 |
+
| `/app/` | OpenClaw Control UI | `7860` | Mounted behind the local reverse proxy |
|
| 369 |
+
| `/terminal/` | JupyterLab terminal (DEV_MODE only) | `8888` | Available only when `DEV_MODE=true`; token login uses `JUPYTER_TOKEN` (default `huggingface`) |
|
| 370 |
+
|
| 371 |
+
When enabled, the terminal notebook root is `/home/node`, so you can inspect HuggingClaw files, logs, workspace state, and runtime scripts from the browser.
|
| 372 |
+
|
| 373 |
+
> [!IMPORTANT]
|
| 374 |
+
> For real deployments, set a strong `JUPYTER_TOKEN` secret. The `huggingface` default exists only to match the duplicateable Hugging Face JupyterLab template.
|
| 375 |
+
|
| 376 |
+
## 🔍 Merge Comparison
|
| 377 |
+
|
| 378 |
+
This repository is a merge of two sources:
|
| 379 |
+
|
| 380 |
+
- `anurag162008/HuggingClaw`: OpenClaw gateway, dashboard, Cloudflare proxy/keep-alive, Telegram/WhatsApp helpers, backup sync, key rotation, docs, and security metadata.
|
| 381 |
+
- Hugging Face `SpacesExamples/jupyterlab` template: JupyterLab Docker behavior, token login UX, Hugging Face-branded login template, pinned Jupyter packages, and Git LFS defaults for large model/data artifacts.
|
| 382 |
+
|
| 383 |
+
The main merge-specific change is the single-port router: HF Spaces exposes `7861`, while the router keeps OpenClaw at `/app/` and JupyterLab at `/terminal/` without leaking internal redirects such as `http://127.0.0.1:8888/...`.
|
| 384 |
+
|
| 385 |
## 🏗️ Architecture
|
| 386 |
|
| 387 |
HuggingClaw uses a multi-layered approach to ensure stability and persistence on Hugging Face's ephemeral infrastructure.
|
|
|
|
| 389 |
<details>
|
| 390 |
<summary><b>Click to view technical details</b></summary>
|
| 391 |
|
| 392 |
+
- **Dashboard (`/`)**: Management, monitoring, and keep-alive tools (terminal controls appear only in DEV mode).
|
| 393 |
+
- **Control UI (`/app/`)**: Secure interface for managing agents and channels, proxied to the OpenClaw gateway on internal port `7860`.
|
| 394 |
+
- **JupyterLab Terminal (`/terminal/`)**: Browser terminal/notebook server on internal port `8888` (DEV mode only).
|
| 395 |
- **Health Check (`/health`)**: Endpoint for uptime monitoring and readiness probes.
|
| 396 |
- **Sync Engine**: Python background process managing HF Dataset persistence.
|
| 397 |
- **Transparent Proxy**: Interceptor for requests to blocked domains (Telegram, etc.).
|
|
|
|
| 402 |
2. Resolve backup namespace and restore workspace from HF Dataset.
|
| 403 |
3. Generate `openclaw.json` configuration.
|
| 404 |
4. Launch background tasks (auto-sync, channel helpers).
|
| 405 |
+
5. Start the local dashboard/reverse proxy and OpenClaw gateway (JupyterLab starts only when `DEV_MODE=true`).
|
| 406 |
|
| 407 |
</details>
|
| 408 |
|
| 409 |
## 🐛 Troubleshooting
|
| 410 |
|
| 411 |
+
- **Private Space 404:** If your Space is private, raw `https://<space>.hf.space/app/` or `/terminal/` links can show Hugging Face's own 404 page when opened outside the embedded App session. Open the Space's **App** tab first, then use the in-page dashboard buttons for `/app/` and `/terminal/`.
|
| 412 |
+
- **Terminal 404 or redirect loop:** Open `/terminal/` with the trailing slash from the dashboard/App tab, rebuild after Dockerfile changes, and confirm `JUPYTER_TOKEN` is set correctly if you changed the default.
|
| 413 |
+
- **Control UI 404:** Open `/app/` with the trailing slash from the dashboard/App tab; the reverse proxy rewrites backend redirects into this mount path.
|
| 414 |
- **Missing secrets:** Ensure `LLM_API_KEY`, `LLM_MODEL`, and `GATEWAY_TOKEN` are set in your Space **Settings → Secrets**.
|
| 415 |
- **Telegram bot issues:** Verify your `TELEGRAM_BOT_TOKEN`. Check Space logs for lines like `📱 Enabling Telegram`.
|
| 416 |
- **Backup restore failing:** Make sure `HF_TOKEN` is valid and has write access to your HF account dataset. Set `HF_USERNAME` only if auto-detection is not available in your environment.
|
|
|
|
| 435 |
|
| 436 |
## 📚 Links
|
| 437 |
|
| 438 |
+
- [OpenClaw Docs](https://docs.openclaw.ai)
|
| 439 |
+
- [OpenClaw GitHub](https://github.com/openclaw/openclaw)
|
| 440 |
+
- [HuggingFace Spaces Docs](https://huggingface.co/docs/hub/spaces)
|
| 441 |
|
| 442 |
## ❤️ Support
|
| 443 |
|
|
|
|
| 460 |
|
| 461 |
MIT — see [LICENSE](LICENSE) for details.
|
| 462 |
|
| 463 |
+
*Made with ❤️ by [@somratpro](https://github.com/somratpro) for the OpenClaw community.*
|
SECURITY.md
CHANGED
|
@@ -16,6 +16,7 @@ When deploying HuggingClaw:
|
|
| 16 |
|
| 17 |
- **Set your Space to Private** — prevents unauthorized access to your gateway
|
| 18 |
- **Use a strong `GATEWAY_TOKEN`** — generate with `openssl rand -hex 32`
|
|
|
|
| 19 |
- **Keep your HF token scoped** — use fine-grained tokens with minimum permissions
|
| 20 |
- **Don't commit `.env` files** — the `.gitignore` already excludes them
|
| 21 |
- **Use `TELEGRAM_ALLOWED_USERS`** — restricts bot access to your account only
|
|
|
|
| 16 |
|
| 17 |
- **Set your Space to Private** — prevents unauthorized access to your gateway
|
| 18 |
- **Use a strong `GATEWAY_TOKEN`** — generate with `openssl rand -hex 32`
|
| 19 |
+
- **Set a strong `JUPYTER_TOKEN`** — the `/terminal/` JupyterLab login defaults to `huggingface` only for template convenience
|
| 20 |
- **Keep your HF token scoped** — use fine-grained tokens with minimum permissions
|
| 21 |
- **Don't commit `.env` files** — the `.gitignore` already excludes them
|
| 22 |
- **Use `TELEGRAM_ALLOWED_USERS`** — restricts bot access to your account only
|
health-server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
// Single public entrypoint for HF Spaces:
|
| 2 |
const http = require("http");
|
| 3 |
const fs = require("fs");
|
| 4 |
const net = require("net");
|
|
@@ -6,6 +6,13 @@ const net = require("net");
|
|
| 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;
|
|
@@ -19,40 +26,26 @@ const CLOUDFLARE_KEEPALIVE_STATUS_FILE =
|
|
| 19 |
"/tmp/huggingclaw-cloudflare-keepalive-status.json";
|
| 20 |
|
| 21 |
function parseRequestUrl(url) {
|
| 22 |
-
try {
|
| 23 |
-
|
| 24 |
-
} catch {
|
| 25 |
-
return new URL("http://localhost/");
|
| 26 |
-
}
|
| 27 |
}
|
| 28 |
|
| 29 |
function getSyncStatus() {
|
| 30 |
try {
|
| 31 |
-
if (fs.existsSync(SYNC_STATUS_FILE))
|
| 32 |
return JSON.parse(fs.readFileSync(SYNC_STATUS_FILE, "utf8"));
|
| 33 |
-
}
|
| 34 |
} catch {}
|
| 35 |
-
if (HF_BACKUP_ENABLED)
|
| 36 |
-
return {
|
| 37 |
-
status: "configured",
|
| 38 |
-
message: `Backup is enabled. Waiting for sync window (${SYNC_INTERVAL}s).`,
|
| 39 |
-
};
|
| 40 |
-
}
|
| 41 |
return { status: "unknown", message: "No sync data yet" };
|
| 42 |
}
|
| 43 |
|
| 44 |
function readGuardianStatus() {
|
| 45 |
-
if (!WHATSAPP_ENABLED) {
|
| 46 |
-
return { configured: false, connected: false, pairing: false };
|
| 47 |
-
}
|
| 48 |
try {
|
| 49 |
if (fs.existsSync(WHATSAPP_STATUS_FILE)) {
|
| 50 |
-
const
|
| 51 |
-
return {
|
| 52 |
-
configured: parsed.configured !== false,
|
| 53 |
-
connected: parsed.connected === true,
|
| 54 |
-
pairing: parsed.pairing === true,
|
| 55 |
-
};
|
| 56 |
}
|
| 57 |
} catch {}
|
| 58 |
return { configured: true, connected: false, pairing: false };
|
|
@@ -60,71 +53,42 @@ function readGuardianStatus() {
|
|
| 60 |
|
| 61 |
function getKeepaliveStatus() {
|
| 62 |
try {
|
| 63 |
-
if (fs.existsSync(CLOUDFLARE_KEEPALIVE_STATUS_FILE))
|
| 64 |
-
return JSON.parse(
|
| 65 |
-
fs.readFileSync(CLOUDFLARE_KEEPALIVE_STATUS_FILE, "utf8"),
|
| 66 |
-
);
|
| 67 |
-
}
|
| 68 |
} catch {}
|
| 69 |
return null;
|
| 70 |
}
|
| 71 |
|
| 72 |
-
function
|
| 73 |
return new Promise((resolve) => {
|
| 74 |
-
const
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
port: GATEWAY_PORT,
|
| 78 |
-
path: "/health",
|
| 79 |
-
timeout: timeoutMs,
|
| 80 |
-
},
|
| 81 |
-
(response) => {
|
| 82 |
-
response.resume();
|
| 83 |
-
resolve(response.statusCode >= 200 && response.statusCode < 400);
|
| 84 |
-
},
|
| 85 |
-
);
|
| 86 |
-
request.on("timeout", () => {
|
| 87 |
-
request.destroy();
|
| 88 |
-
resolve(false);
|
| 89 |
});
|
| 90 |
-
|
|
|
|
| 91 |
});
|
| 92 |
}
|
| 93 |
|
| 94 |
function formatUptime(ms) {
|
| 95 |
-
const
|
| 96 |
-
const
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
if (hours) return `${hours}h ${minutes}m`;
|
| 101 |
-
return `${minutes}m`;
|
| 102 |
}
|
| 103 |
|
| 104 |
-
function escapeHtml(
|
| 105 |
-
return String(
|
| 106 |
-
.replace(/&/g, "&")
|
| 107 |
-
.replace(/</g, "<")
|
| 108 |
-
.replace(/>/g, ">")
|
| 109 |
-
.replace(/"/g, """);
|
| 110 |
}
|
| 111 |
|
| 112 |
-
function
|
| 113 |
return `<span class="badge ${tone}">${escapeHtml(label)}</span>`;
|
| 114 |
}
|
| 115 |
|
| 116 |
-
function
|
| 117 |
-
title,
|
| 118 |
-
value,
|
| 119 |
-
detail = "",
|
| 120 |
-
tone = "neutral",
|
| 121 |
-
meta = "",
|
| 122 |
-
}) {
|
| 123 |
return `<article class="tile ${tone}">
|
| 124 |
-
<div class="tile-head">
|
| 125 |
-
<span class="tile-title">${escapeHtml(title)}</span>
|
| 126 |
-
<span class="tile-dot"></span>
|
| 127 |
-
</div>
|
| 128 |
<div class="tile-value">${value}</div>
|
| 129 |
${detail ? `<div class="tile-detail">${detail}</div>` : ""}
|
| 130 |
${meta ? `<div class="tile-meta">${meta}</div>` : ""}
|
|
@@ -133,276 +97,239 @@ function renderTile({
|
|
| 133 |
|
| 134 |
function renderDashboard(data) {
|
| 135 |
const syncStatus = String(data.sync?.status || "unknown");
|
| 136 |
-
const syncTone = ["success",
|
| 137 |
-
|
| 138 |
-
)
|
| 139 |
-
|
| 140 |
-
: syncStatus === "disabled"
|
| 141 |
-
? "warn"
|
| 142 |
-
: "neutral";
|
| 143 |
-
const backupDetail = data.sync?.message
|
| 144 |
-
? escapeHtml(data.sync.message)
|
| 145 |
-
: "No status yet";
|
| 146 |
-
|
| 147 |
-
const keepaliveConfigured = data.keepalive?.configured === true;
|
| 148 |
-
const keepaliveStatus = String(
|
| 149 |
-
data.keepalive?.status ||
|
| 150 |
-
(process.env.CLOUDFLARE_WORKERS_TOKEN ? "pending" : "not configured"),
|
| 151 |
-
);
|
| 152 |
-
const keepAliveTone = keepaliveConfigured
|
| 153 |
-
? "ok"
|
| 154 |
-
: process.env.CLOUDFLARE_WORKERS_TOKEN
|
| 155 |
-
? "warn"
|
| 156 |
-
: "neutral";
|
| 157 |
-
const keepAliveDetail = keepaliveConfigured
|
| 158 |
-
? `Pinging <code>${escapeHtml(data.keepalive.targetUrl || "/health")}</code>`
|
| 159 |
-
: process.env.CLOUDFLARE_WORKERS_TOKEN
|
| 160 |
-
? "Worker pending or failed"
|
| 161 |
-
: "Not configured";
|
| 162 |
|
| 163 |
const tiles = [
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
title: "Runtime",
|
| 181 |
-
value: escapeHtml(data.uptimeHuman),
|
| 182 |
-
detail: `Public Port ${PORT}`,
|
| 183 |
-
tone: "neutral",
|
| 184 |
-
}),
|
| 185 |
-
renderTile({
|
| 186 |
-
title: "Telegram",
|
| 187 |
-
value: toneBadge(
|
| 188 |
-
TELEGRAM_ENABLED ? "Enabled" : "Disabled",
|
| 189 |
-
TELEGRAM_ENABLED ? "ok" : "neutral",
|
| 190 |
-
),
|
| 191 |
-
detail: TELEGRAM_ENABLED ? "Bot Channel active" : "Not configured",
|
| 192 |
-
tone: TELEGRAM_ENABLED ? "ok" : "neutral",
|
| 193 |
-
}),
|
| 194 |
-
renderTile({
|
| 195 |
-
title: "Backup",
|
| 196 |
-
value: toneBadge(syncStatus.toUpperCase(), syncTone),
|
| 197 |
-
detail: backupDetail,
|
| 198 |
-
tone: syncTone,
|
| 199 |
-
meta: data.sync?.timestamp
|
| 200 |
-
? `<span class="local-time" data-iso="${data.sync.timestamp}"></span>`
|
| 201 |
-
: "",
|
| 202 |
-
}),
|
| 203 |
-
renderTile({
|
| 204 |
-
title: "Keep Awake",
|
| 205 |
-
value: toneBadge(
|
| 206 |
-
keepaliveConfigured ? "CF Cron" : keepaliveStatus.toUpperCase(),
|
| 207 |
-
keepAliveTone,
|
| 208 |
-
),
|
| 209 |
-
detail: keepAliveDetail,
|
| 210 |
-
tone: keepAliveTone,
|
| 211 |
-
}),
|
| 212 |
-
].join("");
|
| 213 |
-
|
| 214 |
-
return `<!doctype html>
|
| 215 |
-
<html lang="en">
|
| 216 |
-
<head>
|
| 217 |
-
<meta charset="utf-8" />
|
| 218 |
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 219 |
<title>HuggingClaw</title>
|
| 220 |
<style>
|
| 221 |
-
:root
|
| 222 |
-
*
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
.
|
| 228 |
-
.hero-action
|
| 229 |
-
.
|
| 230 |
-
.
|
| 231 |
-
.tile
|
| 232 |
-
.tile
|
| 233 |
-
.tile
|
| 234 |
-
.tile
|
| 235 |
-
.tile
|
| 236 |
-
.tile-
|
| 237 |
-
.tile-
|
| 238 |
-
|
| 239 |
-
.
|
| 240 |
-
.
|
| 241 |
-
.
|
| 242 |
-
.
|
| 243 |
-
.
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
</style>
|
| 255 |
-
</head>
|
| 256 |
-
<body>
|
| 257 |
-
<main>
|
| 258 |
-
<header>
|
| 259 |
-
<h1>🦞 HuggingClaw</h1>
|
| 260 |
-
<div class="subtitle">OpenClaw Gateway Dashboard</div>
|
| 261 |
-
</header>
|
| 262 |
-
<a class="hero-action" href="${APP_BASE}/" target="_blank" rel="noopener noreferrer">Open Control UI -></a>
|
| 263 |
-
<section class="overview">
|
| 264 |
-
${tiles}
|
| 265 |
-
</section>
|
| 266 |
-
<footer>Built by <a href="https://github.com/somratpro" target="_blank" rel="noopener noreferrer" style="color: var(--accent); text-decoration: none;">@somratpro</a></footer>
|
| 267 |
</main>
|
| 268 |
-
<script>
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
}
|
|
|
|
|
|
|
|
|
|
| 274 |
});
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
}
|
| 279 |
|
|
|
|
| 280 |
const server = http.createServer(async (req, res) => {
|
| 281 |
-
const
|
| 282 |
-
const pathname = url.pathname;
|
| 283 |
|
| 284 |
-
// 1. Dashboard Routes
|
| 285 |
if (pathname === "/health") {
|
| 286 |
-
const gatewayReady = await
|
| 287 |
-
res.writeHead(gatewayReady ? 200 : 503, {
|
| 288 |
-
|
| 289 |
-
});
|
| 290 |
-
return res.end(
|
| 291 |
-
JSON.stringify({
|
| 292 |
-
status: gatewayReady ? "ok" : "degraded",
|
| 293 |
-
gatewayReady,
|
| 294 |
-
uptime: formatUptime(Date.now() - startTime),
|
| 295 |
-
sync: getSyncStatus(),
|
| 296 |
-
keepalive: getKeepaliveStatus(),
|
| 297 |
-
}),
|
| 298 |
-
);
|
| 299 |
}
|
| 300 |
|
| 301 |
if (pathname === "/status") {
|
| 302 |
-
const gatewayReady = await
|
|
|
|
|
|
|
|
|
|
| 303 |
res.writeHead(200, { "Content-Type": "application/json" });
|
| 304 |
-
return res.end(
|
| 305 |
-
JSON.stringify({
|
| 306 |
-
model: LLM_MODEL,
|
| 307 |
-
uptime: formatUptime(Date.now() - startTime),
|
| 308 |
-
gatewayReady,
|
| 309 |
-
sync: getSyncStatus(),
|
| 310 |
-
whatsapp: readGuardianStatus(),
|
| 311 |
-
keepalive: getKeepaliveStatus(),
|
| 312 |
-
}),
|
| 313 |
-
);
|
| 314 |
}
|
| 315 |
|
| 316 |
if (pathname === "/" || pathname === "/dashboard") {
|
| 317 |
-
const gatewayReady = await
|
|
|
|
|
|
|
|
|
|
| 318 |
res.writeHead(200, { "Content-Type": "text/html" });
|
| 319 |
-
return res.end(
|
| 320 |
-
renderDashboard({
|
| 321 |
-
uptimeHuman: formatUptime(Date.now() - startTime),
|
| 322 |
-
gatewayReady,
|
| 323 |
-
sync: getSyncStatus(),
|
| 324 |
-
whatsapp: readGuardianStatus(),
|
| 325 |
-
keepalive: getKeepaliveStatus(),
|
| 326 |
-
}),
|
| 327 |
-
);
|
| 328 |
}
|
| 329 |
|
| 330 |
-
//
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
"x-forwarded-host": req.headers.host,
|
| 336 |
-
"x-forwarded-proto": "https",
|
| 337 |
-
};
|
| 338 |
-
|
| 339 |
-
const proxyReq = http.request(
|
| 340 |
-
{
|
| 341 |
-
hostname: GATEWAY_HOST,
|
| 342 |
-
port: GATEWAY_PORT,
|
| 343 |
-
path: pathname + url.search,
|
| 344 |
-
method: req.method,
|
| 345 |
-
headers: proxyHeaders,
|
| 346 |
-
},
|
| 347 |
-
(proxyRes) => {
|
| 348 |
-
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
| 349 |
-
proxyRes.pipe(res);
|
| 350 |
-
proxyRes.on("error", (err) => {
|
| 351 |
-
console.error("proxyRes error:", err);
|
| 352 |
-
res.end();
|
| 353 |
-
});
|
| 354 |
-
},
|
| 355 |
-
);
|
| 356 |
-
|
| 357 |
-
req.on("error", (err) => {
|
| 358 |
-
console.error("req error:", err);
|
| 359 |
-
proxyReq.destroy();
|
| 360 |
-
});
|
| 361 |
-
|
| 362 |
-
res.on("error", (err) => {
|
| 363 |
-
console.error("res error:", err);
|
| 364 |
-
proxyReq.destroy();
|
| 365 |
-
});
|
| 366 |
-
|
| 367 |
-
proxyReq.on("error", (err) => {
|
| 368 |
-
console.error("proxyReq error:", err);
|
| 369 |
-
if (!res.headersSent) {
|
| 370 |
-
res.writeHead(503, { "Content-Type": "application/json" });
|
| 371 |
-
res.end(
|
| 372 |
-
JSON.stringify({
|
| 373 |
-
status: "starting",
|
| 374 |
-
message: "Gateway is initializing... or connection failed",
|
| 375 |
-
}),
|
| 376 |
-
);
|
| 377 |
-
} else {
|
| 378 |
-
res.end();
|
| 379 |
}
|
| 380 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
|
| 382 |
-
|
|
|
|
| 383 |
});
|
| 384 |
|
|
|
|
| 385 |
server.on("upgrade", (req, socket, head) => {
|
| 386 |
-
const
|
| 387 |
-
const
|
| 388 |
-
const
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
| 393 |
-
|
|
|
|
|
|
|
|
|
|
| 394 |
}
|
| 395 |
-
|
| 396 |
-
if (head && head.length)
|
| 397 |
-
|
| 398 |
});
|
| 399 |
-
|
| 400 |
});
|
| 401 |
|
| 402 |
server.timeout = 0;
|
| 403 |
server.keepAliveTimeout = 65000;
|
| 404 |
server.listen(PORT, "0.0.0.0", () =>
|
| 405 |
-
console.log(
|
| 406 |
-
`🦞 HuggingClaw Dashboard on ${PORT} -> Gateway on ${GATEWAY_PORT}`,
|
| 407 |
-
),
|
| 408 |
);
|
|
|
|
| 1 |
+
// Single public entrypoint for HF Spaces: dashboard + reverse proxy to OpenClaw + JupyterLab.
|
| 2 |
const http = require("http");
|
| 3 |
const fs = require("fs");
|
| 4 |
const net = require("net");
|
|
|
|
| 6 |
const PORT = 7861;
|
| 7 |
const GATEWAY_PORT = 7860;
|
| 8 |
const GATEWAY_HOST = "127.0.0.1";
|
| 9 |
+
const JUPYTER_PORT = 8888;
|
| 10 |
+
const JUPYTER_HOST = "127.0.0.1";
|
| 11 |
+
const JUPYTER_BASE = "/terminal";
|
| 12 |
+
const DEV_MODE_ENABLED = /^(true|1|yes|on)$/i.test(process.env.DEV_MODE || "");
|
| 13 |
+
const JUPYTER_ENABLED = /^(true|1|yes|on)$/i.test(
|
| 14 |
+
process.env.HUGGINGCLAW_JUPYTER_ENABLED || (DEV_MODE_ENABLED ? "true" : "false")
|
| 15 |
+
);
|
| 16 |
const startTime = Date.now();
|
| 17 |
const LLM_MODEL = process.env.LLM_MODEL || "Not Set";
|
| 18 |
const TELEGRAM_ENABLED = !!process.env.TELEGRAM_BOT_TOKEN;
|
|
|
|
| 26 |
"/tmp/huggingclaw-cloudflare-keepalive-status.json";
|
| 27 |
|
| 28 |
function parseRequestUrl(url) {
|
| 29 |
+
try { return new URL(url, "http://localhost"); }
|
| 30 |
+
catch { return new URL("http://localhost/"); }
|
|
|
|
|
|
|
|
|
|
| 31 |
}
|
| 32 |
|
| 33 |
function getSyncStatus() {
|
| 34 |
try {
|
| 35 |
+
if (fs.existsSync(SYNC_STATUS_FILE))
|
| 36 |
return JSON.parse(fs.readFileSync(SYNC_STATUS_FILE, "utf8"));
|
|
|
|
| 37 |
} catch {}
|
| 38 |
+
if (HF_BACKUP_ENABLED)
|
| 39 |
+
return { status: "configured", message: `Backup enabled. Waiting for sync window (${SYNC_INTERVAL}s).` };
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
return { status: "unknown", message: "No sync data yet" };
|
| 41 |
}
|
| 42 |
|
| 43 |
function readGuardianStatus() {
|
| 44 |
+
if (!WHATSAPP_ENABLED) return { configured: false, connected: false, pairing: false };
|
|
|
|
|
|
|
| 45 |
try {
|
| 46 |
if (fs.existsSync(WHATSAPP_STATUS_FILE)) {
|
| 47 |
+
const p = JSON.parse(fs.readFileSync(WHATSAPP_STATUS_FILE, "utf8"));
|
| 48 |
+
return { configured: p.configured !== false, connected: p.connected === true, pairing: p.pairing === true };
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
}
|
| 50 |
} catch {}
|
| 51 |
return { configured: true, connected: false, pairing: false };
|
|
|
|
| 53 |
|
| 54 |
function getKeepaliveStatus() {
|
| 55 |
try {
|
| 56 |
+
if (fs.existsSync(CLOUDFLARE_KEEPALIVE_STATUS_FILE))
|
| 57 |
+
return JSON.parse(fs.readFileSync(CLOUDFLARE_KEEPALIVE_STATUS_FILE, "utf8"));
|
|
|
|
|
|
|
|
|
|
| 58 |
} catch {}
|
| 59 |
return null;
|
| 60 |
}
|
| 61 |
|
| 62 |
+
function probePort(host, port, path, timeoutMs = 1500) {
|
| 63 |
return new Promise((resolve) => {
|
| 64 |
+
const req = http.get({ hostname: host, port, path, timeout: timeoutMs }, (res) => {
|
| 65 |
+
res.resume();
|
| 66 |
+
resolve(res.statusCode >= 200 && res.statusCode < 500);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
});
|
| 68 |
+
req.on("timeout", () => { req.destroy(); resolve(false); });
|
| 69 |
+
req.on("error", () => resolve(false));
|
| 70 |
});
|
| 71 |
}
|
| 72 |
|
| 73 |
function formatUptime(ms) {
|
| 74 |
+
const t = Math.floor(ms / 1000);
|
| 75 |
+
const d = Math.floor(t / 86400), h = Math.floor((t % 86400) / 3600), m = Math.floor((t % 3600) / 60);
|
| 76 |
+
if (d) return `${d}d ${h}h ${m}m`;
|
| 77 |
+
if (h) return `${h}h ${m}m`;
|
| 78 |
+
return `${m}m`;
|
|
|
|
|
|
|
| 79 |
}
|
| 80 |
|
| 81 |
+
function escapeHtml(v) {
|
| 82 |
+
return String(v).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
}
|
| 84 |
|
| 85 |
+
function badge(label, tone = "neutral") {
|
| 86 |
return `<span class="badge ${tone}">${escapeHtml(label)}</span>`;
|
| 87 |
}
|
| 88 |
|
| 89 |
+
function tile({ title, value, detail = "", tone = "neutral", meta = "" }) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
return `<article class="tile ${tone}">
|
| 91 |
+
<div class="tile-head"><span class="tile-title">${escapeHtml(title)}</span><span class="tile-dot"></span></div>
|
|
|
|
|
|
|
|
|
|
| 92 |
<div class="tile-value">${value}</div>
|
| 93 |
${detail ? `<div class="tile-detail">${detail}</div>` : ""}
|
| 94 |
${meta ? `<div class="tile-meta">${meta}</div>` : ""}
|
|
|
|
| 97 |
|
| 98 |
function renderDashboard(data) {
|
| 99 |
const syncStatus = String(data.sync?.status || "unknown");
|
| 100 |
+
const syncTone = ["success","restored","synced","configured"].includes(syncStatus) ? "ok" : syncStatus === "disabled" ? "warn" : "neutral";
|
| 101 |
+
const kaConf = data.keepalive?.configured === true;
|
| 102 |
+
const kaStatus = String(data.keepalive?.status || (process.env.CLOUDFLARE_WORKERS_TOKEN ? "pending" : "not configured"));
|
| 103 |
+
const kaTone = kaConf ? "ok" : process.env.CLOUDFLARE_WORKERS_TOKEN ? "warn" : "neutral";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
const tiles = [
|
| 106 |
+
tile({ title: "Gateway", value: badge(data.gatewayReady ? "Online" : "Offline", data.gatewayReady ? "ok" : "off"), detail: `OpenClaw on internal port ${GATEWAY_PORT}`, tone: data.gatewayReady ? "ok" : "off" }),
|
| 107 |
+
tile({ title: "Model", value: `<code>${escapeHtml(LLM_MODEL)}</code>`, detail: "Primary LLM configured", tone: "neutral" }),
|
| 108 |
+
tile({ title: "Runtime", value: escapeHtml(data.uptimeHuman), detail: `Public port ${PORT}`, tone: "neutral" }),
|
| 109 |
+
tile({ title: "Telegram", value: badge(TELEGRAM_ENABLED ? "Enabled" : "Disabled", TELEGRAM_ENABLED ? "ok" : "neutral"), detail: TELEGRAM_ENABLED ? "Bot channel active" : "Not configured", tone: TELEGRAM_ENABLED ? "ok" : "neutral" }),
|
| 110 |
+
tile({ title: "Backup", value: badge(syncStatus.toUpperCase(), syncTone), detail: escapeHtml(data.sync?.message || "No status yet"), tone: syncTone, meta: data.sync?.timestamp ? `<span class="local-time" data-iso="${data.sync.timestamp}"></span>` : "" }),
|
| 111 |
+
tile({ title: "Keep Awake", value: badge(kaConf ? "CF Cron" : kaStatus.toUpperCase(), kaTone), detail: kaConf ? `Pinging <code>${escapeHtml(data.keepalive?.targetUrl || "/health")}</code>` : process.env.CLOUDFLARE_WORKERS_TOKEN ? "Worker pending or failed" : "Not configured", tone: kaTone }),
|
| 112 |
+
];
|
| 113 |
+
|
| 114 |
+
if (JUPYTER_ENABLED) {
|
| 115 |
+
tiles.push(tile({ title: "Terminal", value: badge(data.jupyterReady ? "Online" : "Starting…", data.jupyterReady ? "ok" : "warn"), detail: `JupyterLab at <a href="${JUPYTER_BASE}/" style="color:inherit">${JUPYTER_BASE}/</a>`, tone: data.jupyterReady ? "ok" : "warn" }));
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
const tilesHtml = tiles.join("");
|
| 119 |
+
|
| 120 |
+
return `<!doctype html><html lang="en"><head>
|
| 121 |
+
<meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
<title>HuggingClaw</title>
|
| 123 |
<style>
|
| 124 |
+
:root{color-scheme:dark;--bg:#08080f;--panel:#12111b;--line:#26243a;--text:#f6f4ff;--muted:#7f7a9e;--soft:#b8b3d7;--good:#22c55e;--warn:#f5c542;--bad:#fb7185}
|
| 125 |
+
*{box-sizing:border-box}body{margin:0;min-height:100vh;font-family:Inter,ui-sans-serif,system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--text);font-size:13px}
|
| 126 |
+
main{width:min(720px,calc(100% - 32px));margin:0 auto;padding:36px 0 44px}
|
| 127 |
+
header{text-align:center;margin-bottom:22px}h1{margin:0;font-size:1.65rem;line-height:1}
|
| 128 |
+
.subtitle{margin-top:12px;color:var(--muted);font-size:.72rem;text-transform:uppercase;letter-spacing:.14em;font-weight:800}
|
| 129 |
+
.btn-row{display:flex;gap:12px;margin:24px 0 20px}
|
| 130 |
+
.hero-action{display:flex;flex:1;min-height:46px;align-items:center;justify-content:center;border-radius:8px;background:#fff;color:#000;text-decoration:none;font-weight:850;font-size:.98rem;transition:opacity .15s}
|
| 131 |
+
.hero-action:hover{opacity:.9}.hero-action.terminal{background:#1e1e2e;color:#cdd6f4;border:1px solid #45475a}
|
| 132 |
+
.overview{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-bottom:10px}
|
| 133 |
+
.tile{border:1px solid var(--line);background:var(--panel);border-radius:11px;padding:18px;min-height:124px;display:flex;flex-direction:column;gap:10px}
|
| 134 |
+
.tile.ok{border-color:rgba(34,197,94,.22)}.tile.warn{border-color:rgba(245,197,66,.24)}.tile.off{border-color:rgba(251,113,133,.28)}
|
| 135 |
+
.tile-head{display:flex;align-items:center;justify-content:space-between;gap:12px}
|
| 136 |
+
.tile-title{color:var(--muted);font-size:.67rem;letter-spacing:.18em;text-transform:uppercase;font-weight:850}
|
| 137 |
+
.tile-dot{width:7px;height:7px;border-radius:50%;background:var(--line)}
|
| 138 |
+
.tile.ok .tile-dot{background:var(--good)}.tile.warn .tile-dot{background:var(--warn)}.tile.off .tile-dot{background:var(--bad)}
|
| 139 |
+
.tile-value{font-size:1.12rem;font-weight:850;overflow-wrap:anywhere}.tile-detail{color:var(--soft);line-height:1.45;font-size:.83rem}
|
| 140 |
+
.tile-meta{color:var(--muted);line-height:1.4;font-size:.75rem;margin-top:auto;overflow-wrap:anywhere}
|
| 141 |
+
code{background:#232234;border:1px solid #34324c;border-radius:6px;padding:2px 6px;color:var(--text);font-size:.9em}
|
| 142 |
+
.badge{display:inline-flex;align-items:center;width:max-content;border:1px solid var(--line);border-radius:999px;padding:5px 10px;font-size:.72rem;font-weight:850;line-height:1;text-transform:uppercase}
|
| 143 |
+
.badge.ok{color:var(--good);border-color:rgba(34,197,94,.34);background:rgba(34,197,94,.11)}
|
| 144 |
+
.badge.warn{color:var(--warn);border-color:rgba(245,197,66,.34);background:rgba(245,197,66,.11)}
|
| 145 |
+
.badge.off{color:var(--bad);border-color:rgba(251,113,133,.34);background:rgba(251,113,133,.11)}
|
| 146 |
+
.badge.neutral{color:var(--soft)}
|
| 147 |
+
footer{color:var(--muted);text-align:center;font-size:.74rem;margin-top:18px}
|
| 148 |
+
@media(max-width:700px){.overview{grid-template-columns:1fr}main{width:min(100% - 22px,720px);padding-top:28px}.btn-row{flex-direction:column}}
|
| 149 |
+
</style></head><body><main>
|
| 150 |
+
<header><h1>🦞 HuggingClaw</h1><div class="subtitle">OpenClaw Gateway</div></header>
|
| 151 |
+
<div class="btn-row">
|
| 152 |
+
<a class="hero-action" href="${APP_BASE}/">Open Control UI →</a>
|
| 153 |
+
${JUPYTER_ENABLED ? `<a class="hero-action terminal" href="${JUPYTER_BASE}/">💻 Open Terminal →</a>` : ""}
|
| 154 |
+
</div>
|
| 155 |
+
<section class="overview">${tilesHtml}</section>
|
| 156 |
+
<footer>Built by <a href="https://github.com/somratpro" target="_blank" rel="noopener noreferrer" style="color:inherit;text-decoration:none">@somratpro</a>${JUPYTER_ENABLED ? " · Terminal by JupyterLab" : ""}<br><span>Public Spaces can be opened directly via <code>.hf.space</code>; private Spaces require the App tab session.</span></footer>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
</main>
|
| 158 |
+
<script>document.querySelectorAll('.local-time').forEach(el=>{const d=new Date(el.getAttribute('data-iso'));if(!isNaN(d))el.textContent='At '+d.toLocaleTimeString()});</script>
|
| 159 |
+
</body></html>`;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
// ── Generic proxy ──
|
| 163 |
+
function proxiedPath(url, { stripPrefix = "" } = {}) {
|
| 164 |
+
if (!stripPrefix) return url.pathname + url.search;
|
| 165 |
+
if (url.pathname === stripPrefix) return "/" + url.search;
|
| 166 |
+
if (url.pathname.startsWith(stripPrefix + "/")) {
|
| 167 |
+
return url.pathname.slice(stripPrefix.length) + url.search;
|
| 168 |
+
}
|
| 169 |
+
return url.pathname + url.search;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
function rewriteProxyHeaders(headers, { publicPrefix = "", targetHost = "", targetPort = "" } = {}) {
|
| 173 |
+
const next = { ...headers };
|
| 174 |
+
|
| 175 |
+
// Keep browser redirects inside the public HF Space path. Backends may emit
|
| 176 |
+
// root-relative redirects ("/login") or absolute redirects pointing at their
|
| 177 |
+
// internal listener ("http://127.0.0.1:8888/..."). Both break from a browser
|
| 178 |
+
// if we do not normalize them back to the public mount path.
|
| 179 |
+
if (publicPrefix && typeof next.location === "string") {
|
| 180 |
+
try {
|
| 181 |
+
const internalOrigins = new Set([
|
| 182 |
+
"http://huggingclaw.local",
|
| 183 |
+
`http://${targetHost}:${targetPort}`,
|
| 184 |
+
`http://localhost:${targetPort}`,
|
| 185 |
+
`http://127.0.0.1:${targetPort}`,
|
| 186 |
+
]);
|
| 187 |
+
const location = new URL(next.location, "http://huggingclaw.local");
|
| 188 |
+
if (internalOrigins.has(location.origin)) {
|
| 189 |
+
let path = location.pathname;
|
| 190 |
+
if (path !== publicPrefix && !path.startsWith(publicPrefix + "/")) {
|
| 191 |
+
path = publicPrefix + (path.startsWith("/") ? path : `/${path}`);
|
| 192 |
+
}
|
| 193 |
+
next.location = path + location.search + location.hash;
|
| 194 |
+
}
|
| 195 |
+
} catch {}
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
return next;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
function sendServiceUnavailable(res) {
|
| 202 |
+
if (!res.headersSent) {
|
| 203 |
+
res.writeHead(503, { "Content-Type": "application/json" });
|
| 204 |
+
res.end(JSON.stringify({ status: "starting", message: "Service is initializing… please wait." }));
|
| 205 |
+
} else {
|
| 206 |
+
res.end();
|
| 207 |
+
}
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
function proxyHTTP(req, res, targetHost, targetPort, options = {}) {
|
| 211 |
+
const url = parseRequestUrl(req.url);
|
| 212 |
+
const headers = {
|
| 213 |
+
...req.headers,
|
| 214 |
+
host: `${targetHost}:${targetPort}`,
|
| 215 |
+
"x-forwarded-for": req.socket.remoteAddress,
|
| 216 |
+
"x-forwarded-host": req.headers.host,
|
| 217 |
+
"x-forwarded-proto": "https",
|
| 218 |
+
"x-forwarded-prefix": options.publicPrefix || "",
|
| 219 |
+
};
|
| 220 |
+
|
| 221 |
+
const canReplayRequest = req.method === "GET" || req.method === "HEAD";
|
| 222 |
+
const proxyOnce = (path, retryOn404) => {
|
| 223 |
+
const pr = http.request({ hostname: targetHost, port: targetPort, path, method: req.method, headers }, (pres) => {
|
| 224 |
+
if (canReplayRequest && retryOn404 && pres.statusCode === 404 && options.stripPrefix) {
|
| 225 |
+
pres.resume();
|
| 226 |
+
return proxyOnce(proxiedPath(url, { stripPrefix: options.stripPrefix }), false);
|
| 227 |
}
|
| 228 |
+
res.writeHead(pres.statusCode, rewriteProxyHeaders(pres.headers, { ...options, targetHost, targetPort }));
|
| 229 |
+
pres.pipe(res);
|
| 230 |
+
pres.on("error", () => res.end());
|
| 231 |
});
|
| 232 |
+
req.on("error", () => pr.destroy());
|
| 233 |
+
res.on("error", () => pr.destroy());
|
| 234 |
+
pr.on("error", () => sendServiceUnavailable(res));
|
| 235 |
+
req.pipe(pr);
|
| 236 |
+
};
|
| 237 |
+
|
| 238 |
+
// First try the public path as-is because OpenClaw and JupyterLab are both
|
| 239 |
+
// configured with base paths. If a backend still returns 404, retry with the
|
| 240 |
+
// mount prefix stripped; that covers images built before the base-path config
|
| 241 |
+
// took effect and avoids the common HF Spaces "404 at /app or /terminal" trap.
|
| 242 |
+
proxyOnce(url.pathname + url.search, !!options.retryWithoutPrefixOn404);
|
| 243 |
}
|
| 244 |
|
| 245 |
+
// ── HTTP server ──
|
| 246 |
const server = http.createServer(async (req, res) => {
|
| 247 |
+
const { pathname } = parseRequestUrl(req.url);
|
|
|
|
| 248 |
|
|
|
|
| 249 |
if (pathname === "/health") {
|
| 250 |
+
const gatewayReady = await probePort(GATEWAY_HOST, GATEWAY_PORT, "/health");
|
| 251 |
+
res.writeHead(gatewayReady ? 200 : 503, { "Content-Type": "application/json" });
|
| 252 |
+
return res.end(JSON.stringify({ status: gatewayReady ? "ok" : "degraded", gatewayReady, uptime: formatUptime(Date.now() - startTime), sync: getSyncStatus(), keepalive: getKeepaliveStatus() }));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
}
|
| 254 |
|
| 255 |
if (pathname === "/status") {
|
| 256 |
+
const [gatewayReady, jupyterReady] = await Promise.all([
|
| 257 |
+
probePort(GATEWAY_HOST, GATEWAY_PORT, "/health"),
|
| 258 |
+
JUPYTER_ENABLED ? probePort(JUPYTER_HOST, JUPYTER_PORT, `${JUPYTER_BASE}/api`) : Promise.resolve(false),
|
| 259 |
+
]);
|
| 260 |
res.writeHead(200, { "Content-Type": "application/json" });
|
| 261 |
+
return res.end(JSON.stringify({ model: LLM_MODEL, uptime: formatUptime(Date.now() - startTime), gatewayReady, jupyterReady, sync: getSyncStatus(), whatsapp: readGuardianStatus(), keepalive: getKeepaliveStatus() }));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
}
|
| 263 |
|
| 264 |
if (pathname === "/" || pathname === "/dashboard") {
|
| 265 |
+
const [gatewayReady, jupyterReady] = await Promise.all([
|
| 266 |
+
probePort(GATEWAY_HOST, GATEWAY_PORT, "/health"),
|
| 267 |
+
JUPYTER_ENABLED ? probePort(JUPYTER_HOST, JUPYTER_PORT, `${JUPYTER_BASE}/api`) : Promise.resolve(false),
|
| 268 |
+
]);
|
| 269 |
res.writeHead(200, { "Content-Type": "text/html" });
|
| 270 |
+
return res.end(renderDashboard({ uptimeHuman: formatUptime(Date.now() - startTime), gatewayReady, jupyterReady, sync: getSyncStatus(), whatsapp: readGuardianStatus(), keepalive: getKeepaliveStatus() }));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
}
|
| 272 |
|
| 273 |
+
// JupyterLab terminal
|
| 274 |
+
if (pathname === JUPYTER_BASE || pathname.startsWith(JUPYTER_BASE + "/")) {
|
| 275 |
+
if (!JUPYTER_ENABLED) {
|
| 276 |
+
res.writeHead(404, { "Content-Type": "application/json" });
|
| 277 |
+
return res.end(JSON.stringify({ status: "disabled", message: "JupyterLab terminal is disabled. Set DEV_MODE=true to enable /terminal/." }));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
}
|
| 279 |
+
return proxyHTTP(req, res, JUPYTER_HOST, JUPYTER_PORT, {
|
| 280 |
+
publicPrefix: JUPYTER_BASE,
|
| 281 |
+
// Jupyter is started with --ServerApp.base_url=/terminal/, so keep the
|
| 282 |
+
// /terminal prefix when proxying. Stripping it breaks static/theme URLs.
|
| 283 |
+
stripPrefix: "",
|
| 284 |
+
retryWithoutPrefixOn404: false,
|
| 285 |
+
});
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
// OpenClaw Control UI mounted under /app. Retry without the mount prefix on
|
| 289 |
+
// 404 so deployments keep working across OpenClaw basePath behavior changes.
|
| 290 |
+
if (pathname === APP_BASE || pathname.startsWith(APP_BASE + "/")) {
|
| 291 |
+
return proxyHTTP(req, res, GATEWAY_HOST, GATEWAY_PORT, {
|
| 292 |
+
publicPrefix: APP_BASE,
|
| 293 |
+
stripPrefix: APP_BASE,
|
| 294 |
+
retryWithoutPrefixOn404: true,
|
| 295 |
+
});
|
| 296 |
+
}
|
| 297 |
|
| 298 |
+
// OpenClaw gateway API/static fallback (everything else)
|
| 299 |
+
proxyHTTP(req, res, GATEWAY_HOST, GATEWAY_PORT);
|
| 300 |
});
|
| 301 |
|
| 302 |
+
// ── WebSocket upgrade (JupyterLab kernels + terminals need this) ──
|
| 303 |
server.on("upgrade", (req, socket, head) => {
|
| 304 |
+
const { pathname, search } = parseRequestUrl(req.url);
|
| 305 |
+
const isJupyter = JUPYTER_ENABLED && (pathname === JUPYTER_BASE || pathname.startsWith(JUPYTER_BASE + "/"));
|
| 306 |
+
const isApp = pathname === APP_BASE || pathname.startsWith(APP_BASE + "/");
|
| 307 |
+
const [targetHost, targetPort] = isJupyter ? [JUPYTER_HOST, JUPYTER_PORT] : [GATEWAY_HOST, GATEWAY_PORT];
|
| 308 |
+
const publicPrefix = isJupyter ? JUPYTER_BASE : isApp ? APP_BASE : "";
|
| 309 |
+
const targetPath = pathname + search;
|
| 310 |
+
|
| 311 |
+
const ps = net.connect(targetPort, targetHost, () => {
|
| 312 |
+
ps.write(`${req.method} ${targetPath} HTTP/${req.httpVersion}\r\n`);
|
| 313 |
+
ps.write(`Host: ${targetHost}:${targetPort}\r\n`);
|
| 314 |
+
ps.write(`X-Forwarded-For: ${req.socket.remoteAddress || ""}\r\n`);
|
| 315 |
+
ps.write(`X-Forwarded-Host: ${req.headers.host || ""}\r\n`);
|
| 316 |
+
ps.write("X-Forwarded-Proto: https\r\n");
|
| 317 |
+
if (publicPrefix) ps.write(`X-Forwarded-Prefix: ${publicPrefix}\r\n`);
|
| 318 |
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
| 319 |
+
const header = req.rawHeaders[i];
|
| 320 |
+
const lower = header.toLowerCase();
|
| 321 |
+
if (["host", "x-forwarded-for", "x-forwarded-host", "x-forwarded-proto", "x-forwarded-prefix"].includes(lower)) continue;
|
| 322 |
+
ps.write(`${header}: ${req.rawHeaders[i + 1]}\r\n`);
|
| 323 |
}
|
| 324 |
+
ps.write("\r\n");
|
| 325 |
+
if (head && head.length) ps.write(head);
|
| 326 |
+
ps.pipe(socket).pipe(ps);
|
| 327 |
});
|
| 328 |
+
ps.on("error", () => socket.destroy());
|
| 329 |
});
|
| 330 |
|
| 331 |
server.timeout = 0;
|
| 332 |
server.keepAliveTimeout = 65000;
|
| 333 |
server.listen(PORT, "0.0.0.0", () =>
|
| 334 |
+
console.log(`🦞 HuggingClaw :${PORT} → Gateway :${GATEWAY_PORT}${JUPYTER_ENABLED ? ` | Terminal :${JUPYTER_PORT} at ${JUPYTER_BASE}/` : " | Terminal disabled"}`),
|
|
|
|
|
|
|
| 335 |
);
|
login.html
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "page.html" %}
|
| 2 |
+
|
| 3 |
+
{% block stylesheet %}
|
| 4 |
+
{% endblock %}
|
| 5 |
+
|
| 6 |
+
{% block site %}
|
| 7 |
+
<div id="jupyter-main-app" class="container" style="text-align:center; max-width: 760px; margin-top: 40px;">
|
| 8 |
+
<img src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg" alt="Hugging Face Logo" style="max-width: 120px; margin-bottom: 24px;">
|
| 9 |
+
<h3>HuggingClaw Terminal</h3>
|
| 10 |
+
<h4>Welcome to JupyterLab</h4>
|
| 11 |
+
<h5>The default token is <span style="color:orange;">huggingface</span></h5>
|
| 12 |
+
<p style="color:#666;">This terminal is mounted at <code>/terminal/</code> inside the same Hugging Face Space as the OpenClaw UI.</p>
|
| 13 |
+
|
| 14 |
+
{% if login_available %}
|
| 15 |
+
<div class="row" style="display:flex; justify-content:center; margin-top:24px;">
|
| 16 |
+
<div class="navbar col-sm-8">
|
| 17 |
+
<div class="navbar-inner">
|
| 18 |
+
<div class="container">
|
| 19 |
+
<div class="center-nav">
|
| 20 |
+
<form action="{{base_url}}login?next={{next}}" method="post" class="navbar-form pull-left">
|
| 21 |
+
{{ xsrf_form_html() | safe }}
|
| 22 |
+
{% if token_available %}
|
| 23 |
+
<label for="password_input"><strong>{% trans %}Jupyter token <span title="This is the secret you set up when deploying your JupyterLab terminal">ⓘ</span> {% endtrans %}</strong></label>
|
| 24 |
+
{% else %}
|
| 25 |
+
<label for="password_input"><strong>{% trans %}Jupyter password:{% endtrans %}</strong></label>
|
| 26 |
+
{% endif %}
|
| 27 |
+
<input type="password" name="password" id="password_input" class="form-control">
|
| 28 |
+
<button type="submit" class="btn btn-default" id="login_submit">{% trans %}Log in{% endtrans %}</button>
|
| 29 |
+
</form>
|
| 30 |
+
</div>
|
| 31 |
+
</div>
|
| 32 |
+
</div>
|
| 33 |
+
</div>
|
| 34 |
+
</div>
|
| 35 |
+
{% else %}
|
| 36 |
+
<p>{% trans %}No login available, you shouldn't be seeing this page.{% endtrans %}</p>
|
| 37 |
+
{% endif %}
|
| 38 |
+
|
| 39 |
+
<h5 style="margin-top:28px;"><a href="/dashboard">Back to HuggingClaw dashboard</a></h5>
|
| 40 |
+
<p>This login page is based on the Hugging Face JupyterLab Space template.</p>
|
| 41 |
+
|
| 42 |
+
{% if message %}
|
| 43 |
+
<div class="row">
|
| 44 |
+
{% for key in message %}
|
| 45 |
+
<div class="message {{key}}">
|
| 46 |
+
{{message[key]}}
|
| 47 |
+
</div>
|
| 48 |
+
{% endfor %}
|
| 49 |
+
</div>
|
| 50 |
+
{% endif %}
|
| 51 |
+
{% if token_available %}
|
| 52 |
+
{% block token_message %}
|
| 53 |
+
{% endblock token_message %}
|
| 54 |
+
{% endif %}
|
| 55 |
+
</div>
|
| 56 |
+
{% endblock %}
|
| 57 |
+
|
| 58 |
+
{% block script %}
|
| 59 |
+
{% endblock %}
|
openclaw-sync.py
CHANGED
|
@@ -483,10 +483,7 @@ def _sync_once_unlocked(
|
|
| 483 |
commit_message=f"HuggingClaw sync {time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}",
|
| 484 |
ignore_patterns=[".git/*", ".git"],
|
| 485 |
)
|
| 486 |
-
|
| 487 |
-
prune_remote_deleted_files(repo_id, snapshot_dir)
|
| 488 |
-
except Exception as prune_exc:
|
| 489 |
-
print(f"Warning: could not prune stale remote files: {prune_exc}")
|
| 490 |
finally:
|
| 491 |
shutil.rmtree(snapshot_dir, ignore_errors=True)
|
| 492 |
|
|
|
|
| 483 |
commit_message=f"HuggingClaw sync {time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}",
|
| 484 |
ignore_patterns=[".git/*", ".git"],
|
| 485 |
)
|
| 486 |
+
prune_remote_deleted_files(repo_id, snapshot_dir)
|
|
|
|
|
|
|
|
|
|
| 487 |
finally:
|
| 488 |
shutil.rmtree(snapshot_dir, ignore_errors=True)
|
| 489 |
|
start.sh
CHANGED
|
@@ -21,6 +21,13 @@ WHATSAPP_ENABLED_CONFIGURED=false
|
|
| 21 |
[ "${WHATSAPP_ENABLED+x}" = "x" ] && WHATSAPP_ENABLED_CONFIGURED=true
|
| 22 |
WHATSAPP_ENABLED="${WHATSAPP_ENABLED:-false}"
|
| 23 |
WHATSAPP_ENABLED_NORMALIZED=$(printf '%s' "$WHATSAPP_ENABLED" | tr '[:upper:]' '[:lower:]')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
SYNC_INTERVAL="${SYNC_INTERVAL:-180}"
|
| 25 |
if [ -n "${SPACE_HOST:-}" ]; then
|
| 26 |
OPENCLAW_CONSOLE_LOG_LEVEL="${OPENCLAW_CONSOLE_LOG_LEVEL:-warn}"
|
|
@@ -39,7 +46,7 @@ else
|
|
| 39 |
fi
|
| 40 |
echo ""
|
| 41 |
echo " ╔══════════════════════════════════════════╗"
|
| 42 |
-
echo " ║
|
| 43 |
echo " ╚══════════════════════════════════════════╝"
|
| 44 |
echo ""
|
| 45 |
|
|
@@ -433,6 +440,8 @@ fi
|
|
| 433 |
# plugins that the Control UI/dashboard needs to render correctly
|
| 434 |
# on HF Spaces. Without these the UI shows blank panels.
|
| 435 |
# telegram/whatsapp/browser/acpx are added conditionally below.
|
|
|
|
|
|
|
| 436 |
# DENY: lmstudio crashes on boot when no local server is reachable;
|
| 437 |
# xai PLUGIN (separate from the xai model PROVIDER) is broken in
|
| 438 |
# current OpenClaw releases and prevents gateway start. Disabling
|
|
@@ -452,20 +461,17 @@ if [ "$WHATSAPP_ENABLED_NORMALIZED" = "true" ]; then
|
|
| 452 |
fi
|
| 453 |
|
| 454 |
# Apply plugin allow/deny + per-entry toggles in one jq pass.
|
| 455 |
-
ACPX_DISABLED=false
|
| 456 |
-
if [ "$ACP_PLUGIN_MODE" = "disabled" ]; then ACPX_DISABLED=true; fi
|
| 457 |
BROWSER_DISABLED=true
|
| 458 |
if [ "$BROWSER_SHOULD_ENABLE" = "true" ]; then BROWSER_DISABLED=false; fi
|
| 459 |
|
| 460 |
CONFIG_JSON=$(jq \
|
| 461 |
--argjson allow "$PLUGIN_ALLOW_JSON" \
|
| 462 |
-
--argjson acpxDisabled "$ACPX_DISABLED" \
|
| 463 |
--argjson browserDisabled "$BROWSER_DISABLED" \
|
| 464 |
'.plugins.allow = $allow
|
| 465 |
| .plugins.deny = ["lmstudio","xai"]
|
| 466 |
| .plugins.entries.lmstudio.enabled = false
|
| 467 |
| .plugins.entries.xai.enabled = false
|
| 468 |
-
| (
|
| 469 |
| (if $browserDisabled then
|
| 470 |
.plugins.entries.browser.enabled = false | .browser.enabled = false
|
| 471 |
else . end)' <<<"$CONFIG_JSON")
|
|
@@ -649,8 +655,29 @@ fi
|
|
| 649 |
if [ -n "${CLOUDFLARE_PROXY_URL:-}" ]; then
|
| 650 |
echo "Proxy : ${CLOUDFLARE_PROXY_URL}"
|
| 651 |
fi
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 652 |
if [ -n "${SPACE_HOST:-}" ]; then
|
| 653 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 654 |
fi
|
| 655 |
echo ""
|
| 656 |
|
|
@@ -709,6 +736,47 @@ export LLM_MODEL="$LLM_MODEL"
|
|
| 709 |
node /home/node/app/health-server.js &
|
| 710 |
HEALTH_PID=$!
|
| 711 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 712 |
if [ -n "${CLOUDFLARE_WORKERS_TOKEN:-}" ]; then
|
| 713 |
echo "Setting up Cloudflare KeepAlive monitor..."
|
| 714 |
python3 /home/node/app/cloudflare-keepalive-setup.py || true
|
|
@@ -730,6 +798,9 @@ export PYTHONUSERBASE="${PYTHONUSERBASE:-/home/node/.local}"
|
|
| 730 |
export DEBIAN_FRONTEND="${DEBIAN_FRONTEND:-noninteractive}"
|
| 731 |
STARTUP_FILE="/home/node/.openclaw/workspace/startup.sh"
|
| 732 |
_hc_append() {
|
|
|
|
|
|
|
|
|
|
| 733 |
local line="$*"
|
| 734 |
mkdir -p "$(dirname "$STARTUP_FILE")"
|
| 735 |
touch "$STARTUP_FILE"
|
|
@@ -756,6 +827,28 @@ _hc_append_cmd() {
|
|
| 756 |
_hc_append "$cmd"
|
| 757 |
fi
|
| 758 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 759 |
_hc_allow_openclaw_plugins() {
|
| 760 |
local config="/home/node/.openclaw/openclaw.json"
|
| 761 |
[ -f "$config" ] || return 0
|
|
@@ -807,7 +900,7 @@ apt-get() {
|
|
| 807 |
_hc_apt_install "$@"
|
| 808 |
local rc=$?
|
| 809 |
if [ $rc -eq 0 ]; then
|
| 810 |
-
_hc_append_cmd "sudo apt-get update && sudo apt-get install -y" "$@"
|
| 811 |
fi
|
| 812 |
return $rc
|
| 813 |
;;
|
|
@@ -834,7 +927,7 @@ apt() {
|
|
| 834 |
_hc_apt_install "$@"
|
| 835 |
local rc=$?
|
| 836 |
if [ $rc -eq 0 ]; then
|
| 837 |
-
_hc_append_cmd "sudo apt-get update && sudo apt-get install -y" "$@"
|
| 838 |
fi
|
| 839 |
return $rc
|
| 840 |
;;
|
|
@@ -861,7 +954,7 @@ pip() {
|
|
| 861 |
command pip "$@"
|
| 862 |
fi
|
| 863 |
local rc=$?
|
| 864 |
-
if [ $rc -eq 0 ] && [ "${1:-}" = "install" ]; then
|
| 865 |
_hc_append_cmd "python3 -m pip install --user" "${@:2}"
|
| 866 |
fi
|
| 867 |
return $rc
|
|
@@ -873,15 +966,39 @@ pip3() {
|
|
| 873 |
command pip3 "$@"
|
| 874 |
fi
|
| 875 |
local rc=$?
|
| 876 |
-
if [ $rc -eq 0 ] && [ "${1:-}" = "install" ]; then
|
| 877 |
_hc_append_cmd "python3 -m pip install --user" "${@:2}"
|
| 878 |
fi
|
| 879 |
return $rc
|
| 880 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 881 |
npm() {
|
| 882 |
command npm "$@"
|
| 883 |
local rc=$?
|
| 884 |
-
if [ $rc -eq 0 ] && [ "${1:-}" = "install" ] && { [ "${2:-}" = "-g" ] || [ "${2:-}" = "--global" ]; }; then
|
| 885 |
_hc_append_cmd "npm install -g" "${@:3}"
|
| 886 |
fi
|
| 887 |
return $rc
|
|
@@ -889,7 +1006,7 @@ npm() {
|
|
| 889 |
openclaw() {
|
| 890 |
command openclaw "$@"
|
| 891 |
local rc=$?
|
| 892 |
-
if [ $rc -eq 0 ] && [ "${1:-}" = "plugins" ] && [ "${2:-}" = "install" ]; then
|
| 893 |
_hc_allow_openclaw_plugins "${@:3}"
|
| 894 |
_hc_append_cmd "openclaw plugins install" "${@:3}"
|
| 895 |
fi
|
|
@@ -935,7 +1052,7 @@ hc_run_startup_command() {
|
|
| 935 |
|
| 936 |
echo "[startup:${source_label}] $command_text"
|
| 937 |
set +e
|
| 938 |
-
bash -lc "$command_text"
|
| 939 |
local rc=$?
|
| 940 |
set -e
|
| 941 |
if [ "$rc" -eq 0 ]; then
|
|
@@ -959,6 +1076,7 @@ hc_run_startup_script() {
|
|
| 959 |
# Load HuggingClaw's install wrappers for env-provided scripts too, so
|
| 960 |
# `apt install`, `pip install`, `npm install -g`, and OpenClaw plugin
|
| 961 |
# installs behave the same way as they do in the interactive shell.
|
|
|
|
| 962 |
echo '[ -f /home/node/.bashrc ] && . /home/node/.bashrc'
|
| 963 |
printf '%s\n' "$script_text"
|
| 964 |
} > "$script_file"
|
|
|
|
| 21 |
[ "${WHATSAPP_ENABLED+x}" = "x" ] && WHATSAPP_ENABLED_CONFIGURED=true
|
| 22 |
WHATSAPP_ENABLED="${WHATSAPP_ENABLED:-false}"
|
| 23 |
WHATSAPP_ENABLED_NORMALIZED=$(printf '%s' "$WHATSAPP_ENABLED" | tr '[:upper:]' '[:lower:]')
|
| 24 |
+
DEV_MODE_RAW="${DEV_MODE:-false}"
|
| 25 |
+
DEV_MODE_NORMALIZED=$(printf '%s' "$DEV_MODE_RAW" | tr '[:upper:]' '[:lower:]')
|
| 26 |
+
DEV_MODE_ENABLED=false
|
| 27 |
+
case "$DEV_MODE_NORMALIZED" in
|
| 28 |
+
true|1|yes|on) DEV_MODE_ENABLED=true ;;
|
| 29 |
+
*) DEV_MODE_ENABLED=false ;;
|
| 30 |
+
esac
|
| 31 |
SYNC_INTERVAL="${SYNC_INTERVAL:-180}"
|
| 32 |
if [ -n "${SPACE_HOST:-}" ]; then
|
| 33 |
OPENCLAW_CONSOLE_LOG_LEVEL="${OPENCLAW_CONSOLE_LOG_LEVEL:-warn}"
|
|
|
|
| 46 |
fi
|
| 47 |
echo ""
|
| 48 |
echo " ╔══════════════════════════════════════════╗"
|
| 49 |
+
echo " ║ 🦞 HuggingClaw + 💻 JupyterLab ║"
|
| 50 |
echo " ╚══════════════════════════════════════════╝"
|
| 51 |
echo ""
|
| 52 |
|
|
|
|
| 440 |
# plugins that the Control UI/dashboard needs to render correctly
|
| 441 |
# on HF Spaces. Without these the UI shows blank panels.
|
| 442 |
# telegram/whatsapp/browser/acpx are added conditionally below.
|
| 443 |
+
# Do not create a disabled acpx entry when the plugin is absent;
|
| 444 |
+
# OpenClaw reports that as a config warning on HF Spaces.
|
| 445 |
# DENY: lmstudio crashes on boot when no local server is reachable;
|
| 446 |
# xai PLUGIN (separate from the xai model PROVIDER) is broken in
|
| 447 |
# current OpenClaw releases and prevents gateway start. Disabling
|
|
|
|
| 461 |
fi
|
| 462 |
|
| 463 |
# Apply plugin allow/deny + per-entry toggles in one jq pass.
|
|
|
|
|
|
|
| 464 |
BROWSER_DISABLED=true
|
| 465 |
if [ "$BROWSER_SHOULD_ENABLE" = "true" ]; then BROWSER_DISABLED=false; fi
|
| 466 |
|
| 467 |
CONFIG_JSON=$(jq \
|
| 468 |
--argjson allow "$PLUGIN_ALLOW_JSON" \
|
|
|
|
| 469 |
--argjson browserDisabled "$BROWSER_DISABLED" \
|
| 470 |
'.plugins.allow = $allow
|
| 471 |
| .plugins.deny = ["lmstudio","xai"]
|
| 472 |
| .plugins.entries.lmstudio.enabled = false
|
| 473 |
| .plugins.entries.xai.enabled = false
|
| 474 |
+
| del(.plugins.entries.acpx)
|
| 475 |
| (if $browserDisabled then
|
| 476 |
.plugins.entries.browser.enabled = false | .browser.enabled = false
|
| 477 |
else . end)' <<<"$CONFIG_JSON")
|
|
|
|
| 655 |
if [ -n "${CLOUDFLARE_PROXY_URL:-}" ]; then
|
| 656 |
echo "Proxy : ${CLOUDFLARE_PROXY_URL}"
|
| 657 |
fi
|
| 658 |
+
RUNTIME_JUPYTER_ENABLED="$DEV_MODE_ENABLED"
|
| 659 |
+
if [ "$DEV_MODE_ENABLED" = "true" ] && ! command -v jupyter-lab >/dev/null 2>&1; then
|
| 660 |
+
echo "DEV_MODE enabled but jupyter-lab is missing; attempting runtime install..."
|
| 661 |
+
if python3 -m pip install --user --no-cache-dir --break-system-packages jupyterlab==4.5.7 tornado==6.5.5 ipywidgets==8.1.8; then
|
| 662 |
+
echo "Runtime Jupyter install complete."
|
| 663 |
+
else
|
| 664 |
+
echo "WARNING: Runtime Jupyter install failed; disabling terminal for this boot."
|
| 665 |
+
RUNTIME_JUPYTER_ENABLED=false
|
| 666 |
+
fi
|
| 667 |
+
fi
|
| 668 |
+
if [ "$RUNTIME_JUPYTER_ENABLED" = "true" ] && ! command -v jupyter-lab >/dev/null 2>&1; then
|
| 669 |
+
echo "WARNING: jupyter-lab still unavailable; disabling terminal for this boot."
|
| 670 |
+
RUNTIME_JUPYTER_ENABLED=false
|
| 671 |
+
fi
|
| 672 |
+
export HUGGINGCLAW_JUPYTER_ENABLED="$RUNTIME_JUPYTER_ENABLED"
|
| 673 |
+
|
| 674 |
if [ -n "${SPACE_HOST:-}" ]; then
|
| 675 |
+
if [ "$RUNTIME_JUPYTER_ENABLED" = "true" ]; then
|
| 676 |
+
echo "Routes : /app/ (Control UI), /terminal/ (JupyterLab)"
|
| 677 |
+
else
|
| 678 |
+
echo "Routes : /app/ (Control UI)"
|
| 679 |
+
fi
|
| 680 |
+
echo "Private : open the Hugging Face App tab first; raw https://${SPACE_HOST}/... links can show HF 404 without the embedded Space session."
|
| 681 |
fi
|
| 682 |
echo ""
|
| 683 |
|
|
|
|
| 736 |
node /home/node/app/health-server.js &
|
| 737 |
HEALTH_PID=$!
|
| 738 |
|
| 739 |
+
# 10.5. Start JupyterLab Terminal on internal port 8888 (DEV_MODE only)
|
| 740 |
+
# Accessible via /terminal/ path through the health-server proxy
|
| 741 |
+
if [ "$RUNTIME_JUPYTER_ENABLED" = "true" ]; then
|
| 742 |
+
JUPYTER_TOKEN="${JUPYTER_TOKEN:-huggingface}"
|
| 743 |
+
JUPYTER_ROOT_DIR="${JUPYTER_ROOT_DIR:-/home/node}"
|
| 744 |
+
mkdir -p "$JUPYTER_ROOT_DIR"
|
| 745 |
+
if [ "$JUPYTER_ROOT_DIR" != "/home/node/app" ]; then
|
| 746 |
+
if [ -L "$JUPYTER_ROOT_DIR/HuggingClaw" ] || [ ! -e "$JUPYTER_ROOT_DIR/HuggingClaw" ]; then
|
| 747 |
+
ln -sfn /home/node/app "$JUPYTER_ROOT_DIR/HuggingClaw"
|
| 748 |
+
fi
|
| 749 |
+
fi
|
| 750 |
+
if [ "$JUPYTER_ROOT_DIR" != "/home/node/.openclaw/workspace" ]; then
|
| 751 |
+
if [ -L "$JUPYTER_ROOT_DIR/HuggingClaw-Workspace" ] || [ ! -e "$JUPYTER_ROOT_DIR/HuggingClaw-Workspace" ]; then
|
| 752 |
+
ln -sfn /home/node/.openclaw/workspace "$JUPYTER_ROOT_DIR/HuggingClaw-Workspace"
|
| 753 |
+
fi
|
| 754 |
+
fi
|
| 755 |
+
|
| 756 |
+
echo "DEV_MODE enabled (${DEV_MODE_RAW}) — starting JupyterLab terminal on internal port 8888 (path: /terminal/) with root: $JUPYTER_ROOT_DIR"
|
| 757 |
+
jupyter-lab \
|
| 758 |
+
--ip 127.0.0.1 \
|
| 759 |
+
--port 8888 \
|
| 760 |
+
--no-browser \
|
| 761 |
+
--IdentityProvider.token="$JUPYTER_TOKEN" \
|
| 762 |
+
--ServerApp.base_url=/terminal/ \
|
| 763 |
+
--ServerApp.terminals_enabled=True \
|
| 764 |
+
--ServerApp.allow_origin='*' \
|
| 765 |
+
--ServerApp.allow_remote_access=True \
|
| 766 |
+
--ServerApp.trust_xheaders=True \
|
| 767 |
+
--ServerApp.tornado_settings="{'headers': {'Content-Security-Policy': 'frame-ancestors *'}}" \
|
| 768 |
+
--IdentityProvider.cookie_options="{'SameSite': 'None', 'Secure': True}" \
|
| 769 |
+
--ServerApp.disable_check_xsrf=True \
|
| 770 |
+
--LabApp.news_url=None \
|
| 771 |
+
--LabApp.check_for_updates_class="jupyterlab.NeverCheckForUpdate" \
|
| 772 |
+
--notebook-dir="$JUPYTER_ROOT_DIR" \
|
| 773 |
+
2>&1 | tee -a /tmp/jupyterlab.log &
|
| 774 |
+
JUPYTER_PID=$!
|
| 775 |
+
echo "JupyterLab started (PID: $JUPYTER_PID)"
|
| 776 |
+
else
|
| 777 |
+
echo "Jupyter terminal disabled for this boot (DEV_MODE=${DEV_MODE_RAW})."
|
| 778 |
+
fi
|
| 779 |
+
|
| 780 |
if [ -n "${CLOUDFLARE_WORKERS_TOKEN:-}" ]; then
|
| 781 |
echo "Setting up Cloudflare KeepAlive monitor..."
|
| 782 |
python3 /home/node/app/cloudflare-keepalive-setup.py || true
|
|
|
|
| 798 |
export DEBIAN_FRONTEND="${DEBIAN_FRONTEND:-noninteractive}"
|
| 799 |
STARTUP_FILE="/home/node/.openclaw/workspace/startup.sh"
|
| 800 |
_hc_append() {
|
| 801 |
+
if [ "${HUGGINGCLAW_CAPTURE_DISABLE:-0}" = "1" ]; then
|
| 802 |
+
return 0
|
| 803 |
+
fi
|
| 804 |
local line="$*"
|
| 805 |
mkdir -p "$(dirname "$STARTUP_FILE")"
|
| 806 |
touch "$STARTUP_FILE"
|
|
|
|
| 827 |
_hc_append "$cmd"
|
| 828 |
fi
|
| 829 |
}
|
| 830 |
+
_hc_args_without_flags() {
|
| 831 |
+
local out=()
|
| 832 |
+
local arg
|
| 833 |
+
for arg in "$@"; do
|
| 834 |
+
case "$arg" in
|
| 835 |
+
''|-) ;;
|
| 836 |
+
--*) ;;
|
| 837 |
+
-*) ;;
|
| 838 |
+
*) out+=("$arg") ;;
|
| 839 |
+
esac
|
| 840 |
+
done
|
| 841 |
+
printf '%s\n' "${out[@]}"
|
| 842 |
+
}
|
| 843 |
+
_hc_has_install_targets() {
|
| 844 |
+
local item
|
| 845 |
+
while IFS= read -r item; do
|
| 846 |
+
[ -n "$item" ] && return 0
|
| 847 |
+
done <<EOF
|
| 848 |
+
$(_hc_args_without_flags "$@")
|
| 849 |
+
EOF
|
| 850 |
+
return 1
|
| 851 |
+
}
|
| 852 |
_hc_allow_openclaw_plugins() {
|
| 853 |
local config="/home/node/.openclaw/openclaw.json"
|
| 854 |
[ -f "$config" ] || return 0
|
|
|
|
| 900 |
_hc_apt_install "$@"
|
| 901 |
local rc=$?
|
| 902 |
if [ $rc -eq 0 ]; then
|
| 903 |
+
_hc_has_install_targets "$@" && _hc_append_cmd "sudo apt-get update && sudo apt-get install -y" "$@"
|
| 904 |
fi
|
| 905 |
return $rc
|
| 906 |
;;
|
|
|
|
| 927 |
_hc_apt_install "$@"
|
| 928 |
local rc=$?
|
| 929 |
if [ $rc -eq 0 ]; then
|
| 930 |
+
_hc_has_install_targets "$@" && _hc_append_cmd "sudo apt-get update && sudo apt-get install -y" "$@"
|
| 931 |
fi
|
| 932 |
return $rc
|
| 933 |
;;
|
|
|
|
| 954 |
command pip "$@"
|
| 955 |
fi
|
| 956 |
local rc=$?
|
| 957 |
+
if [ $rc -eq 0 ] && [ "${1:-}" = "install" ] && _hc_has_install_targets "${@:2}"; then
|
| 958 |
_hc_append_cmd "python3 -m pip install --user" "${@:2}"
|
| 959 |
fi
|
| 960 |
return $rc
|
|
|
|
| 966 |
command pip3 "$@"
|
| 967 |
fi
|
| 968 |
local rc=$?
|
| 969 |
+
if [ $rc -eq 0 ] && [ "${1:-}" = "install" ] && _hc_has_install_targets "${@:2}"; then
|
| 970 |
_hc_append_cmd "python3 -m pip install --user" "${@:2}"
|
| 971 |
fi
|
| 972 |
return $rc
|
| 973 |
}
|
| 974 |
+
python() {
|
| 975 |
+
if [ "${1:-}" = "-m" ] && [ "${2:-}" = "pip" ] && [ "${3:-}" = "install" ] && [ -z "${VIRTUAL_ENV:-}" ] && ! _hc_has_arg --user "${@:3}" && ! _hc_has_arg --prefix "${@:3}"; then
|
| 976 |
+
command python -m pip install --user "${@:4}"
|
| 977 |
+
else
|
| 978 |
+
command python "$@"
|
| 979 |
+
fi
|
| 980 |
+
local rc=$?
|
| 981 |
+
if [ $rc -eq 0 ] && [ "${1:-}" = "-m" ] && [ "${2:-}" = "pip" ] && [ "${3:-}" = "install" ] && _hc_has_install_targets "${@:4}"; then
|
| 982 |
+
_hc_append_cmd "python3 -m pip install --user" "${@:4}"
|
| 983 |
+
fi
|
| 984 |
+
return $rc
|
| 985 |
+
}
|
| 986 |
+
python3() {
|
| 987 |
+
if [ "${1:-}" = "-m" ] && [ "${2:-}" = "pip" ] && [ "${3:-}" = "install" ] && [ -z "${VIRTUAL_ENV:-}" ] && ! _hc_has_arg --user "${@:3}" && ! _hc_has_arg --prefix "${@:3}"; then
|
| 988 |
+
command python3 -m pip install --user "${@:4}"
|
| 989 |
+
else
|
| 990 |
+
command python3 "$@"
|
| 991 |
+
fi
|
| 992 |
+
local rc=$?
|
| 993 |
+
if [ $rc -eq 0 ] && [ "${1:-}" = "-m" ] && [ "${2:-}" = "pip" ] && [ "${3:-}" = "install" ] && _hc_has_install_targets "${@:4}"; then
|
| 994 |
+
_hc_append_cmd "python3 -m pip install --user" "${@:4}"
|
| 995 |
+
fi
|
| 996 |
+
return $rc
|
| 997 |
+
}
|
| 998 |
npm() {
|
| 999 |
command npm "$@"
|
| 1000 |
local rc=$?
|
| 1001 |
+
if [ $rc -eq 0 ] && { [ "${1:-}" = "install" ] || [ "${1:-}" = "i" ]; } && { [ "${2:-}" = "-g" ] || [ "${2:-}" = "--global" ]; } && _hc_has_install_targets "${@:3}"; then
|
| 1002 |
_hc_append_cmd "npm install -g" "${@:3}"
|
| 1003 |
fi
|
| 1004 |
return $rc
|
|
|
|
| 1006 |
openclaw() {
|
| 1007 |
command openclaw "$@"
|
| 1008 |
local rc=$?
|
| 1009 |
+
if [ $rc -eq 0 ] && [ "${1:-}" = "plugins" ] && [ "${2:-}" = "install" ] && _hc_has_install_targets "${@:3}"; then
|
| 1010 |
_hc_allow_openclaw_plugins "${@:3}"
|
| 1011 |
_hc_append_cmd "openclaw plugins install" "${@:3}"
|
| 1012 |
fi
|
|
|
|
| 1052 |
|
| 1053 |
echo "[startup:${source_label}] $command_text"
|
| 1054 |
set +e
|
| 1055 |
+
HUGGINGCLAW_CAPTURE_DISABLE=1 bash -lc "$command_text"
|
| 1056 |
local rc=$?
|
| 1057 |
set -e
|
| 1058 |
if [ "$rc" -eq 0 ]; then
|
|
|
|
| 1076 |
# Load HuggingClaw's install wrappers for env-provided scripts too, so
|
| 1077 |
# `apt install`, `pip install`, `npm install -g`, and OpenClaw plugin
|
| 1078 |
# installs behave the same way as they do in the interactive shell.
|
| 1079 |
+
echo 'export HUGGINGCLAW_CAPTURE_DISABLE=1'
|
| 1080 |
echo '[ -f /home/node/.bashrc ] && . /home/node/.bashrc'
|
| 1081 |
printf '%s\n' "$script_text"
|
| 1082 |
} > "$script_file"
|