Sync from Heretek-AI/ProxmoxVE - 2026-03-21

This commit is contained in:
github-actions[bot]
2026-03-21 17:23:30 +00:00
parent a0e8f36b07
commit abbbab510f
622 changed files with 146005 additions and 2818 deletions
+15 -14
View File
@@ -32,7 +32,8 @@ function update_script() {
fi
# Get current version
CURRENT_VERSION=$(su - openclaw -c "export PATH=/home/openclaw/.npm-global/bin:\$PATH && openclaw --version" 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
# Using sudo -u since openclaw user has nologin shell
CURRENT_VERSION=$(sudo -u openclaw env PATH="/home/openclaw/.npm-global/bin:$PATH" openclaw --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
# Get latest version from npm
LATEST_VERSION=$(npm view openclaw version 2>/dev/null || echo "unknown")
@@ -50,7 +51,7 @@ function update_script() {
msg_info "Updating OpenClaw from v${CURRENT_VERSION} to v${LATEST_VERSION}"
msg_info "Stopping Gateway Service"
su - openclaw -c "systemctl --user stop openclaw-gateway" 2>/dev/null || true
sudo -u openclaw systemctl --user stop openclaw-gateway 2>/dev/null || true
msg_ok "Stopped Gateway Service"
msg_info "Backing up Configuration"
@@ -64,12 +65,12 @@ function update_script() {
fi
msg_info "Updating OpenClaw Package"
# Run npm update as the openclaw user, not root
su - openclaw -c "export PATH=/home/openclaw/.npm-global/bin:\$PATH && npm update -g openclaw" 2>/dev/null
# Run npm update as the openclaw user using sudo -u (nologin shell)
sudo -u openclaw env PATH="/home/openclaw/.npm-global/bin:$PATH" npm update -g openclaw 2>/dev/null
msg_ok "Updated OpenClaw Package"
msg_info "Verifying Update"
NEW_VERSION=$(su - openclaw -c "export PATH=/home/openclaw/.npm-global/bin:\$PATH && openclaw --version" 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
NEW_VERSION=$(sudo -u openclaw env PATH="/home/openclaw/.npm-global/bin:$PATH" openclaw --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
if [[ "$NEW_VERSION" == "$LATEST_VERSION" ]]; then
msg_ok "Verified OpenClaw v${NEW_VERSION}"
else
@@ -77,18 +78,18 @@ function update_script() {
fi
msg_info "Starting Gateway Service"
su - openclaw -c "systemctl --user start openclaw-gateway" 2>/dev/null || true
sudo -u openclaw systemctl --user start openclaw-gateway 2>/dev/null || true
# Wait for service to start
sleep 3
# Check if service is running
if su - openclaw -c "systemctl --user is-active openclaw-gateway" 2>/dev/null | grep -q "active"; then
if sudo -u openclaw systemctl --user is-active openclaw-gateway 2>/dev/null | grep -q "active"; then
msg_ok "Gateway Service Started"
else
msg_warn "Gateway Service may not have started properly"
msg_info "Checking service status..."
su - openclaw -c "systemctl --user status openclaw-gateway" 2>/dev/null || true
sudo -u openclaw systemctl --user status openclaw-gateway 2>/dev/null || true
fi
msg_ok "Updated successfully to v${LATEST_VERSION}!"
@@ -98,15 +99,15 @@ function update_script() {
echo " Post-Update Verification"
echo "═══════════════════════════════════════════════════════════════════════════════"
echo ""
echo " Note: The openclaw user has no login shell. Use 'sudo -u openclaw' to run commands."
echo ""
echo " Run these commands to verify the update:"
echo " su - openclaw"
echo " openclaw --version"
echo " openclaw doctor"
echo " openclaw gateway status"
echo " sudo -u openclaw openclaw --version"
echo " sudo -u openclaw openclaw doctor"
echo " sudo -u openclaw openclaw gateway status"
echo ""
echo " View logs if issues occur:"
echo " su - openclaw"
echo " openclaw logs --follow"
echo " sudo -u openclaw openclaw logs --follow"
echo ""
echo " Backup saved to: $BACKUP_DIR"
echo ""
+81 -102
View File
@@ -6,17 +6,16 @@ After installation, OpenClaw requires a **model provider** to function. This is
### Essential Commands
```bash
# Switch to openclaw user
su - openclaw
**Note:** The `openclaw` user has no login shell for security. Use `sudo -u openclaw` to run commands.
```bash
# Verify installation
openclaw --version
openclaw doctor
openclaw gateway status
sudo -u openclaw openclaw --version
sudo -u openclaw openclaw doctor
sudo -u openclaw openclaw gateway status
# View logs
openclaw logs --follow
sudo -u openclaw openclaw logs --follow
```
---
@@ -54,8 +53,7 @@ ollama pull all-minilm # Smallest, fastest (45MB)
#### 3. Configure OpenClaw for Ollama
```bash
su - openclaw
openclaw configure
sudo -u openclaw openclaw configure
# Select "Ollama" when prompted for model provider
# Enter Ollama URL (default: http://localhost:11434)
```
@@ -107,12 +105,11 @@ If Ollama runs on a different machine:
### Option B: OpenAI API
```bash
su - openclaw
openclaw models auth add --provider openai
sudo -u openclaw openclaw models auth add --provider openai
# Enter your API key when prompted
# Set as default model
openclaw models set openai:gpt-4o
sudo -u openclaw openclaw models set openai:gpt-4o
```
Manual configuration:
@@ -140,12 +137,11 @@ Manual configuration:
### Option C: Anthropic Claude
```bash
su - openclaw
openclaw models auth add --provider anthropic
sudo -u openclaw openclaw models auth add --provider anthropic
# Enter your API key when prompted
# Set as default model
openclaw models set anthropic:claude-opus-4-6
sudo -u openclaw openclaw models set anthropic:claude-opus-4-6
```
### Option D: Other Providers
@@ -199,8 +195,7 @@ OpenClaw supports multiple messaging channels. Configure channels to interact th
3. **Configure OpenClaw**:
```bash
su - openclaw
openclaw channels add --channel telegram --token "YOUR_BOT_TOKEN"
sudo -u openclaw openclaw channels add --channel telegram --token "YOUR_BOT_TOKEN"
```
4. **Start messaging** your bot on Telegram
@@ -213,8 +208,7 @@ openclaw channels add --channel telegram --token "YOUR_BOT_TOKEN"
4. **Configure OpenClaw**:
```bash
su - openclaw
openclaw channels add --channel discord --token "YOUR_BOT_TOKEN"
sudo -u openclaw openclaw channels add --channel discord --token "YOUR_BOT_TOKEN"
```
### WhatsApp Setup
@@ -222,8 +216,7 @@ openclaw channels add --channel discord --token "YOUR_BOT_TOKEN"
WhatsApp requires QR code pairing:
```bash
su - openclaw
openclaw channels add --channel whatsapp
sudo -u openclaw openclaw channels add --channel whatsapp
# A QR code will be displayed
# Scan with WhatsApp on your phone (Settings > Linked Devices)
```
@@ -419,8 +412,8 @@ origin not allowed (open the Control UI from the gateway host or allow it in gat
CONTAINER_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
# Create configuration with allowed origins
mkdir -p ~/.openclaw
cat > ~/.openclaw/openclaw.json << EOF
sudo -u openclaw mkdir -p /home/openclaw/.openclaw
sudo -u openclaw tee /home/openclaw/.openclaw/openclaw.json > /dev/null << EOF
{
"gateway": {
"bind": "lan",
@@ -437,7 +430,7 @@ cat > ~/.openclaw/openclaw.json << EOF
EOF
# Apply and restart
su - openclaw -c "systemctl --user restart openclaw-gateway"
sudo -u openclaw systemctl --user restart openclaw-gateway
```
**Note:** Even with `allowedOrigins` configured, you still need a secure context (HTTPS or localhost) for the device identity requirement. Use SSH tunneling for LAN access.
@@ -464,11 +457,11 @@ By default, OpenClaw binds to localhost (127.0.0.1) only. To make it accessible
```bash
# Open the gateway configuration
openclaw configure --section gateway
sudo -u openclaw openclaw configure --section gateway
# Set bind to "lan" when prompted
# Then restart the service
su - openclaw -c "systemctl --user restart openclaw-gateway"
sudo -u openclaw systemctl --user restart openclaw-gateway
```
### Method 2: Manual Configuration
@@ -476,9 +469,8 @@ su - openclaw -c "systemctl --user restart openclaw-gateway"
Edit the configuration file:
```bash
# Edit as the openclaw user
su - openclaw
nano ~/.openclaw/openclaw.json
# Edit the configuration file
nano /home/openclaw/.openclaw/openclaw.json
```
Add the configuration with your IP:
@@ -498,7 +490,7 @@ Add the configuration with your IP:
Apply changes:
```bash
systemctl --user restart openclaw-gateway
sudo -u openclaw systemctl --user restart openclaw-gateway
```
### Method 3: Quick Fix for Existing Installations
@@ -509,26 +501,26 @@ For existing installations, run these commands:
# Get your container IP
CONTAINER_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
# Create configuration with allowed origins (as openclaw user)
su - openclaw -c "mkdir -p ~/.openclaw"
su - openclaw -c "cat > ~/.openclaw/openclaw.json << EOF
# Create configuration with allowed origins
sudo -u openclaw mkdir -p /home/openclaw/.openclaw
sudo -u openclaw tee /home/openclaw/.openclaw/openclaw.json > /dev/null << EOF
{
\"gateway\": {
\"bind\": \"lan\",
\"port\": 18789,
\"controlUi\": {
\"allowedOrigins\": [
\"http://localhost:18789\",
\"http://127.0.0.1:18789\",
\"http://${CONTAINER_IP}:18789\"
"gateway": {
"bind": "lan",
"port": 18789,
"controlUi": {
"allowedOrigins": [
"http://localhost:18789",
"http://127.0.0.1:18789",
"http://${CONTAINER_IP}:18789"
]
}
}
}
EOF"
EOF
# Restart the service
su - openclaw -c "systemctl --user restart openclaw-gateway"
sudo -u openclaw systemctl --user restart openclaw-gateway
```
### Method 4: Environment Variable
@@ -545,7 +537,7 @@ OPENCLAW_BIND=lan
Check the current binding:
```bash
openclaw gateway status
sudo -u openclaw openclaw gateway status
```
Output should show:
@@ -604,9 +596,9 @@ http://localhost:18789
2. Set OpenClaw bind to `tailnet`:
```bash
openclaw configure --section gateway
sudo -u openclaw openclaw configure --section gateway
# Set bind to "tailnet"
su - openclaw -c "systemctl --user restart openclaw-gateway"
sudo -u openclaw systemctl --user restart openclaw-gateway
```
3. Access via Tailscale IP: `http://<tailscale-ip>:18789`
@@ -616,8 +608,8 @@ http://localhost:18789
### Gateway Not Starting
```bash
# Check logs (as openclaw user)
su - openclaw -c "journalctl --user -u openclaw-gateway -f"
# Check logs
sudo -u openclaw journalctl --user -u openclaw-gateway -f
# Or from root
journalctl --user -u openclaw-gateway -f --user-unit
@@ -625,12 +617,12 @@ journalctl --user -u openclaw-gateway -f --user-unit
# Common issues:
# - Port already in use: lsof -i :18789
# - Permission denied: ensure running as correct user
# - Config errors: openclaw doctor
# - Config errors: sudo -u openclaw openclaw doctor
```
### Cannot Connect from Remote
1. Verify binding: `openclaw gateway status` should show `0.0.0.0:18789`
1. Verify binding: `sudo -u openclaw openclaw gateway status` should show `0.0.0.0:18789`
2. Check firewall: `ufw status` or `iptables -L -n`
3. Verify network connectivity: `ping <server-ip>`
@@ -638,99 +630,86 @@ journalctl --user -u openclaw-gateway -f --user-unit
```bash
# View current token
openclaw gateway status
sudo -u openclaw openclaw gateway status
# Regenerate token (if needed)
openclaw doctor --generate-gateway-token
sudo -u openclaw openclaw doctor --generate-gateway-token
```
### Model Provider Issues
```bash
# Check model status
openclaw models status
sudo -u openclaw openclaw models status
# Probe providers (may consume tokens)
openclaw models status --probe
sudo -u openclaw openclaw models status --probe
# Set default model
openclaw models set <provider:model>
sudo -u openclaw openclaw models set <provider:model>
# Example:
openclaw models set ollama:llama3.2
openclaw models set openai:gpt-4o
sudo -u openclaw openclaw models set ollama:llama3.2
sudo -u openclaw openclaw models set openai:gpt-4o
```
### Channel Issues
```bash
# Check channel status
openclaw channels status --probe
sudo -u openclaw openclaw channels status --probe
# View channel logs
openclaw channels logs --channel telegram
sudo -u openclaw openclaw channels logs --channel telegram
# Re-login to a channel
openclaw channels login --channel whatsapp
sudo -u openclaw openclaw channels login --channel whatsapp
```
## Installing Homebrew (Optional)
## Using Homebrew
If you need to install additional tools via Homebrew, follow these steps:
Homebrew is installed automatically during the OpenClaw installation. It provides access to additional packages that may be useful for extending OpenClaw functionality.
### Prerequisites
The `openclaw` user needs sudo access and a password set. By default, the user is created without a password.
### Step 1: Set Password for openclaw User
As root, set a password for the openclaw user:
### Verify Installation
```bash
passwd openclaw
# Enter and confirm the new password
```
### Step 2: Install Homebrew
Switch to the openclaw user and run the installer:
```bash
su - openclaw
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
```
When prompted:
1. Enter the openclaw password for sudo access
2. Press ENTER to continue when the installer shows the directories it will create
### Step 3: Add Homebrew to PATH
After installation, add Homebrew to your shell:
```bash
echo >> /home/openclaw/.bashrc
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"' >> /home/openclaw/.bashrc
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"
```
### Step 4: Verify Installation
```bash
brew --version
sudo -u openclaw brew --version
```
### Installing Packages
Once Homebrew is installed, you can install packages:
Once Homebrew is installed, you can install additional packages:
```bash
brew install <package-name>
```
### Common Useful Packages
```bash
# Example: Install additional tools
brew install jq # JSON processor
brew install yq # YAML processor
brew install git-delta # Better git diff viewer
```
**Note:** Homebrew packages are installed to `/home/linuxbrew/.linuxbrew/` and are available to all users on the system.
### Troubleshooting Homebrew
If Homebrew is not working correctly:
```bash
# Check if Homebrew is in PATH
which brew
# If not, add it manually
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"
# Add to .bashrc permanently
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"' >> ~/.bashrc
```
---
## Embedding Model Comparison
+8
View File
@@ -89,6 +89,14 @@
{
"text": "Auth token is displayed during installation - save it for pairing devices",
"type": "warning"
},
{
"text": "Homebrew is installed automatically - use 'sudo -u openclaw brew install <package>' to install additional tools",
"type": "info"
},
{
"text": "The openclaw user has no login shell - use 'sudo -u openclaw <command>' to run commands",
"type": "warning"
}
]
}
+57 -27
View File
@@ -45,29 +45,61 @@ NODE_VERSION="24" setup_nodejs
# Install uv (Python package manager) - OpenClaw uses Python for some tools
PYTHON_VERSION="3.12" setup_uv
# Create unprivileged user for OpenClaw
# Create unprivileged user for OpenClaw (with nologin shell for security)
msg_info "Creating OpenClaw User"
if ! id -u openclaw &>/dev/null; then
useradd -r -s /bin/bash -d /home/openclaw -m openclaw
# Add openclaw to sudoers for specific operations if needed
usermod -aG sudo openclaw 2>/dev/null || true
# Create user with nologin shell to prevent direct login
useradd -r -s /usr/sbin/nologin -d /home/openclaw -m openclaw
fi
msg_ok "Created OpenClaw User"
# Configure password-less sudo for openclaw user
msg_info "Configuring Password-less Sudo for OpenClaw User"
cat <<EOF >/etc/sudoers.d/openclaw
# Allow openclaw user to run any command without password
openclaw ALL=(ALL) NOPASSWD: ALL
EOF
chmod 440 /etc/sudoers.d/openclaw
msg_ok "Configured Password-less Sudo"
# Configure npm to use user-writable directory for global packages
msg_info "Configuring npm for User Packages"
mkdir -p /home/openclaw/.npm-global
chown -R openclaw:openclaw /home/openclaw/.npm-global
# Set npm prefix for openclaw user to use home directory
run_user_cmd="su - openclaw -c"
$run_user_cmd "npm config set prefix /home/openclaw/.npm-global"
# Using sudo -u since openclaw user has nologin shell
sudo -u openclaw npm config set prefix /home/openclaw/.npm-global
# Add to PATH for openclaw user
echo 'export PATH=/home/openclaw/.npm-global/bin:$PATH' >> /home/openclaw/.bashrc
msg_ok "Configured npm for User Packages"
# Install Homebrew for the openclaw user
msg_info "Installing Homebrew"
# Create Homebrew directory structure
mkdir -p /home/linuxbrew/.linuxbrew
chown -R openclaw:openclaw /home/linuxbrew 2>/dev/null || true
# Download Homebrew installer
curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh -o /tmp/brew-install.sh
# Run installer non-interactively as openclaw user
# CI=1 and NONINTERACTIVE=1 enable fully automated installation
# Using sudo -u since openclaw user has nologin shell
CI=1 NONINTERACTIVE=1 sudo -u openclaw bash /tmp/brew-install.sh || true
rm -f /tmp/brew-install.sh
# Add Homebrew to PATH for openclaw user
if [[ -f /home/linuxbrew/.linuxbrew/bin/brew ]]; then
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"' >> /home/openclaw/.bashrc
msg_ok "Installed Homebrew"
else
msg_warn "Homebrew installation may have failed - check logs"
fi
msg_info "Installing OpenClaw"
# Install OpenClaw globally as the openclaw user
$run_user_cmd "npm install -g openclaw@latest"
# Using sudo -u since openclaw user has nologin shell
sudo -u openclaw npm install -g openclaw@latest
msg_ok "Installed OpenClaw"
msg_info "Creating Directories"
@@ -184,7 +216,8 @@ export PATH="/home/openclaw/.npm-global/bin:$PATH"
export OPENCLAW_CONFIG_PATH="/home/openclaw/.openclaw/openclaw.json"
# Install the gateway service using the recommended method
$run_user_cmd "export PATH=/home/openclaw/.npm-global/bin:\$PATH && openclaw gateway install" 2>/dev/null || true
# Using sudo -u since openclaw user has nologin shell
sudo -u openclaw env PATH="/home/openclaw/.npm-global/bin:$PATH" openclaw gateway install 2>/dev/null || true
# If the service wasn't created, create it manually
if [[ ! -f /home/openclaw/.config/systemd/user/openclaw-gateway.service ]]; then
@@ -220,8 +253,9 @@ msg_ok "Started Caddy HTTPS Proxy"
msg_info "Starting OpenClaw Gateway"
# Start the gateway service as the openclaw user
$run_user_cmd "systemctl --user enable openclaw-gateway" 2>/dev/null || true
$run_user_cmd "systemctl --user start openclaw-gateway" 2>/dev/null || true
# Using sudo -u since openclaw user has nologin shell
sudo -u openclaw systemctl --user enable openclaw-gateway 2>/dev/null || true
sudo -u openclaw systemctl --user start openclaw-gateway 2>/dev/null || true
msg_ok "Started OpenClaw Gateway"
# Wait for gateway to be ready
@@ -229,10 +263,10 @@ msg_info "Verifying Installation"
sleep 5
# Check if gateway is running
if $run_user_cmd "systemctl --user is-active openclaw-gateway" 2>/dev/null | grep -q "active"; then
if sudo -u openclaw systemctl --user is-active openclaw-gateway 2>/dev/null | grep -q "active"; then
msg_ok "Gateway Service is Active"
else
msg_warn "Gateway Service may not be running - check logs with: su - openclaw -c 'journalctl --user -u openclaw-gateway'"
msg_warn "Gateway Service may not be running - check logs with: sudo -u openclaw journalctl --user -u openclaw-gateway"
fi
# Display auth token for pairing
@@ -250,6 +284,8 @@ echo "═══════���══════════════
echo " Next Steps:"
echo "═══════════════════════════════════════════════════════════════════════════════"
echo ""
echo " Note: The openclaw user has no login shell. Use 'sudo -u openclaw' to run commands."
echo ""
echo " 1. Configure a Model Provider (REQUIRED for OpenClaw to function):"
echo ""
echo " Option A - Ollama (Local, Free):"
@@ -257,34 +293,28 @@ echo " • Install Ollama: curl -fsSL https://ollama.com/install.sh | sh"
echo " • Pull a model: ollama pull llama3.2"
echo " • Pull embedding model: ollama pull nomic-embed-text"
echo " • Configure OpenClaw:"
echo " su - openclaw"
echo " openclaw configure"
echo " sudo -u openclaw openclaw configure"
echo " # Select Ollama as provider"
echo ""
echo " Option B - OpenAI API:"
echo " su - openclaw"
echo " openclaw models auth add --provider openai"
echo " sudo -u openclaw openclaw models auth add --provider openai"
echo " # Enter your API key when prompted"
echo ""
echo " Option C - Anthropic Claude:"
echo " su - openclaw"
echo " openclaw models auth add --provider anthropic"
echo " sudo -u openclaw openclaw models auth add --provider anthropic"
echo " # Enter your API key when prompted"
echo ""
echo " 2. Configure Channels (Optional - for messaging):"
echo " su - openclaw"
echo " openclaw channels add --channel telegram --token YOUR_BOT_TOKEN"
echo " openclaw channels add --channel discord --token YOUR_BOT_TOKEN"
echo " sudo -u openclaw openclaw channels add --channel telegram --token YOUR_BOT_TOKEN"
echo " sudo -u openclaw openclaw channels add --channel discord --token YOUR_BOT_TOKEN"
echo ""
echo " 3. Verify Installation:"
echo " su - openclaw"
echo " openclaw doctor"
echo " openclaw gateway status"
echo " openclaw models status"
echo " sudo -u openclaw openclaw doctor"
echo " sudo -u openclaw openclaw gateway status"
echo " sudo -u openclaw openclaw models status"
echo ""
echo " 4. View Logs:"
echo " su - openclaw"
echo " openclaw logs --follow"
echo " sudo -u openclaw openclaw logs --follow"
echo ""
echo "═══════════════════════════════════════════════════════════════════════════════"
echo " Memory Search Configuration"
+56 -19
View File
@@ -4,6 +4,7 @@ source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/build.func)
# Author: BIllyOutlast
# License: MIT | https://github.com/Heretek-AI/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/openclaw/openclaw
# Documentation: https://docs.openclaw.ai
APP="openclaw"
var_tags="${var_tags:-ai;assistant;automation}"
@@ -25,13 +26,14 @@ function update_script() {
check_container_resources
# Check if openclaw is installed (npm global bin location varies)
if ! command -v openclaw &>/dev/null; then
if ! command -v openclaw &>/dev/null && [[ ! -f /home/openclaw/.npm-global/bin/openclaw ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
# Get current version
CURRENT_VERSION=$(openclaw --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
# Using sudo -u since openclaw user has nologin shell
CURRENT_VERSION=$(sudo -u openclaw env PATH="/home/openclaw/.npm-global/bin:$PATH" openclaw --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
# Get latest version from npm
LATEST_VERSION=$(npm view openclaw version 2>/dev/null || echo "unknown")
@@ -48,33 +50,68 @@ function update_script() {
msg_info "Updating OpenClaw from v${CURRENT_VERSION} to v${LATEST_VERSION}"
msg_info "Stopping Service"
su - openclaw -c "systemctl --user stop openclaw-gateway" 2>/dev/null || true
msg_ok "Stopped Service"
msg_info "Stopping Gateway Service"
sudo -u openclaw systemctl --user stop openclaw-gateway 2>/dev/null || true
msg_ok "Stopped Gateway Service"
msg_info "Backing up Configuration"
BACKUP_DIR="/tmp/openclaw_backup_$(date +%Y%m%d_%H%M%S)"
if [[ -d /home/openclaw/.openclaw ]]; then
cp -r /home/openclaw/.openclaw /tmp/openclaw_backup
mkdir -p "$BACKUP_DIR"
cp -r /home/openclaw/.openclaw/* "$BACKUP_DIR"/ 2>/dev/null || true
msg_ok "Backed up Configuration to $BACKUP_DIR"
else
msg_warn "No configuration directory found"
fi
msg_ok "Backed up Configuration"
msg_info "Updating OpenClaw"
$STD npm update -g openclaw
msg_ok "Updated OpenClaw"
msg_info "Updating OpenClaw Package"
# Run npm update as the openclaw user using sudo -u (nologin shell)
sudo -u openclaw env PATH="/home/openclaw/.npm-global/bin:$PATH" npm update -g openclaw 2>/dev/null
msg_ok "Updated OpenClaw Package"
msg_info "Restoring Configuration"
if [[ -d /tmp/openclaw_backup ]]; then
cp -r /tmp/openclaw_backup/. /home/openclaw/.openclaw 2>/dev/null || true
rm -rf /tmp/openclaw_backup
msg_info "Verifying Update"
NEW_VERSION=$(sudo -u openclaw env PATH="/home/openclaw/.npm-global/bin:$PATH" openclaw --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
if [[ "$NEW_VERSION" == "$LATEST_VERSION" ]]; then
msg_ok "Verified OpenClaw v${NEW_VERSION}"
else
msg_warn "Version verification failed. Installed: v${NEW_VERSION}, Expected: v${LATEST_VERSION}"
fi
chown -R openclaw:openclaw /home/openclaw/.openclaw 2>/dev/null || true
msg_ok "Restored Configuration"
msg_info "Starting Service"
su - openclaw -c "systemctl --user start openclaw-gateway" 2>/dev/null || true
msg_ok "Started Service"
msg_info "Starting Gateway Service"
sudo -u openclaw systemctl --user start openclaw-gateway 2>/dev/null || true
# Wait for service to start
sleep 3
# Check if service is running
if sudo -u openclaw systemctl --user is-active openclaw-gateway 2>/dev/null | grep -q "active"; then
msg_ok "Gateway Service Started"
else
msg_warn "Gateway Service may not have started properly"
msg_info "Checking service status..."
sudo -u openclaw systemctl --user status openclaw-gateway 2>/dev/null || true
fi
msg_ok "Updated successfully to v${LATEST_VERSION}!"
echo ""
echo "═══════════════════════════════════════════════════════════════════════════════"
echo " Post-Update Verification"
echo "═══════════════════════════════════════════════════════════════════════════════"
echo ""
echo " Note: The openclaw user has no login shell. Use 'sudo -u openclaw' to run commands."
echo ""
echo " Run these commands to verify the update:"
echo " sudo -u openclaw openclaw --version"
echo " sudo -u openclaw openclaw doctor"
echo " sudo -u openclaw openclaw gateway status"
echo ""
echo " View logs if issues occur:"
echo " sudo -u openclaw openclaw logs --follow"
echo ""
echo " Backup saved to: $BACKUP_DIR"
echo ""
exit
}
+444 -68
View File
@@ -1,5 +1,315 @@
# OpenClaw Configuration Guide
## Quick Start
After installation, OpenClaw requires a **model provider** to function. This is the most critical configuration step.
### Essential Commands
**Note:** The `openclaw` user has no login shell for security. Use `sudo -u openclaw` to run commands.
```bash
# Verify installation
sudo -u openclaw openclaw --version
sudo -u openclaw openclaw doctor
sudo -u openclaw openclaw gateway status
# View logs
sudo -u openclaw openclaw logs --follow
```
---
## Model Provider Configuration (REQUIRED)
OpenClaw requires a model provider to process messages. Choose one of the following options:
### Option A: Ollama (Local, Free, Recommended for Offline)
Ollama runs models locally on your hardware - no API costs, complete privacy.
#### 1. Install Ollama
```bash
# On the OpenClaw container or a separate server
curl -fsSL https://ollama.com/install.sh | sh
```
#### 2. Pull Required Models
```bash
# Chat model (choose one)
ollama pull llama3.2 # Good balance (recommended)
ollama pull llama3.1:8b # Larger, more capable
ollama pull mistral # Alternative option
ollama pull gemma2 # Google's model
# Embedding model (required for memory search)
ollama pull nomic-embed-text # Recommended (274MB)
ollama pull mxbai-embed-large # Higher quality (670MB)
ollama pull all-minilm # Smallest, fastest (45MB)
```
#### 3. Configure OpenClaw for Ollama
```bash
sudo -u openclaw openclaw configure
# Select "Ollama" when prompted for model provider
# Enter Ollama URL (default: http://localhost:11434)
```
Or manually edit `~/.openclaw/openclaw.json`:
```json
{
"agents": {
"defaults": {
"model": {
"primary": "ollama:llama3.2"
},
"memorySearch": {
"provider": "ollama",
"model": "nomic-embed-text",
"remote": {
"baseUrl": "http://localhost:11434"
}
}
}
}
}
```
#### 4. Ollama on a Separate Server
If Ollama runs on a different machine:
```json
{
"agents": {
"defaults": {
"model": {
"primary": "ollama:llama3.2"
},
"memorySearch": {
"provider": "ollama",
"model": "nomic-embed-text",
"remote": {
"baseUrl": "http://192.168.1.100:11434"
}
}
}
}
}
```
### Option B: OpenAI API
```bash
sudo -u openclaw openclaw models auth add --provider openai
# Enter your API key when prompted
# Set as default model
sudo -u openclaw openclaw models set openai:gpt-4o
```
Manual configuration:
```json
{
"secrets": {
"providers": {
"openai-api": {
"source": "env",
"env": "OPENAI_API_KEY"
}
}
},
"agents": {
"defaults": {
"model": {
"primary": "openai:gpt-4o"
}
}
}
}
```
### Option C: Anthropic Claude
```bash
sudo -u openclaw openclaw models auth add --provider anthropic
# Enter your API key when prompted
# Set as default model
sudo -u openclaw openclaw models set anthropic:claude-opus-4-6
```
### Option D: Other Providers
OpenClaw supports many providers:
| Provider | Command |
| ------------- | ------------------------------------------------ |
| Google Gemini | `openclaw models auth add --provider google` |
| Mistral | `openclaw models auth add --provider mistral` |
| OpenRouter | `openclaw models auth add --provider openrouter` |
| Groq | `openclaw models auth add --provider groq` |
| xAI | `openclaw models auth add --provider xai` |
| Together AI | `openclaw models auth add --provider together` |
### Verify Model Configuration
```bash
# Check model status
openclaw models status
# Test with a message
openclaw agent --message "Hello, are you working?" --local
```
---
## Channel Configuration
OpenClaw supports multiple messaging channels. Configure channels to interact through your preferred platform.
### Supported Channels
| Channel | Type | Setup Method |
| --------------- | ------------- | ------------------------------- |
| Telegram | Bot API | Bot token from @BotFather |
| Discord | Bot API | Bot token from Developer Portal |
| WhatsApp | Baileys | QR code pairing |
| Slack | Bolt SDK | App credentials |
| Signal | signal-cli | Phone number pairing |
| Matrix | Plugin | Homeserver config |
| iMessage | BlueBubbles | macOS server required |
| Microsoft Teams | Bot Framework | App registration |
| IRC | Native | Server connection |
| Google Chat | Webhook | App configuration |
### Telegram Setup (Easiest)
1. **Create a bot** by messaging [@BotFather](https://t.me/BotFather) on Telegram
2. **Get your bot token** (format: `123456789:ABCdefGHIjklMNOpqrsTUVwxyz`)
3. **Configure OpenClaw**:
```bash
sudo -u openclaw openclaw channels add --channel telegram --token "YOUR_BOT_TOKEN"
```
4. **Start messaging** your bot on Telegram
### Discord Setup
1. **Create a bot** at [Discord Developer Portal](https://discord.com/developers/applications)
2. **Get your bot token** from the Bot section
3. **Invite bot to server** using OAuth2 URL generator
4. **Configure OpenClaw**:
```bash
sudo -u openclaw openclaw channels add --channel discord --token "YOUR_BOT_TOKEN"
```
### WhatsApp Setup
WhatsApp requires QR code pairing:
```bash
sudo -u openclaw openclaw channels add --channel whatsapp
# A QR code will be displayed
# Scan with WhatsApp on your phone (Settings > Linked Devices)
```
### Multiple Accounts
You can configure multiple accounts per channel:
```bash
# Add a second Telegram account
openclaw channels add --channel telegram --account work --name "Work Bot" --token "WORK_BOT_TOKEN"
# List all channels
openclaw channels list
# Check channel status
openclaw channels status --probe
```
### Channel Routing
Route different channels to different agents:
```bash
# Create an agent
openclaw agents add --workspace ~/.openclaw-workspaces/work
# Bind channels to agent
openclaw agents bind --agent <agent-id> --bind telegram:work
openclaw agents bind --agent <agent-id> --bind discord:main
```
---
## Memory Configuration
OpenClaw's memory search requires an embedding provider to index and search through your memory files.
### Memory Files Location
Memory files are stored in `~/.openclaw/workspace/memory/`:
```bash
# Create memory directory
mkdir -p ~/.openclaw/workspace/memory
# Create your first memory file
cat > ~/.openclaw/workspace/MEMORY.md << 'EOF'
# Personal Memory
## Preferences
- I prefer concise responses
- Use bullet points for lists
## Projects
- Home automation: running on Proxmox
- Media server: Jellyfin on port 8096
EOF
```
### Configure Memory Search
```json
{
"agents": {
"defaults": {
"memorySearch": {
"provider": "ollama",
"model": "nomic-embed-text",
"remote": {
"baseUrl": "http://localhost:11434"
}
}
}
}
}
```
### Memory Commands
```bash
# Check memory status
openclaw memory status
# Reindex memory files
openclaw memory index
# Search memory
openclaw memory search "project details"
```
---
## "Device Identity Required" Error Fix
If you see this error when accessing the Control UI from another machine:
@@ -43,7 +353,6 @@ nano ~/.openclaw/openclaw.json
"allowInsecureAuth": true
},
"auth": {
"mode": "token",
"token": "your-secure-token-here"
}
}
@@ -64,7 +373,7 @@ su - openclaw -c "systemctl --user restart openclaw-gateway"
### Solution 3: HTTPS Reverse Proxy (Production)
Set up Caddy or Nginx with SSL certificates for secure HTTPS access.
The installation includes Caddy for HTTPS access on port 18790.
### Solution 4: Tailscale VPN
@@ -86,6 +395,8 @@ For emergency access only, you can completely disable device auth:
**⚠️ WARNING:** This is a severe security downgrade. Revert immediately after emergency use.
---
## "Origin Not Allowed" Error Fix
If you see this error when accessing the Control UI from another machine:
@@ -101,8 +412,8 @@ origin not allowed (open the Control UI from the gateway host or allow it in gat
CONTAINER_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
# Create configuration with allowed origins
mkdir -p ~/.openclaw
cat > ~/.openclaw/openclaw.json << EOF
sudo -u openclaw mkdir -p /home/openclaw/.openclaw
sudo -u openclaw tee /home/openclaw/.openclaw/openclaw.json > /dev/null << EOF
{
"gateway": {
"bind": "lan",
@@ -119,11 +430,13 @@ cat > ~/.openclaw/openclaw.json << EOF
EOF
# Apply and restart
su - openclaw -c "systemctl --user restart openclaw-gateway"
sudo -u openclaw systemctl --user restart openclaw-gateway
```
**Note:** Even with `allowedOrigins` configured, you still need a secure context (HTTPS or localhost) for the device identity requirement. Use SSH tunneling for LAN access.
---
## Network Binding Configuration
By default, OpenClaw binds to localhost (127.0.0.1) only. To make it accessible from other machines on your network, you need to:
@@ -144,11 +457,11 @@ By default, OpenClaw binds to localhost (127.0.0.1) only. To make it accessible
```bash
# Open the gateway configuration
openclaw configure --section gateway
sudo -u openclaw openclaw configure --section gateway
# Set bind to "lan" when prompted
# Then restart the service
su - openclaw -c "systemctl --user restart openclaw-gateway"
sudo -u openclaw systemctl --user restart openclaw-gateway
```
### Method 2: Manual Configuration
@@ -156,9 +469,8 @@ su - openclaw -c "systemctl --user restart openclaw-gateway"
Edit the configuration file:
```bash
# Edit as the openclaw user
su - openclaw
nano ~/.openclaw/openclaw.json
# Edit the configuration file
nano /home/openclaw/.openclaw/openclaw.json
```
Add the configuration with your IP:
@@ -178,7 +490,7 @@ Add the configuration with your IP:
Apply changes:
```bash
systemctl --user restart openclaw-gateway
sudo -u openclaw systemctl --user restart openclaw-gateway
```
### Method 3: Quick Fix for Existing Installations
@@ -189,26 +501,26 @@ For existing installations, run these commands:
# Get your container IP
CONTAINER_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
# Create configuration with allowed origins (as openclaw user)
su - openclaw -c "mkdir -p ~/.openclaw"
su - openclaw -c "cat > ~/.openclaw/openclaw.json << EOF
# Create configuration with allowed origins
sudo -u openclaw mkdir -p /home/openclaw/.openclaw
sudo -u openclaw tee /home/openclaw/.openclaw/openclaw.json > /dev/null << EOF
{
\"gateway\": {
\"bind\": \"lan\",
\"port\": 18789,
\"controlUi\": {
\"allowedOrigins\": [
\"http://localhost:18789\",
\"http://127.0.0.1:18789\",
\"http://${CONTAINER_IP}:18789\"
"gateway": {
"bind": "lan",
"port": 18789,
"controlUi": {
"allowedOrigins": [
"http://localhost:18789",
"http://127.0.0.1:18789",
"http://${CONTAINER_IP}:18789"
]
}
}
}
EOF"
EOF
# Restart the service
su - openclaw -c "systemctl --user restart openclaw-gateway"
sudo -u openclaw systemctl --user restart openclaw-gateway
```
### Method 4: Environment Variable
@@ -225,7 +537,7 @@ OPENCLAW_BIND=lan
Check the current binding:
```bash
openclaw gateway status
sudo -u openclaw openclaw gateway status
```
Output should show:
@@ -284,9 +596,9 @@ http://localhost:18789
2. Set OpenClaw bind to `tailnet`:
```bash
openclaw configure --section gateway
sudo -u openclaw openclaw configure --section gateway
# Set bind to "tailnet"
su - openclaw -c "systemctl --user restart openclaw-gateway"
sudo -u openclaw systemctl --user restart openclaw-gateway
```
3. Access via Tailscale IP: `http://<tailscale-ip>:18789`
@@ -296,8 +608,8 @@ http://localhost:18789
### Gateway Not Starting
```bash
# Check logs (as openclaw user)
su - openclaw -c "journalctl --user -u openclaw-gateway -f"
# Check logs
sudo -u openclaw journalctl --user -u openclaw-gateway -f
# Or from root
journalctl --user -u openclaw-gateway -f --user-unit
@@ -305,83 +617,147 @@ journalctl --user -u openclaw-gateway -f --user-unit
# Common issues:
# - Port already in use: lsof -i :18789
# - Permission denied: ensure running as correct user
# - Config errors: sudo -u openclaw openclaw doctor
```
### Cannot Connect from Remote
1. Verify binding: `openclaw gateway status` should show `0.0.0.0:18789`
1. Verify binding: `sudo -u openclaw openclaw gateway status` should show `0.0.0.0:18789`
2. Check firewall: `ufw status` or `iptables -L -n`
3. Verify network connectivity: `ping <server-ip>`
### Token Authentication Issues
```bash
# Regenerate token
openclaw doctor --generate-gateway-token
# View current token
openclaw gateway status
sudo -u openclaw openclaw gateway status
# Regenerate token (if needed)
sudo -u openclaw openclaw doctor --generate-gateway-token
```
## Installing Homebrew (Optional)
If you need to install additional tools via Homebrew, follow these steps:
### Prerequisites
The `openclaw` user needs sudo access and a password set. By default, the user is created without a password.
### Step 1: Set Password for openclaw User
As root, set a password for the openclaw user:
### Model Provider Issues
```bash
passwd openclaw
# Enter and confirm the new password
# Check model status
sudo -u openclaw openclaw models status
# Probe providers (may consume tokens)
sudo -u openclaw openclaw models status --probe
# Set default model
sudo -u openclaw openclaw models set <provider:model>
# Example:
sudo -u openclaw openclaw models set ollama:llama3.2
sudo -u openclaw openclaw models set openai:gpt-4o
```
### Step 2: Install Homebrew
Switch to the openclaw user and run the installer:
### Channel Issues
```bash
su - openclaw
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Check channel status
sudo -u openclaw openclaw channels status --probe
# View channel logs
sudo -u openclaw openclaw channels logs --channel telegram
# Re-login to a channel
sudo -u openclaw openclaw channels login --channel whatsapp
```
When prompted:
## Using Homebrew
1. Enter the openclaw password for sudo access
2. Press ENTER to continue when the installer shows the directories it will create
Homebrew is installed automatically during the OpenClaw installation. It provides access to additional packages that may be useful for extending OpenClaw functionality.
### Step 3: Add Homebrew to PATH
After installation, add Homebrew to your shell:
### Verify Installation
```bash
echo >> /home/openclaw/.bashrc
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"' >> /home/openclaw/.bashrc
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"
```
### Step 4: Verify Installation
```bash
brew --version
sudo -u openclaw brew --version
```
### Installing Packages
Once Homebrew is installed, you can install packages:
Once Homebrew is installed, you can install additional packages:
```bash
brew install <package-name>
```
### Common Useful Packages
```bash
# Example: Install additional tools
brew install jq # JSON processor
brew install yq # YAML processor
brew install git-delta # Better git diff viewer
```
**Note:** Homebrew packages are installed to `/home/linuxbrew/.linuxbrew/` and are available to all users on the system.
### Troubleshooting Homebrew
If Homebrew is not working correctly:
```bash
# Check if Homebrew is in PATH
which brew
# If not, add it manually
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"
# Add to .bashrc permanently
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"' >> ~/.bashrc
```
---
## Embedding Model Comparison
| Model | Size | Quality | Speed | Use Case |
| ----------------- | ------ | ------- | ------- | ----------------------------- |
| nomic-embed-text | ~274MB | Good | Fast | General purpose (recommended) |
| mxbai-embed-large | ~670MB | Better | Medium | Higher accuracy needed |
| all-minilm | ~45MB | Basic | Fastest | Resource-constrained |
---
## Hybrid Search (BM25 + Vector)
Enable hybrid search for better recall:
```json
{
"agents": {
"defaults": {
"memorySearch": {
"provider": "ollama",
"model": "nomic-embed-text",
"remote": {
"baseUrl": "http://localhost:11434"
},
"query": {
"hybrid": {
"enabled": true,
"vectorWeight": 0.7,
"textWeight": 0.3
}
}
}
}
}
}
```
---
## Additional Resources
- [OpenClaw Gateway Documentation](https://docs.openclaw.ai/gateway)
- [OpenClaw Security Guide](https://docs.openclaw.ai/gateway/security)
- [OpenClaw Troubleshooting](https://docs.openclaw.ai/gateway/troubleshooting)
- [OpenClaw Memory Configuration Reference](https://docs.openclaw.ai/reference/memory-config)
- [OpenClaw CLI Reference](https://docs.openclaw.ai/cli)
- [OpenClaw Channels](https://docs.openclaw.ai/channels)
- [OpenClaw Model Providers](https://docs.openclaw.ai/providers)
- [Ollama Documentation](https://ollama.com/docs)
+35 -3
View File
@@ -43,7 +43,7 @@
"type": "info"
},
{
"text": "Requires Node.js 22+ (installed automatically)",
"text": "Requires Node.js 24 (installed automatically)",
"type": "info"
},
{
@@ -59,12 +59,44 @@
"type": "info"
},
{
"text": "Supports OpenAI, Anthropic, and other AI providers - API keys required",
"text": "CRITICAL: A model provider MUST be configured after installation (Ollama, OpenAI, Anthropic, etc.)",
"type": "warning"
},
{
"text": "Can work with local models via Ollama for offline operation",
"text": "To configure a model provider: su - openclaw && openclaw models auth add --provider <provider>",
"type": "info"
},
{
"text": "For local/offline use: Install Ollama and run 'ollama pull llama3.2' then 'ollama pull nomic-embed-text'",
"type": "info"
},
{
"text": "Memory search requires embedding provider (Ollama, OpenAI, Gemini, Voyage, or local model)",
"type": "warning"
},
{
"text": "To configure channels (Telegram, Discord, WhatsApp, etc.): su - openclaw && openclaw channels add --channel <name>",
"type": "info"
},
{
"text": "Verify installation: su - openclaw && openclaw doctor && openclaw gateway status",
"type": "info"
},
{
"text": "View logs: su - openclaw && openclaw logs --follow",
"type": "info"
},
{
"text": "Auth token is displayed during installation - save it for pairing devices",
"type": "warning"
},
{
"text": "Homebrew is installed automatically - use 'sudo -u openclaw brew install <package>' to install additional tools",
"type": "info"
},
{
"text": "The openclaw user has no login shell - use 'sudo -u openclaw <command>' to run commands",
"type": "warning"
}
]
}
+181 -13
View File
@@ -3,6 +3,7 @@
# Author: BIllyOutlast
# License: MIT | https://github.com/Heretek-AI/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/openclaw/openclaw
# Documentation: https://docs.openclaw.ai
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
@@ -38,41 +39,78 @@ $STD apt-get update
$STD apt-get install -y caddy
msg_ok "Installed Caddy"
# Setup Node.js 24 (recommended by OpenClaw)
# Setup Node.js 24 (recommended by OpenClaw documentation)
NODE_VERSION="24" setup_nodejs
# Install uv (Python package manager) - OpenClaw uses Python for some tools
PYTHON_VERSION="3.12" setup_uv
# Create unprivileged user for OpenClaw
# Create unprivileged user for OpenClaw (with nologin shell for security)
msg_info "Creating OpenClaw User"
if ! id -u openclaw &>/dev/null; then
useradd -r -s /bin/bash -d /home/openclaw -m openclaw
# Add openclaw to sudoers for specific operations if needed
usermod -aG sudo openclaw 2>/dev/null || true
# Create user with nologin shell to prevent direct login
useradd -r -s /usr/sbin/nologin -d /home/openclaw -m openclaw
fi
msg_ok "Created OpenClaw User"
# Configure password-less sudo for openclaw user
msg_info "Configuring Password-less Sudo for OpenClaw User"
cat <<EOF >/etc/sudoers.d/openclaw
# Allow openclaw user to run any command without password
openclaw ALL=(ALL) NOPASSWD: ALL
EOF
chmod 440 /etc/sudoers.d/openclaw
msg_ok "Configured Password-less Sudo"
# Configure npm to use user-writable directory for global packages
msg_info "Configuring npm for User Packages"
mkdir -p /home/openclaw/.npm-global
chown -R openclaw:openclaw /home/openclaw/.npm-global
# Set npm prefix for openclaw user to use home directory
run_user_cmd="su - openclaw -c"
$run_user_cmd "npm config set prefix /home/openclaw/.npm-global"
# Using sudo -u since openclaw user has nologin shell
sudo -u openclaw npm config set prefix /home/openclaw/.npm-global
# Add to PATH for openclaw user
echo 'export PATH=/home/openclaw/.npm-global/bin:$PATH' >> /home/openclaw/.bashrc
msg_ok "Configured npm for User Packages"
# Install Homebrew for the openclaw user
msg_info "Installing Homebrew"
# Create Homebrew directory structure
mkdir -p /home/linuxbrew/.linuxbrew
chown -R openclaw:openclaw /home/linuxbrew 2>/dev/null || true
# Download Homebrew installer
curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh -o /tmp/brew-install.sh
# Run installer non-interactively as openclaw user
# CI=1 and NONINTERACTIVE=1 enable fully automated installation
# Using sudo -u since openclaw user has nologin shell
CI=1 NONINTERACTIVE=1 sudo -u openclaw bash /tmp/brew-install.sh || true
rm -f /tmp/brew-install.sh
# Add Homebrew to PATH for openclaw user
if [[ -f /home/linuxbrew/.linuxbrew/bin/brew ]]; then
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"' >> /home/openclaw/.bashrc
msg_ok "Installed Homebrew"
else
msg_warn "Homebrew installation may have failed - check logs"
fi
msg_info "Installing OpenClaw"
$STD npm install -g openclaw@latest
# Install OpenClaw globally as the openclaw user
# Using sudo -u since openclaw user has nologin shell
sudo -u openclaw npm install -g openclaw@latest
msg_ok "Installed OpenClaw"
msg_info "Creating Directories"
mkdir -p /opt/openclaw
mkdir -p /home/openclaw/.openclaw
mkdir -p /home/openclaw/.openclaw/workspace
mkdir -p /home/openclaw/.openclaw/workspace/memory
chown -R openclaw:openclaw /opt/openclaw
chown -R openclaw:openclaw /home/openclaw/.openclaw
# Security: Restrict state directory to owner only (fixes world-readable warning)
chmod 700 /home/openclaw/.openclaw
msg_ok "Created Directories"
msg_info "Creating OpenClaw Configuration"
@@ -80,11 +118,16 @@ msg_info "Creating OpenClaw Configuration"
CONTAINER_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
# Get hostname for HTTPS origins
HOSTNAME_FQDN=$(hostname -f 2>/dev/null || hostname)
# Generate a secure random auth token (32 bytes = 64 hex chars)
AUTH_TOKEN=$(openssl rand -hex 32)
cat <<EOF >/home/openclaw/.openclaw/openclaw.json
{
"gateway": {
"bind": "lan",
"port": 18789,
"auth": {
"token": "${AUTH_TOKEN}"
},
"controlUi": {
"allowedOrigins": [
"http://localhost:18789",
@@ -94,11 +137,21 @@ cat <<EOF >/home/openclaw/.openclaw/openclaw.json
"https://${CONTAINER_IP}:18790",
"https://${HOSTNAME_FQDN}:18790"
]
},
"reload": {
"mode": "hybrid"
}
},
"agents": {
"defaults": {
"workspace": "/home/openclaw/.openclaw/workspace"
}
}
}
EOF
chown openclaw:openclaw /home/openclaw/.openclaw/openclaw.json
# Security: Restrict config file to owner only (fixes world-readable critical issue)
chmod 600 /home/openclaw/.openclaw/openclaw.json
msg_ok "Created OpenClaw Configuration"
msg_info "Creating Self-Signed Certificate for HTTPS"
@@ -156,18 +209,133 @@ msg_info "Enabling Lingering for OpenClaw User"
loginctl enable-linger openclaw 2>/dev/null || true
msg_ok "Enabled Lingering"
msg_info "Initializing OpenClaw Gateway Service"
# Run OpenClaw installer as the openclaw user to create user-level systemd service
# The installer will prompt for hooks and create the service automatically
msg_info "Installing OpenClaw Gateway Service"
# Run OpenClaw onboarding to create user-level systemd service properly
# Using --non-interactive with essential defaults
export PATH="/home/openclaw/.npm-global/bin:$PATH"
$run_user_cmd "openclaw install --no-hooks" 2>/dev/null || true
msg_ok "Initialized OpenClaw Gateway Service"
export OPENCLAW_CONFIG_PATH="/home/openclaw/.openclaw/openclaw.json"
# Install the gateway service using the recommended method
# Using sudo -u since openclaw user has nologin shell
sudo -u openclaw env PATH="/home/openclaw/.npm-global/bin:$PATH" openclaw gateway install 2>/dev/null || true
# If the service wasn't created, create it manually
if [[ ! -f /home/openclaw/.config/systemd/user/openclaw-gateway.service ]]; then
msg_info "Creating Manual Systemd Service"
mkdir -p /home/openclaw/.config/systemd/user
cat <<EOF >/home/openclaw/.config/systemd/user/openclaw-gateway.service
[Unit]
Description=OpenClaw Gateway
Documentation=https://docs.openclaw.ai
After=network.target
[Service]
Type=simple
WorkingDirectory=/home/openclaw/.openclaw
Environment="PATH=/home/openclaw/.npm-global/bin:/usr/local/bin:/usr/bin:/bin"
Environment="OPENCLAW_CONFIG_PATH=/home/openclaw/.openclaw/openclaw.json"
ExecStart=/home/openclaw/.npm-global/bin/openclaw gateway
Restart=on-failure
RestartSec=10
[Install]
WantedBy=default.target
EOF
chown -R openclaw:openclaw /home/openclaw/.config
chmod 600 /home/openclaw/.config/systemd/user/openclaw-gateway.service
fi
msg_ok "Installed OpenClaw Gateway Service"
msg_info "Starting Caddy HTTPS Proxy"
# Restart Caddy to apply configuration
systemctl restart caddy
msg_ok "Started Caddy HTTPS Proxy"
msg_info "Starting OpenClaw Gateway"
# Start the gateway service as the openclaw user
# Using sudo -u since openclaw user has nologin shell
sudo -u openclaw systemctl --user enable openclaw-gateway 2>/dev/null || true
sudo -u openclaw systemctl --user start openclaw-gateway 2>/dev/null || true
msg_ok "Started OpenClaw Gateway"
# Wait for gateway to be ready
msg_info "Verifying Installation"
sleep 5
# Check if gateway is running
if sudo -u openclaw systemctl --user is-active openclaw-gateway 2>/dev/null | grep -q "active"; then
msg_ok "Gateway Service is Active"
else
msg_warn "Gateway Service may not be running - check logs with: sudo -u openclaw journalctl --user -u openclaw-gateway"
fi
# Display auth token for pairing
echo ""
echo "═══════════════════════════════════════════════════════════════════════════════"
echo " OpenClaw Auth Token (save this for pairing):"
echo " ${AUTH_TOKEN}"
echo "═══════════════════════════════════════════════════════════════════════════════"
echo ""
echo " Access URLs:"
echo " HTTP: http://${CONTAINER_IP}:18789"
echo " HTTPS: https://${CONTAINER_IP}:18790"
echo ""
echo "═══════���═══════════════════════════════════════════════════════════════════════"
echo " Next Steps:"
echo "═══════════════════════════════════════════════════════════════════════════════"
echo ""
echo " Note: The openclaw user has no login shell. Use 'sudo -u openclaw' to run commands."
echo ""
echo " 1. Configure a Model Provider (REQUIRED for OpenClaw to function):"
echo ""
echo " Option A - Ollama (Local, Free):"
echo " • Install Ollama: curl -fsSL https://ollama.com/install.sh | sh"
echo " • Pull a model: ollama pull llama3.2"
echo " • Pull embedding model: ollama pull nomic-embed-text"
echo " • Configure OpenClaw:"
echo " sudo -u openclaw openclaw configure"
echo " # Select Ollama as provider"
echo ""
echo " Option B - OpenAI API:"
echo " sudo -u openclaw openclaw models auth add --provider openai"
echo " # Enter your API key when prompted"
echo ""
echo " Option C - Anthropic Claude:"
echo " sudo -u openclaw openclaw models auth add --provider anthropic"
echo " # Enter your API key when prompted"
echo ""
echo " 2. Configure Channels (Optional - for messaging):"
echo " sudo -u openclaw openclaw channels add --channel telegram --token YOUR_BOT_TOKEN"
echo " sudo -u openclaw openclaw channels add --channel discord --token YOUR_BOT_TOKEN"
echo ""
echo " 3. Verify Installation:"
echo " sudo -u openclaw openclaw doctor"
echo " sudo -u openclaw openclaw gateway status"
echo " sudo -u openclaw openclaw models status"
echo ""
echo " 4. View Logs:"
echo " sudo -u openclaw openclaw logs --follow"
echo ""
echo "═══════════════════════════════════════════════════════════════════════════════"
echo " Memory Search Configuration"
echo "═══════════════════════════════════════════════════════════════════════════════"
echo ""
echo " Memory search requires an embedding provider. Configure one of:"
echo " - Ollama (local): https://docs.openclaw.ai/reference/memory-config"
echo " - OpenAI, Gemini, Voyage, or Mistral (cloud)"
echo ""
echo " To configure Ollama (recommended for offline use):"
echo " 1. Ensure Ollama is running on this host or accessible remotely"
echo " 2. Pull an embedding model: ollama pull nomic-embed-text"
echo " 3. Edit config: nano ~/.openclaw/openclaw.json"
echo ""
echo " Add to config under 'agents.defaults.memorySearch':"
echo ' { "provider": "ollama", "model": "nomic-embed-text",'
echo ' "remote": { "baseUrl": "http://YOUR_OLLAMA_HOST:11434" } }'
echo ""
echo " See docs/guides/openclaw-configuration.md for detailed setup."
echo ""
motd_ssh
customize
cleanup_lxc
+56 -19
View File
@@ -4,6 +4,7 @@ source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/build.func)
# Author: BIllyOutlast
# License: MIT | https://github.com/Heretek-AI/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/openclaw/openclaw
# Documentation: https://docs.openclaw.ai
APP="openclaw"
var_tags="${var_tags:-ai;assistant;automation}"
@@ -25,13 +26,14 @@ function update_script() {
check_container_resources
# Check if openclaw is installed (npm global bin location varies)
if ! command -v openclaw &>/dev/null; then
if ! command -v openclaw &>/dev/null && [[ ! -f /home/openclaw/.npm-global/bin/openclaw ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
# Get current version
CURRENT_VERSION=$(openclaw --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
# Using sudo -u since openclaw user has nologin shell
CURRENT_VERSION=$(sudo -u openclaw env PATH="/home/openclaw/.npm-global/bin:$PATH" openclaw --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
# Get latest version from npm
LATEST_VERSION=$(npm view openclaw version 2>/dev/null || echo "unknown")
@@ -48,33 +50,68 @@ function update_script() {
msg_info "Updating OpenClaw from v${CURRENT_VERSION} to v${LATEST_VERSION}"
msg_info "Stopping Service"
su - openclaw -c "systemctl --user stop openclaw-gateway" 2>/dev/null || true
msg_ok "Stopped Service"
msg_info "Stopping Gateway Service"
sudo -u openclaw systemctl --user stop openclaw-gateway 2>/dev/null || true
msg_ok "Stopped Gateway Service"
msg_info "Backing up Configuration"
BACKUP_DIR="/tmp/openclaw_backup_$(date +%Y%m%d_%H%M%S)"
if [[ -d /home/openclaw/.openclaw ]]; then
cp -r /home/openclaw/.openclaw /tmp/openclaw_backup
mkdir -p "$BACKUP_DIR"
cp -r /home/openclaw/.openclaw/* "$BACKUP_DIR"/ 2>/dev/null || true
msg_ok "Backed up Configuration to $BACKUP_DIR"
else
msg_warn "No configuration directory found"
fi
msg_ok "Backed up Configuration"
msg_info "Updating OpenClaw"
$STD npm update -g openclaw
msg_ok "Updated OpenClaw"
msg_info "Updating OpenClaw Package"
# Run npm update as the openclaw user using sudo -u (nologin shell)
sudo -u openclaw env PATH="/home/openclaw/.npm-global/bin:$PATH" npm update -g openclaw 2>/dev/null
msg_ok "Updated OpenClaw Package"
msg_info "Restoring Configuration"
if [[ -d /tmp/openclaw_backup ]]; then
cp -r /tmp/openclaw_backup/. /home/openclaw/.openclaw 2>/dev/null || true
rm -rf /tmp/openclaw_backup
msg_info "Verifying Update"
NEW_VERSION=$(sudo -u openclaw env PATH="/home/openclaw/.npm-global/bin:$PATH" openclaw --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
if [[ "$NEW_VERSION" == "$LATEST_VERSION" ]]; then
msg_ok "Verified OpenClaw v${NEW_VERSION}"
else
msg_warn "Version verification failed. Installed: v${NEW_VERSION}, Expected: v${LATEST_VERSION}"
fi
chown -R openclaw:openclaw /home/openclaw/.openclaw 2>/dev/null || true
msg_ok "Restored Configuration"
msg_info "Starting Service"
su - openclaw -c "systemctl --user start openclaw-gateway" 2>/dev/null || true
msg_ok "Started Service"
msg_info "Starting Gateway Service"
sudo -u openclaw systemctl --user start openclaw-gateway 2>/dev/null || true
# Wait for service to start
sleep 3
# Check if service is running
if sudo -u openclaw systemctl --user is-active openclaw-gateway 2>/dev/null | grep -q "active"; then
msg_ok "Gateway Service Started"
else
msg_warn "Gateway Service may not have started properly"
msg_info "Checking service status..."
sudo -u openclaw systemctl --user status openclaw-gateway 2>/dev/null || true
fi
msg_ok "Updated successfully to v${LATEST_VERSION}!"
echo ""
echo "═══════════════════════════════════════════════════════════════════════════════"
echo " Post-Update Verification"
echo "═══════════════════════════════════════════════════════════════════════════════"
echo ""
echo " Note: The openclaw user has no login shell. Use 'sudo -u openclaw' to run commands."
echo ""
echo " Run these commands to verify the update:"
echo " sudo -u openclaw openclaw --version"
echo " sudo -u openclaw openclaw doctor"
echo " sudo -u openclaw openclaw gateway status"
echo ""
echo " View logs if issues occur:"
echo " sudo -u openclaw openclaw logs --follow"
echo ""
echo " Backup saved to: $BACKUP_DIR"
echo ""
exit
}
@@ -1,5 +1,315 @@
# OpenClaw Configuration Guide
## Quick Start
After installation, OpenClaw requires a **model provider** to function. This is the most critical configuration step.
### Essential Commands
**Note:** The `openclaw` user has no login shell for security. Use `sudo -u openclaw` to run commands.
```bash
# Verify installation
sudo -u openclaw openclaw --version
sudo -u openclaw openclaw doctor
sudo -u openclaw openclaw gateway status
# View logs
sudo -u openclaw openclaw logs --follow
```
---
## Model Provider Configuration (REQUIRED)
OpenClaw requires a model provider to process messages. Choose one of the following options:
### Option A: Ollama (Local, Free, Recommended for Offline)
Ollama runs models locally on your hardware - no API costs, complete privacy.
#### 1. Install Ollama
```bash
# On the OpenClaw container or a separate server
curl -fsSL https://ollama.com/install.sh | sh
```
#### 2. Pull Required Models
```bash
# Chat model (choose one)
ollama pull llama3.2 # Good balance (recommended)
ollama pull llama3.1:8b # Larger, more capable
ollama pull mistral # Alternative option
ollama pull gemma2 # Google's model
# Embedding model (required for memory search)
ollama pull nomic-embed-text # Recommended (274MB)
ollama pull mxbai-embed-large # Higher quality (670MB)
ollama pull all-minilm # Smallest, fastest (45MB)
```
#### 3. Configure OpenClaw for Ollama
```bash
sudo -u openclaw openclaw configure
# Select "Ollama" when prompted for model provider
# Enter Ollama URL (default: http://localhost:11434)
```
Or manually edit `~/.openclaw/openclaw.json`:
```json
{
"agents": {
"defaults": {
"model": {
"primary": "ollama:llama3.2"
},
"memorySearch": {
"provider": "ollama",
"model": "nomic-embed-text",
"remote": {
"baseUrl": "http://localhost:11434"
}
}
}
}
}
```
#### 4. Ollama on a Separate Server
If Ollama runs on a different machine:
```json
{
"agents": {
"defaults": {
"model": {
"primary": "ollama:llama3.2"
},
"memorySearch": {
"provider": "ollama",
"model": "nomic-embed-text",
"remote": {
"baseUrl": "http://192.168.1.100:11434"
}
}
}
}
}
```
### Option B: OpenAI API
```bash
sudo -u openclaw openclaw models auth add --provider openai
# Enter your API key when prompted
# Set as default model
sudo -u openclaw openclaw models set openai:gpt-4o
```
Manual configuration:
```json
{
"secrets": {
"providers": {
"openai-api": {
"source": "env",
"env": "OPENAI_API_KEY"
}
}
},
"agents": {
"defaults": {
"model": {
"primary": "openai:gpt-4o"
}
}
}
}
```
### Option C: Anthropic Claude
```bash
sudo -u openclaw openclaw models auth add --provider anthropic
# Enter your API key when prompted
# Set as default model
sudo -u openclaw openclaw models set anthropic:claude-opus-4-6
```
### Option D: Other Providers
OpenClaw supports many providers:
| Provider | Command |
| ------------- | ------------------------------------------------ |
| Google Gemini | `openclaw models auth add --provider google` |
| Mistral | `openclaw models auth add --provider mistral` |
| OpenRouter | `openclaw models auth add --provider openrouter` |
| Groq | `openclaw models auth add --provider groq` |
| xAI | `openclaw models auth add --provider xai` |
| Together AI | `openclaw models auth add --provider together` |
### Verify Model Configuration
```bash
# Check model status
openclaw models status
# Test with a message
openclaw agent --message "Hello, are you working?" --local
```
---
## Channel Configuration
OpenClaw supports multiple messaging channels. Configure channels to interact through your preferred platform.
### Supported Channels
| Channel | Type | Setup Method |
| --------------- | ------------- | ------------------------------- |
| Telegram | Bot API | Bot token from @BotFather |
| Discord | Bot API | Bot token from Developer Portal |
| WhatsApp | Baileys | QR code pairing |
| Slack | Bolt SDK | App credentials |
| Signal | signal-cli | Phone number pairing |
| Matrix | Plugin | Homeserver config |
| iMessage | BlueBubbles | macOS server required |
| Microsoft Teams | Bot Framework | App registration |
| IRC | Native | Server connection |
| Google Chat | Webhook | App configuration |
### Telegram Setup (Easiest)
1. **Create a bot** by messaging [@BotFather](https://t.me/BotFather) on Telegram
2. **Get your bot token** (format: `123456789:ABCdefGHIjklMNOpqrsTUVwxyz`)
3. **Configure OpenClaw**:
```bash
sudo -u openclaw openclaw channels add --channel telegram --token "YOUR_BOT_TOKEN"
```
4. **Start messaging** your bot on Telegram
### Discord Setup
1. **Create a bot** at [Discord Developer Portal](https://discord.com/developers/applications)
2. **Get your bot token** from the Bot section
3. **Invite bot to server** using OAuth2 URL generator
4. **Configure OpenClaw**:
```bash
sudo -u openclaw openclaw channels add --channel discord --token "YOUR_BOT_TOKEN"
```
### WhatsApp Setup
WhatsApp requires QR code pairing:
```bash
sudo -u openclaw openclaw channels add --channel whatsapp
# A QR code will be displayed
# Scan with WhatsApp on your phone (Settings > Linked Devices)
```
### Multiple Accounts
You can configure multiple accounts per channel:
```bash
# Add a second Telegram account
openclaw channels add --channel telegram --account work --name "Work Bot" --token "WORK_BOT_TOKEN"
# List all channels
openclaw channels list
# Check channel status
openclaw channels status --probe
```
### Channel Routing
Route different channels to different agents:
```bash
# Create an agent
openclaw agents add --workspace ~/.openclaw-workspaces/work
# Bind channels to agent
openclaw agents bind --agent <agent-id> --bind telegram:work
openclaw agents bind --agent <agent-id> --bind discord:main
```
---
## Memory Configuration
OpenClaw's memory search requires an embedding provider to index and search through your memory files.
### Memory Files Location
Memory files are stored in `~/.openclaw/workspace/memory/`:
```bash
# Create memory directory
mkdir -p ~/.openclaw/workspace/memory
# Create your first memory file
cat > ~/.openclaw/workspace/MEMORY.md << 'EOF'
# Personal Memory
## Preferences
- I prefer concise responses
- Use bullet points for lists
## Projects
- Home automation: running on Proxmox
- Media server: Jellyfin on port 8096
EOF
```
### Configure Memory Search
```json
{
"agents": {
"defaults": {
"memorySearch": {
"provider": "ollama",
"model": "nomic-embed-text",
"remote": {
"baseUrl": "http://localhost:11434"
}
}
}
}
}
```
### Memory Commands
```bash
# Check memory status
openclaw memory status
# Reindex memory files
openclaw memory index
# Search memory
openclaw memory search "project details"
```
---
## "Device Identity Required" Error Fix
If you see this error when accessing the Control UI from another machine:
@@ -43,7 +353,6 @@ nano ~/.openclaw/openclaw.json
"allowInsecureAuth": true
},
"auth": {
"mode": "token",
"token": "your-secure-token-here"
}
}
@@ -64,7 +373,7 @@ su - openclaw -c "systemctl --user restart openclaw-gateway"
### Solution 3: HTTPS Reverse Proxy (Production)
Set up Caddy or Nginx with SSL certificates for secure HTTPS access.
The installation includes Caddy for HTTPS access on port 18790.
### Solution 4: Tailscale VPN
@@ -86,6 +395,8 @@ For emergency access only, you can completely disable device auth:
**⚠️ WARNING:** This is a severe security downgrade. Revert immediately after emergency use.
---
## "Origin Not Allowed" Error Fix
If you see this error when accessing the Control UI from another machine:
@@ -101,8 +412,8 @@ origin not allowed (open the Control UI from the gateway host or allow it in gat
CONTAINER_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
# Create configuration with allowed origins
mkdir -p ~/.openclaw
cat > ~/.openclaw/openclaw.json << EOF
sudo -u openclaw mkdir -p /home/openclaw/.openclaw
sudo -u openclaw tee /home/openclaw/.openclaw/openclaw.json > /dev/null << EOF
{
"gateway": {
"bind": "lan",
@@ -119,11 +430,13 @@ cat > ~/.openclaw/openclaw.json << EOF
EOF
# Apply and restart
su - openclaw -c "systemctl --user restart openclaw-gateway"
sudo -u openclaw systemctl --user restart openclaw-gateway
```
**Note:** Even with `allowedOrigins` configured, you still need a secure context (HTTPS or localhost) for the device identity requirement. Use SSH tunneling for LAN access.
---
## Network Binding Configuration
By default, OpenClaw binds to localhost (127.0.0.1) only. To make it accessible from other machines on your network, you need to:
@@ -144,11 +457,11 @@ By default, OpenClaw binds to localhost (127.0.0.1) only. To make it accessible
```bash
# Open the gateway configuration
openclaw configure --section gateway
sudo -u openclaw openclaw configure --section gateway
# Set bind to "lan" when prompted
# Then restart the service
su - openclaw -c "systemctl --user restart openclaw-gateway"
sudo -u openclaw systemctl --user restart openclaw-gateway
```
### Method 2: Manual Configuration
@@ -156,9 +469,8 @@ su - openclaw -c "systemctl --user restart openclaw-gateway"
Edit the configuration file:
```bash
# Edit as the openclaw user
su - openclaw
nano ~/.openclaw/openclaw.json
# Edit the configuration file
nano /home/openclaw/.openclaw/openclaw.json
```
Add the configuration with your IP:
@@ -178,7 +490,7 @@ Add the configuration with your IP:
Apply changes:
```bash
systemctl --user restart openclaw-gateway
sudo -u openclaw systemctl --user restart openclaw-gateway
```
### Method 3: Quick Fix for Existing Installations
@@ -189,26 +501,26 @@ For existing installations, run these commands:
# Get your container IP
CONTAINER_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
# Create configuration with allowed origins (as openclaw user)
su - openclaw -c "mkdir -p ~/.openclaw"
su - openclaw -c "cat > ~/.openclaw/openclaw.json << EOF
# Create configuration with allowed origins
sudo -u openclaw mkdir -p /home/openclaw/.openclaw
sudo -u openclaw tee /home/openclaw/.openclaw/openclaw.json > /dev/null << EOF
{
\"gateway\": {
\"bind\": \"lan\",
\"port\": 18789,
\"controlUi\": {
\"allowedOrigins\": [
\"http://localhost:18789\",
\"http://127.0.0.1:18789\",
\"http://${CONTAINER_IP}:18789\"
"gateway": {
"bind": "lan",
"port": 18789,
"controlUi": {
"allowedOrigins": [
"http://localhost:18789",
"http://127.0.0.1:18789",
"http://${CONTAINER_IP}:18789"
]
}
}
}
EOF"
EOF
# Restart the service
su - openclaw -c "systemctl --user restart openclaw-gateway"
sudo -u openclaw systemctl --user restart openclaw-gateway
```
### Method 4: Environment Variable
@@ -225,7 +537,7 @@ OPENCLAW_BIND=lan
Check the current binding:
```bash
openclaw gateway status
sudo -u openclaw openclaw gateway status
```
Output should show:
@@ -284,9 +596,9 @@ http://localhost:18789
2. Set OpenClaw bind to `tailnet`:
```bash
openclaw configure --section gateway
sudo -u openclaw openclaw configure --section gateway
# Set bind to "tailnet"
su - openclaw -c "systemctl --user restart openclaw-gateway"
sudo -u openclaw systemctl --user restart openclaw-gateway
```
3. Access via Tailscale IP: `http://<tailscale-ip>:18789`
@@ -296,8 +608,8 @@ http://localhost:18789
### Gateway Not Starting
```bash
# Check logs (as openclaw user)
su - openclaw -c "journalctl --user -u openclaw-gateway -f"
# Check logs
sudo -u openclaw journalctl --user -u openclaw-gateway -f
# Or from root
journalctl --user -u openclaw-gateway -f --user-unit
@@ -305,83 +617,147 @@ journalctl --user -u openclaw-gateway -f --user-unit
# Common issues:
# - Port already in use: lsof -i :18789
# - Permission denied: ensure running as correct user
# - Config errors: sudo -u openclaw openclaw doctor
```
### Cannot Connect from Remote
1. Verify binding: `openclaw gateway status` should show `0.0.0.0:18789`
1. Verify binding: `sudo -u openclaw openclaw gateway status` should show `0.0.0.0:18789`
2. Check firewall: `ufw status` or `iptables -L -n`
3. Verify network connectivity: `ping <server-ip>`
### Token Authentication Issues
```bash
# Regenerate token
openclaw doctor --generate-gateway-token
# View current token
openclaw gateway status
sudo -u openclaw openclaw gateway status
# Regenerate token (if needed)
sudo -u openclaw openclaw doctor --generate-gateway-token
```
## Installing Homebrew (Optional)
If you need to install additional tools via Homebrew, follow these steps:
### Prerequisites
The `openclaw` user needs sudo access and a password set. By default, the user is created without a password.
### Step 1: Set Password for openclaw User
As root, set a password for the openclaw user:
### Model Provider Issues
```bash
passwd openclaw
# Enter and confirm the new password
# Check model status
sudo -u openclaw openclaw models status
# Probe providers (may consume tokens)
sudo -u openclaw openclaw models status --probe
# Set default model
sudo -u openclaw openclaw models set <provider:model>
# Example:
sudo -u openclaw openclaw models set ollama:llama3.2
sudo -u openclaw openclaw models set openai:gpt-4o
```
### Step 2: Install Homebrew
Switch to the openclaw user and run the installer:
### Channel Issues
```bash
su - openclaw
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Check channel status
sudo -u openclaw openclaw channels status --probe
# View channel logs
sudo -u openclaw openclaw channels logs --channel telegram
# Re-login to a channel
sudo -u openclaw openclaw channels login --channel whatsapp
```
When prompted:
## Using Homebrew
1. Enter the openclaw password for sudo access
2. Press ENTER to continue when the installer shows the directories it will create
Homebrew is installed automatically during the OpenClaw installation. It provides access to additional packages that may be useful for extending OpenClaw functionality.
### Step 3: Add Homebrew to PATH
After installation, add Homebrew to your shell:
### Verify Installation
```bash
echo >> /home/openclaw/.bashrc
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"' >> /home/openclaw/.bashrc
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"
```
### Step 4: Verify Installation
```bash
brew --version
sudo -u openclaw brew --version
```
### Installing Packages
Once Homebrew is installed, you can install packages:
Once Homebrew is installed, you can install additional packages:
```bash
brew install <package-name>
```
### Common Useful Packages
```bash
# Example: Install additional tools
brew install jq # JSON processor
brew install yq # YAML processor
brew install git-delta # Better git diff viewer
```
**Note:** Homebrew packages are installed to `/home/linuxbrew/.linuxbrew/` and are available to all users on the system.
### Troubleshooting Homebrew
If Homebrew is not working correctly:
```bash
# Check if Homebrew is in PATH
which brew
# If not, add it manually
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"
# Add to .bashrc permanently
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"' >> ~/.bashrc
```
---
## Embedding Model Comparison
| Model | Size | Quality | Speed | Use Case |
| ----------------- | ------ | ------- | ------- | ----------------------------- |
| nomic-embed-text | ~274MB | Good | Fast | General purpose (recommended) |
| mxbai-embed-large | ~670MB | Better | Medium | Higher accuracy needed |
| all-minilm | ~45MB | Basic | Fastest | Resource-constrained |
---
## Hybrid Search (BM25 + Vector)
Enable hybrid search for better recall:
```json
{
"agents": {
"defaults": {
"memorySearch": {
"provider": "ollama",
"model": "nomic-embed-text",
"remote": {
"baseUrl": "http://localhost:11434"
},
"query": {
"hybrid": {
"enabled": true,
"vectorWeight": 0.7,
"textWeight": 0.3
}
}
}
}
}
}
```
---
## Additional Resources
- [OpenClaw Gateway Documentation](https://docs.openclaw.ai/gateway)
- [OpenClaw Security Guide](https://docs.openclaw.ai/gateway/security)
- [OpenClaw Troubleshooting](https://docs.openclaw.ai/gateway/troubleshooting)
- [OpenClaw Memory Configuration Reference](https://docs.openclaw.ai/reference/memory-config)
- [OpenClaw CLI Reference](https://docs.openclaw.ai/cli)
- [OpenClaw Channels](https://docs.openclaw.ai/channels)
- [OpenClaw Model Providers](https://docs.openclaw.ai/providers)
- [Ollama Documentation](https://ollama.com/docs)
+58 -19
View File
@@ -4,6 +4,7 @@ source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/build.func)
# Author: BIllyOutlast
# License: MIT | https://github.com/Heretek-AI/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/openclaw/openclaw
# Documentation: https://docs.openclaw.ai
APP="openclaw"
var_tags="${var_tags:-ai;assistant;automation}"
@@ -24,13 +25,15 @@ function update_script() {
check_container_storage
check_container_resources
if [[ ! -x /usr/bin/openclaw ]]; then
# Check if openclaw is installed (npm global bin location varies)
if ! command -v openclaw &>/dev/null && [[ ! -f /home/openclaw/.npm-global/bin/openclaw ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
# Get current version
CURRENT_VERSION=$(openclaw --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
# Using sudo -u since openclaw user has nologin shell
CURRENT_VERSION=$(sudo -u openclaw env PATH="/home/openclaw/.npm-global/bin:$PATH" openclaw --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
# Get latest version from npm
LATEST_VERSION=$(npm view openclaw version 2>/dev/null || echo "unknown")
@@ -47,32 +50,68 @@ function update_script() {
msg_info "Updating OpenClaw from v${CURRENT_VERSION} to v${LATEST_VERSION}"
msg_info "Stopping Service"
systemctl stop openclaw
msg_ok "Stopped Service"
msg_info "Stopping Gateway Service"
sudo -u openclaw systemctl --user stop openclaw-gateway 2>/dev/null || true
msg_ok "Stopped Gateway Service"
msg_info "Backing up Configuration"
if [[ -d /root/.openclaw ]]; then
cp -r /root/.openclaw /tmp/openclaw_backup
BACKUP_DIR="/tmp/openclaw_backup_$(date +%Y%m%d_%H%M%S)"
if [[ -d /home/openclaw/.openclaw ]]; then
mkdir -p "$BACKUP_DIR"
cp -r /home/openclaw/.openclaw/* "$BACKUP_DIR"/ 2>/dev/null || true
msg_ok "Backed up Configuration to $BACKUP_DIR"
else
msg_warn "No configuration directory found"
fi
msg_ok "Backed up Configuration"
msg_info "Updating OpenClaw"
$STD npm update -g openclaw
msg_ok "Updated OpenClaw"
msg_info "Updating OpenClaw Package"
# Run npm update as the openclaw user using sudo -u (nologin shell)
sudo -u openclaw env PATH="/home/openclaw/.npm-global/bin:$PATH" npm update -g openclaw 2>/dev/null
msg_ok "Updated OpenClaw Package"
msg_info "Restoring Configuration"
if [[ -d /tmp/openclaw_backup ]]; then
cp -r /tmp/openclaw_backup/. /root/.openclaw 2>/dev/null || true
rm -rf /tmp/openclaw_backup
msg_info "Verifying Update"
NEW_VERSION=$(sudo -u openclaw env PATH="/home/openclaw/.npm-global/bin:$PATH" openclaw --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
if [[ "$NEW_VERSION" == "$LATEST_VERSION" ]]; then
msg_ok "Verified OpenClaw v${NEW_VERSION}"
else
msg_warn "Version verification failed. Installed: v${NEW_VERSION}, Expected: v${LATEST_VERSION}"
fi
msg_ok "Restored Configuration"
msg_info "Starting Service"
systemctl start openclaw
msg_ok "Started Service"
msg_info "Starting Gateway Service"
sudo -u openclaw systemctl --user start openclaw-gateway 2>/dev/null || true
# Wait for service to start
sleep 3
# Check if service is running
if sudo -u openclaw systemctl --user is-active openclaw-gateway 2>/dev/null | grep -q "active"; then
msg_ok "Gateway Service Started"
else
msg_warn "Gateway Service may not have started properly"
msg_info "Checking service status..."
sudo -u openclaw systemctl --user status openclaw-gateway 2>/dev/null || true
fi
msg_ok "Updated successfully to v${LATEST_VERSION}!"
echo ""
echo "═══════════════════════════════════════════════════════════════════════════════"
echo " Post-Update Verification"
echo "═══════════════════════════════════════════════════════════════════════════════"
echo ""
echo " Note: The openclaw user has no login shell. Use 'sudo -u openclaw' to run commands."
echo ""
echo " Run these commands to verify the update:"
echo " sudo -u openclaw openclaw --version"
echo " sudo -u openclaw openclaw doctor"
echo " sudo -u openclaw openclaw gateway status"
echo ""
echo " View logs if issues occur:"
echo " sudo -u openclaw openclaw logs --follow"
echo ""
echo " Backup saved to: $BACKUP_DIR"
echo ""
exit
}
@@ -1,5 +1,315 @@
# OpenClaw Configuration Guide
## Quick Start
After installation, OpenClaw requires a **model provider** to function. This is the most critical configuration step.
### Essential Commands
**Note:** The `openclaw` user has no login shell for security. Use `sudo -u openclaw` to run commands.
```bash
# Verify installation
sudo -u openclaw openclaw --version
sudo -u openclaw openclaw doctor
sudo -u openclaw openclaw gateway status
# View logs
sudo -u openclaw openclaw logs --follow
```
---
## Model Provider Configuration (REQUIRED)
OpenClaw requires a model provider to process messages. Choose one of the following options:
### Option A: Ollama (Local, Free, Recommended for Offline)
Ollama runs models locally on your hardware - no API costs, complete privacy.
#### 1. Install Ollama
```bash
# On the OpenClaw container or a separate server
curl -fsSL https://ollama.com/install.sh | sh
```
#### 2. Pull Required Models
```bash
# Chat model (choose one)
ollama pull llama3.2 # Good balance (recommended)
ollama pull llama3.1:8b # Larger, more capable
ollama pull mistral # Alternative option
ollama pull gemma2 # Google's model
# Embedding model (required for memory search)
ollama pull nomic-embed-text # Recommended (274MB)
ollama pull mxbai-embed-large # Higher quality (670MB)
ollama pull all-minilm # Smallest, fastest (45MB)
```
#### 3. Configure OpenClaw for Ollama
```bash
sudo -u openclaw openclaw configure
# Select "Ollama" when prompted for model provider
# Enter Ollama URL (default: http://localhost:11434)
```
Or manually edit `~/.openclaw/openclaw.json`:
```json
{
"agents": {
"defaults": {
"model": {
"primary": "ollama:llama3.2"
},
"memorySearch": {
"provider": "ollama",
"model": "nomic-embed-text",
"remote": {
"baseUrl": "http://localhost:11434"
}
}
}
}
}
```
#### 4. Ollama on a Separate Server
If Ollama runs on a different machine:
```json
{
"agents": {
"defaults": {
"model": {
"primary": "ollama:llama3.2"
},
"memorySearch": {
"provider": "ollama",
"model": "nomic-embed-text",
"remote": {
"baseUrl": "http://192.168.1.100:11434"
}
}
}
}
}
```
### Option B: OpenAI API
```bash
sudo -u openclaw openclaw models auth add --provider openai
# Enter your API key when prompted
# Set as default model
sudo -u openclaw openclaw models set openai:gpt-4o
```
Manual configuration:
```json
{
"secrets": {
"providers": {
"openai-api": {
"source": "env",
"env": "OPENAI_API_KEY"
}
}
},
"agents": {
"defaults": {
"model": {
"primary": "openai:gpt-4o"
}
}
}
}
```
### Option C: Anthropic Claude
```bash
sudo -u openclaw openclaw models auth add --provider anthropic
# Enter your API key when prompted
# Set as default model
sudo -u openclaw openclaw models set anthropic:claude-opus-4-6
```
### Option D: Other Providers
OpenClaw supports many providers:
| Provider | Command |
| ------------- | ------------------------------------------------ |
| Google Gemini | `openclaw models auth add --provider google` |
| Mistral | `openclaw models auth add --provider mistral` |
| OpenRouter | `openclaw models auth add --provider openrouter` |
| Groq | `openclaw models auth add --provider groq` |
| xAI | `openclaw models auth add --provider xai` |
| Together AI | `openclaw models auth add --provider together` |
### Verify Model Configuration
```bash
# Check model status
openclaw models status
# Test with a message
openclaw agent --message "Hello, are you working?" --local
```
---
## Channel Configuration
OpenClaw supports multiple messaging channels. Configure channels to interact through your preferred platform.
### Supported Channels
| Channel | Type | Setup Method |
| --------------- | ------------- | ------------------------------- |
| Telegram | Bot API | Bot token from @BotFather |
| Discord | Bot API | Bot token from Developer Portal |
| WhatsApp | Baileys | QR code pairing |
| Slack | Bolt SDK | App credentials |
| Signal | signal-cli | Phone number pairing |
| Matrix | Plugin | Homeserver config |
| iMessage | BlueBubbles | macOS server required |
| Microsoft Teams | Bot Framework | App registration |
| IRC | Native | Server connection |
| Google Chat | Webhook | App configuration |
### Telegram Setup (Easiest)
1. **Create a bot** by messaging [@BotFather](https://t.me/BotFather) on Telegram
2. **Get your bot token** (format: `123456789:ABCdefGHIjklMNOpqrsTUVwxyz`)
3. **Configure OpenClaw**:
```bash
sudo -u openclaw openclaw channels add --channel telegram --token "YOUR_BOT_TOKEN"
```
4. **Start messaging** your bot on Telegram
### Discord Setup
1. **Create a bot** at [Discord Developer Portal](https://discord.com/developers/applications)
2. **Get your bot token** from the Bot section
3. **Invite bot to server** using OAuth2 URL generator
4. **Configure OpenClaw**:
```bash
sudo -u openclaw openclaw channels add --channel discord --token "YOUR_BOT_TOKEN"
```
### WhatsApp Setup
WhatsApp requires QR code pairing:
```bash
sudo -u openclaw openclaw channels add --channel whatsapp
# A QR code will be displayed
# Scan with WhatsApp on your phone (Settings > Linked Devices)
```
### Multiple Accounts
You can configure multiple accounts per channel:
```bash
# Add a second Telegram account
openclaw channels add --channel telegram --account work --name "Work Bot" --token "WORK_BOT_TOKEN"
# List all channels
openclaw channels list
# Check channel status
openclaw channels status --probe
```
### Channel Routing
Route different channels to different agents:
```bash
# Create an agent
openclaw agents add --workspace ~/.openclaw-workspaces/work
# Bind channels to agent
openclaw agents bind --agent <agent-id> --bind telegram:work
openclaw agents bind --agent <agent-id> --bind discord:main
```
---
## Memory Configuration
OpenClaw's memory search requires an embedding provider to index and search through your memory files.
### Memory Files Location
Memory files are stored in `~/.openclaw/workspace/memory/`:
```bash
# Create memory directory
mkdir -p ~/.openclaw/workspace/memory
# Create your first memory file
cat > ~/.openclaw/workspace/MEMORY.md << 'EOF'
# Personal Memory
## Preferences
- I prefer concise responses
- Use bullet points for lists
## Projects
- Home automation: running on Proxmox
- Media server: Jellyfin on port 8096
EOF
```
### Configure Memory Search
```json
{
"agents": {
"defaults": {
"memorySearch": {
"provider": "ollama",
"model": "nomic-embed-text",
"remote": {
"baseUrl": "http://localhost:11434"
}
}
}
}
}
```
### Memory Commands
```bash
# Check memory status
openclaw memory status
# Reindex memory files
openclaw memory index
# Search memory
openclaw memory search "project details"
```
---
## "Device Identity Required" Error Fix
If you see this error when accessing the Control UI from another machine:
@@ -43,7 +353,6 @@ nano ~/.openclaw/openclaw.json
"allowInsecureAuth": true
},
"auth": {
"mode": "token",
"token": "your-secure-token-here"
}
}
@@ -53,7 +362,7 @@ nano ~/.openclaw/openclaw.json
Then restart:
```bash
systemctl restart openclaw
su - openclaw -c "systemctl --user restart openclaw-gateway"
```
**Important:**
@@ -64,7 +373,7 @@ systemctl restart openclaw
### Solution 3: HTTPS Reverse Proxy (Production)
Set up Caddy or Nginx with SSL certificates for secure HTTPS access.
The installation includes Caddy for HTTPS access on port 18790.
### Solution 4: Tailscale VPN
@@ -86,6 +395,8 @@ For emergency access only, you can completely disable device auth:
**⚠️ WARNING:** This is a severe security downgrade. Revert immediately after emergency use.
---
## "Origin Not Allowed" Error Fix
If you see this error when accessing the Control UI from another machine:
@@ -101,8 +412,8 @@ origin not allowed (open the Control UI from the gateway host or allow it in gat
CONTAINER_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
# Create configuration with allowed origins
mkdir -p ~/.openclaw
cat > ~/.openclaw/openclaw.json << EOF
sudo -u openclaw mkdir -p /home/openclaw/.openclaw
sudo -u openclaw tee /home/openclaw/.openclaw/openclaw.json > /dev/null << EOF
{
"gateway": {
"bind": "lan",
@@ -118,16 +429,14 @@ cat > ~/.openclaw/openclaw.json << EOF
}
EOF
# Update service to use lan binding
sed -i 's|--port 18789|--port 18789 --bind lan|' /etc/systemd/system/openclaw.service
# Apply and restart
systemctl daemon-reload
systemctl restart openclaw
sudo -u openclaw systemctl --user restart openclaw-gateway
```
**Note:** Even with `allowedOrigins` configured, you still need a secure context (HTTPS or localhost) for the device identity requirement. Use SSH tunneling for LAN access.
---
## Network Binding Configuration
By default, OpenClaw binds to localhost (127.0.0.1) only. To make it accessible from other machines on your network, you need to:
@@ -148,20 +457,20 @@ By default, OpenClaw binds to localhost (127.0.0.1) only. To make it accessible
```bash
# Open the gateway configuration
openclaw configure --section gateway
sudo -u openclaw openclaw configure --section gateway
# Set bind to "lan" when prompted
# Then restart the service
systemctl restart openclaw
sudo -u openclaw systemctl --user restart openclaw-gateway
```
### Method 2: Manual Configuration
Edit both the configuration file and systemd service:
Edit the configuration file:
```bash
# Create/edit the configuration file
nano ~/.openclaw/openclaw.json
# Edit the configuration file
nano /home/openclaw/.openclaw/openclaw.json
```
Add the configuration with your IP:
@@ -178,24 +487,10 @@ Add the configuration with your IP:
}
```
Then edit the systemd service:
```bash
nano /etc/systemd/system/openclaw.service
```
Modify the `ExecStart` line to include `--bind lan`:
```ini
[Service]
ExecStart=/usr/bin/openclaw gateway --port 18789 --bind lan
```
Apply changes:
```bash
systemctl daemon-reload
systemctl restart openclaw
sudo -u openclaw systemctl --user restart openclaw-gateway
```
### Method 3: Quick Fix for Existing Installations
@@ -207,8 +502,8 @@ For existing installations, run these commands:
CONTAINER_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
# Create configuration with allowed origins
mkdir -p ~/.openclaw
cat > ~/.openclaw/openclaw.json << EOF
sudo -u openclaw mkdir -p /home/openclaw/.openclaw
sudo -u openclaw tee /home/openclaw/.openclaw/openclaw.json > /dev/null << EOF
{
"gateway": {
"bind": "lan",
@@ -224,12 +519,8 @@ cat > ~/.openclaw/openclaw.json << EOF
}
EOF
# Update service to use lan binding
sed -i 's|--port 18789|--port 18789 --bind lan|' /etc/systemd/system/openclaw.service
# Apply and restart
systemctl daemon-reload
systemctl restart openclaw
# Restart the service
sudo -u openclaw systemctl --user restart openclaw-gateway
```
### Method 4: Environment Variable
@@ -246,7 +537,7 @@ OPENCLAW_BIND=lan
Check the current binding:
```bash
openclaw gateway status
sudo -u openclaw openclaw gateway status
```
Output should show:
@@ -305,9 +596,9 @@ http://localhost:18789
2. Set OpenClaw bind to `tailnet`:
```bash
openclaw configure --section gateway
sudo -u openclaw openclaw configure --section gateway
# Set bind to "tailnet"
systemctl restart openclaw
sudo -u openclaw systemctl --user restart openclaw-gateway
```
3. Access via Tailscale IP: `http://<tailscale-ip>:18789`
@@ -318,88 +609,155 @@ http://localhost:18789
```bash
# Check logs
journalctl -u openclaw -f
sudo -u openclaw journalctl --user -u openclaw-gateway -f
# Or from root
journalctl --user -u openclaw-gateway -f --user-unit
# Common issues:
# - Port already in use: lsof -i :18789
# - Permission denied: ensure running as root or correct user
# - Permission denied: ensure running as correct user
# - Config errors: sudo -u openclaw openclaw doctor
```
### Cannot Connect from Remote
1. Verify binding: `openclaw gateway status` should show `0.0.0.0:18789`
1. Verify binding: `sudo -u openclaw openclaw gateway status` should show `0.0.0.0:18789`
2. Check firewall: `ufw status` or `iptables -L -n`
3. Verify network connectivity: `ping <server-ip>`
### Token Authentication Issues
```bash
# Regenerate token
openclaw doctor --generate-gateway-token
# View current token
openclaw gateway status
sudo -u openclaw openclaw gateway status
# Regenerate token (if needed)
sudo -u openclaw openclaw doctor --generate-gateway-token
```
## Installing Homebrew (Optional)
If you need to install additional tools via Homebrew, follow these steps:
### Prerequisites
The `openclaw` user needs sudo access and a password set. By default, the user is created without a password.
### Step 1: Set Password for openclaw User
As root, set a password for the openclaw user:
### Model Provider Issues
```bash
passwd openclaw
# Enter and confirm the new password
# Check model status
sudo -u openclaw openclaw models status
# Probe providers (may consume tokens)
sudo -u openclaw openclaw models status --probe
# Set default model
sudo -u openclaw openclaw models set <provider:model>
# Example:
sudo -u openclaw openclaw models set ollama:llama3.2
sudo -u openclaw openclaw models set openai:gpt-4o
```
### Step 2: Install Homebrew
Switch to the openclaw user and run the installer:
### Channel Issues
```bash
su - openclaw
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Check channel status
sudo -u openclaw openclaw channels status --probe
# View channel logs
sudo -u openclaw openclaw channels logs --channel telegram
# Re-login to a channel
sudo -u openclaw openclaw channels login --channel whatsapp
```
When prompted:
## Using Homebrew
1. Enter the openclaw password for sudo access
2. Press ENTER to continue when the installer shows the directories it will create
Homebrew is installed automatically during the OpenClaw installation. It provides access to additional packages that may be useful for extending OpenClaw functionality.
### Step 3: Add Homebrew to PATH
After installation, add Homebrew to your shell:
### Verify Installation
```bash
echo >> /home/openclaw/.bashrc
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"' >> /home/openclaw/.bashrc
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"
```
### Step 4: Verify Installation
```bash
brew --version
sudo -u openclaw brew --version
```
### Installing Packages
Once Homebrew is installed, you can install packages:
Once Homebrew is installed, you can install additional packages:
```bash
brew install <package-name>
```
### Common Useful Packages
```bash
# Example: Install additional tools
brew install jq # JSON processor
brew install yq # YAML processor
brew install git-delta # Better git diff viewer
```
**Note:** Homebrew packages are installed to `/home/linuxbrew/.linuxbrew/` and are available to all users on the system.
### Troubleshooting Homebrew
If Homebrew is not working correctly:
```bash
# Check if Homebrew is in PATH
which brew
# If not, add it manually
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"
# Add to .bashrc permanently
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"' >> ~/.bashrc
```
---
## Embedding Model Comparison
| Model | Size | Quality | Speed | Use Case |
| ----------------- | ------ | ------- | ------- | ----------------------------- |
| nomic-embed-text | ~274MB | Good | Fast | General purpose (recommended) |
| mxbai-embed-large | ~670MB | Better | Medium | Higher accuracy needed |
| all-minilm | ~45MB | Basic | Fastest | Resource-constrained |
---
## Hybrid Search (BM25 + Vector)
Enable hybrid search for better recall:
```json
{
"agents": {
"defaults": {
"memorySearch": {
"provider": "ollama",
"model": "nomic-embed-text",
"remote": {
"baseUrl": "http://localhost:11434"
},
"query": {
"hybrid": {
"enabled": true,
"vectorWeight": 0.7,
"textWeight": 0.3
}
}
}
}
}
}
```
---
## Additional Resources
- [OpenClaw Gateway Documentation](https://docs.openclaw.ai/gateway)
- [OpenClaw Security Guide](https://docs.openclaw.ai/gateway/security)
- [OpenClaw Troubleshooting](https://docs.openclaw.ai/gateway/troubleshooting)
- [OpenClaw Memory Configuration Reference](https://docs.openclaw.ai/reference/memory-config)
- [OpenClaw CLI Reference](https://docs.openclaw.ai/cli)
- [OpenClaw Channels](https://docs.openclaw.ai/channels)
- [OpenClaw Model Providers](https://docs.openclaw.ai/providers)
- [Ollama Documentation](https://ollama.com/docs)
@@ -38,14 +38,11 @@ $STD apt-get update
$STD apt-get install -y caddy
msg_ok "Installed Caddy"
# Setup Node.js 22 (required by OpenClaw)
NODE_VERSION="22" setup_nodejs
# Setup Node.js 24 (recommended by OpenClaw)
NODE_VERSION="24" setup_nodejs
# Install uv (Python package manager)
msg_info "Installing uv (Python Package Manager)"
$STD curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
msg_ok "Installed uv"
# Install uv (Python package manager) - OpenClaw uses Python for some tools
PYTHON_VERSION="3.12" setup_uv
# Create unprivileged user for OpenClaw
msg_info "Creating OpenClaw User"
@@ -154,32 +151,17 @@ chown caddy:caddy /var/log/caddy
systemctl enable -q caddy
msg_ok "Created Caddy HTTPS Reverse Proxy"
msg_info "Creating OpenClaw Service"
cat <<EOF >/etc/systemd/system/openclaw.service
[Unit]
Description=OpenClaw Gateway - Personal AI Assistant
After=network.target network-online.target
Wants=network-online.target
msg_info "Enabling Lingering for OpenClaw User"
# Enable lingering so user-level systemd services start on boot
loginctl enable-linger openclaw 2>/dev/null || true
msg_ok "Enabled Lingering"
[Service]
Type=simple
User=openclaw
Group=openclaw
WorkingDirectory=/opt/openclaw
Environment=NODE_ENV=production
Environment=PATH=/home/openclaw/.npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ExecStart=/usr/bin/openclaw gateway --port 18789 --bind lan
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=openclaw
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now openclaw
msg_ok "Created OpenClaw Service"
msg_info "Initializing OpenClaw Gateway Service"
# Run OpenClaw installer as the openclaw user to create user-level systemd service
# The installer will prompt for hooks and create the service automatically
export PATH="/home/openclaw/.npm-global/bin:$PATH"
$run_user_cmd "openclaw install --no-hooks" 2>/dev/null || true
msg_ok "Initialized OpenClaw Gateway Service"
msg_info "Starting Caddy HTTPS Proxy"
# Restart Caddy to apply configuration
@@ -1,9 +1,7 @@
{
"name": "OpenClaw",
"slug": "openclaw",
"categories": [
20
],
"categories": [20],
"date_created": "2026-03-12",
"type": "ct",
"updateable": true,
@@ -12,7 +10,7 @@
"documentation": "https://docs.openclaw.ai",
"website": "https://openclaw.ai",
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/openclaw.webp",
"config_path": "/root/.openclaw/openclaw.json",
"config_path": "/home/openclaw/.openclaw/openclaw.json",
"description": "OpenClaw is a personal AI assistant that runs on your own devices. It supports multiple messaging channels including WhatsApp, Telegram, Slack, Discord, Signal, iMessage, IRC, Microsoft Teams, Matrix, and more. Features voice wake mode, live canvas, browser control, skills platform, and a Gateway WebSocket control plane.",
"install_methods": [
{
@@ -37,7 +35,11 @@
"type": "info"
},
{
"text": "WebSocket endpoint: ws://<IP>:18789",
"text": "HTTPS Access (secure): https://<IP>:18790",
"type": "info"
},
{
"text": "WebSocket endpoint: ws://<IP>:18789 (or wss://<IP>:18790 for secure)",
"type": "info"
},
{
@@ -45,11 +47,15 @@
"type": "info"
},
{
"text": "Run 'openclaw onboard' after installation to configure channels and AI providers",
"text": "Configuration stored in /home/openclaw/.openclaw/",
"type": "info"
},
{
"text": "Configuration stored in /root/.openclaw/",
"text": "HTTPS uses self-signed certificate - click 'Advanced' -> 'Proceed to site' in browser",
"type": "warning"
},
{
"text": "For remote LAN access, use HTTPS (port 18790) or SSH tunnel: ssh -L 18789:localhost:18789 root@<container-ip>",
"type": "info"
},
{
@@ -83,5 +83,8 @@ description
msg_ok "Completed Successfully!"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:18789${CL}"
echo -e "${INFO}${YW} Gateway WebSocket: ws://${IP}:18789${CL}"
echo -e "${TAB}${GATEWAY}${BGN}http://localhost:18789${CL}"
echo -e "${INFO}${YW} HTTPS Access (secure, works from any machine):${CL}"
echo -e "${TAB}${GATEWAY}${BGN}https://${IP}:18790${CL}"
echo -e "${INFO}${YW} Note: Your browser will warn about self-signed certificate.${CL}"
echo -e "${TAB}${YW}Click 'Advanced' -> 'Proceed to site' to continue.${CL}"
@@ -22,14 +22,70 @@ ssh -L 18789:localhost:18789 root@<container-ip>
http://localhost:18789
```
### Solution 2: HTTPS Reverse Proxy (Production)
### Solution 2: Allow Insecure Auth (LAN Only)
**Warning:** This reduces security. Only use on trusted networks.
Add `allowInsecureAuth: true` to your OpenClaw configuration:
```bash
# Edit configuration
nano ~/.openclaw/openclaw.json
```
```json
{
"gateway": {
"bind": "lan",
"port": 18789,
"controlUi": {
"allowedOrigins": ["http://localhost:18789", "http://127.0.0.1:18789", "http://192.168.31.39:18789"],
"allowInsecureAuth": true
},
"auth": {
"mode": "token",
"token": "your-secure-token-here"
}
}
}
```
Then restart:
```bash
systemctl restart openclaw
```
**Important:**
- `allowInsecureAuth` only works for localhost connections in non-secure HTTP contexts
- It does **NOT** bypass device identity for remote (non-localhost) connections
- You must still use SSH tunnel or HTTPS for remote LAN access
### Solution 3: HTTPS Reverse Proxy (Production)
Set up Caddy or Nginx with SSL certificates for secure HTTPS access.
### Solution 3: Tailscale VPN
### Solution 4: Tailscale VPN
Install Tailscale on both machines and access via Tailscale IP.
### Emergency Break-Glass (NOT Recommended)
For emergency access only, you can completely disable device auth:
```json
{
"gateway": {
"controlUi": {
"dangerouslyDisableDeviceAuth": true
}
}
}
```
**⚠️ WARNING:** This is a severe security downgrade. Revert immediately after emergency use.
## "Origin Not Allowed" Error Fix
If you see this error when accessing the Control UI from another machine:
@@ -116,7 +172,7 @@ Add the configuration with your IP:
"bind": "lan",
"port": 18789,
"controlUi": {
"allowedOrigins": ["http://localhost:18789", "http://127.0.0.1:18789", "http://192.168.31.39:18789"]
"allowedOrigins": ["http://localhost:18789", "http://127.0.0.1:18789", "http://192.168.1.4:18789"]
}
}
}
@@ -285,6 +341,63 @@ openclaw doctor --generate-gateway-token
openclaw gateway status
```
## Installing Homebrew (Optional)
If you need to install additional tools via Homebrew, follow these steps:
### Prerequisites
The `openclaw` user needs sudo access and a password set. By default, the user is created without a password.
### Step 1: Set Password for openclaw User
As root, set a password for the openclaw user:
```bash
passwd openclaw
# Enter and confirm the new password
```
### Step 2: Install Homebrew
Switch to the openclaw user and run the installer:
```bash
su - openclaw
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
```
When prompted:
1. Enter the openclaw password for sudo access
2. Press ENTER to continue when the installer shows the directories it will create
### Step 3: Add Homebrew to PATH
After installation, add Homebrew to your shell:
```bash
echo >> /home/openclaw/.bashrc
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"' >> /home/openclaw/.bashrc
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"
```
### Step 4: Verify Installation
```bash
brew --version
```
### Installing Packages
Once Homebrew is installed, you can install packages:
```bash
brew install <package-name>
```
**Note:** Homebrew packages are installed to `/home/linuxbrew/.linuxbrew/` and are available to all users on the system.
## Additional Resources
- [OpenClaw Gateway Documentation](https://docs.openclaw.ai/gateway)
@@ -22,9 +22,22 @@ $STD apt-get install -y \
python3-pip \
python3-venv \
git \
procps
procps \
debian-keyring \
debian-archive-keyring \
ffmpeg \
sudo
msg_ok "Installed Dependencies"
# Install Caddy for HTTPS reverse proxy
msg_info "Installing Caddy (HTTPS Reverse Proxy)"
$STD apt-get install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' >/etc/apt/sources.list.d/caddy-stable.list
$STD apt-get update
$STD apt-get install -y caddy
msg_ok "Installed Caddy"
# Setup Node.js 22 (required by OpenClaw)
NODE_VERSION="22" setup_nodejs
@@ -34,60 +47,25 @@ $STD curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
msg_ok "Installed uv"
# Install Homebrew (Linuxbrew) - use alternative installation for root
msg_info "Installing Homebrew (Linuxbrew)"
# Homebrew refuses to install as root, so we use the alternative untar method
# This installs to /home/linuxbrew/.linuxbrew which is the recommended location
if [[ $EUID -eq 0 ]]; then
# Create the linuxbrew directory with proper permissions
mkdir -p /home/linuxbrew/.linuxbrew
chmod 775 /home/linuxbrew
chmod 775 /home/linuxbrew/.linuxbrew
# Download and extract Homebrew (alternative installation method)
cd /tmp
$STD curl -fsSL https://github.com/Homebrew/brew/tarball/master | tar xz --strip 1 -C /home/linuxbrew/.linuxbrew
# Set permissions so brew can be run by anyone
chown -R root:root /home/linuxbrew/.linuxbrew
chmod -R g+w /home/linuxbrew/.linuxbrew
# Create symlinks for easy access
ln -sf /home/linuxbrew/.linuxbrew/bin/brew /usr/local/bin/brew 2>/dev/null || true
# Add to bashrc for persistence
if ! grep -q 'linuxbrew/.linuxbrew/bin/brew' /root/.bashrc 2>/dev/null; then
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' >>/root/.bashrc
fi
# Add to system profile for all users
if ! grep -q 'linuxbrew/.linuxbrew' /etc/profile 2>/dev/null; then
echo 'export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH"' >>/etc/profile
fi
# Make brew available in current session
export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH"
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv 2>/dev/null)" || true
else
# Not running as root, install normally
$STD /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Create unprivileged user for OpenClaw
msg_info "Creating OpenClaw User"
if ! id -u openclaw &>/dev/null; then
useradd -r -s /bin/bash -d /home/openclaw -m openclaw
# Add openclaw to sudoers for specific operations if needed
usermod -aG sudo openclaw 2>/dev/null || true
fi
msg_ok "Installed Homebrew"
msg_ok "Created OpenClaw User"
# Install common brew dependencies for OpenClaw skills
msg_info "Installing Homebrew Dependencies"
if command -v brew &>/dev/null || [[ -x /home/linuxbrew/.linuxbrew/bin/brew ]]; then
# Use the full path to brew if not in PATH
BREW_CMD="${BREW_CMD:-/home/linuxbrew/.linuxbrew/bin/brew}"
if [[ ! -x "$BREW_CMD" ]]; then
BREW_CMD="brew"
fi
# Install ffmpeg for video-frames skill
$STD $BREW_CMD install ffmpeg 2>/dev/null || true
# Note: Other brew packages (camsnap, obsidian, summarize, songsee) require specific taps
# These can be installed manually by the user if needed
fi
msg_ok "Installed Homebrew Dependencies"
# Configure npm to use user-writable directory for global packages
msg_info "Configuring npm for User Packages"
mkdir -p /home/openclaw/.npm-global
chown -R openclaw:openclaw /home/openclaw/.npm-global
# Set npm prefix for openclaw user to use home directory
run_user_cmd="su - openclaw -c"
$run_user_cmd "npm config set prefix /home/openclaw/.npm-global"
# Add to PATH for openclaw user
echo 'export PATH=/home/openclaw/.npm-global/bin:$PATH' >> /home/openclaw/.bashrc
msg_ok "Configured npm for User Packages"
msg_info "Installing OpenClaw"
$STD npm install -g openclaw@latest
@@ -95,13 +73,17 @@ msg_ok "Installed OpenClaw"
msg_info "Creating Directories"
mkdir -p /opt/openclaw
mkdir -p /root/.openclaw
mkdir -p /home/openclaw/.openclaw
chown -R openclaw:openclaw /opt/openclaw
chown -R openclaw:openclaw /home/openclaw/.openclaw
msg_ok "Created Directories"
msg_info "Creating OpenClaw Configuration"
# Get the container's IP address for allowedOrigins
CONTAINER_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
cat <<EOF >/root/.openclaw/openclaw.json
# Get hostname for HTTPS origins
HOSTNAME_FQDN=$(hostname -f 2>/dev/null || hostname)
cat <<EOF >/home/openclaw/.openclaw/openclaw.json
{
"gateway": {
"bind": "lan",
@@ -110,15 +92,69 @@ cat <<EOF >/root/.openclaw/openclaw.json
"allowedOrigins": [
"http://localhost:18789",
"http://127.0.0.1:18789",
"http://${CONTAINER_IP}:18789"
"https://localhost:18790",
"https://127.0.0.1:18790",
"https://${CONTAINER_IP}:18790",
"https://${HOSTNAME_FQDN}:18790"
]
}
}
}
EOF
chown openclaw:openclaw /home/openclaw/.openclaw/openclaw.json
msg_ok "Created OpenClaw Configuration"
msg_info "Creating Service"
msg_info "Creating Self-Signed Certificate for HTTPS"
# Generate self-signed certificate with IP in SAN (Caddy's tls internal doesn't work with IPs)
mkdir -p /etc/caddy/certs
CONTAINER_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
HOSTNAME_FQDN=$(hostname -f 2>/dev/null || hostname)
# Generate self-signed certificate valid for 365 days
openssl req -x509 -newkey rsa:4096 -keyout /etc/caddy/certs/openclaw.key \
-out /etc/caddy/certs/openclaw.crt \
-days 365 -nodes \
-subj "/CN=openclaw-local" \
-addext "subjectAltName=IP:127.0.0.1,IP:${CONTAINER_IP},DNS:localhost,DNS:${HOSTNAME_FQDN}" 2>/dev/null
# Set proper permissions
chmod 644 /etc/caddy/certs/openclaw.crt
chmod 600 /etc/caddy/certs/openclaw.key
chown caddy:caddy /etc/caddy/certs/openclaw.crt /etc/caddy/certs/openclaw.key
msg_ok "Created Self-Signed Certificate"
msg_info "Creating Caddy HTTPS Reverse Proxy"
# Create Caddyfile for HTTPS reverse proxy with explicit certificate paths
cat <<EOF >/etc/caddy/Caddyfile
# OpenClaw HTTPS Reverse Proxy
# Access via https://<container-ip>:18790
:18790 {
tls /etc/caddy/certs/openclaw.crt /etc/caddy/certs/openclaw.key
# Caddy automatically handles WebSocket upgrades
reverse_proxy localhost:18789 {
header_up Host {host}
header_up X-Real-IP {remote_host}
}
# Log requests
log {
output file /var/log/caddy/openclaw.log
format console
}
}
EOF
# Create log directory
mkdir -p /var/log/caddy
chown caddy:caddy /var/log/caddy
# Enable Caddy
systemctl enable -q caddy
msg_ok "Created Caddy HTTPS Reverse Proxy"
msg_info "Creating OpenClaw Service"
cat <<EOF >/etc/systemd/system/openclaw.service
[Unit]
Description=OpenClaw Gateway - Personal AI Assistant
@@ -127,9 +163,11 @@ Wants=network-online.target
[Service]
Type=simple
User=root
User=openclaw
Group=openclaw
WorkingDirectory=/opt/openclaw
Environment=NODE_ENV=production
Environment=PATH=/home/openclaw/.npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ExecStart=/usr/bin/openclaw gateway --port 18789 --bind lan
Restart=on-failure
RestartSec=10
@@ -141,7 +179,12 @@ SyslogIdentifier=openclaw
WantedBy=multi-user.target
EOF
systemctl enable -q --now openclaw
msg_ok "Created Service"
msg_ok "Created OpenClaw Service"
msg_info "Starting Caddy HTTPS Proxy"
# Restart Caddy to apply configuration
systemctl restart caddy
msg_ok "Started Caddy HTTPS Proxy"
motd_ssh
customize
@@ -0,0 +1,292 @@
# OpenClaw Configuration Guide
## "Device Identity Required" Error Fix
If you see this error when accessing the Control UI from another machine:
```
control ui requires device identity (use HTTPS or localhost secure context)
```
**This is a browser security requirement.** The Web Crypto API (used for device identity) only works in secure contexts - either HTTPS or localhost.
### Solution 1: SSH Tunnel (Recommended for LAN)
Access OpenClaw via localhost through an SSH tunnel:
```bash
# On your local machine, create an SSH tunnel
ssh -L 18789:localhost:18789 root@<container-ip>
# Then access in your browser
http://localhost:18789
```
### Solution 2: HTTPS Reverse Proxy (Production)
Set up Caddy or Nginx with SSL certificates for secure HTTPS access.
### Solution 3: Tailscale VPN
Install Tailscale on both machines and access via Tailscale IP.
## "Origin Not Allowed" Error Fix
If you see this error when accessing the Control UI from another machine:
```
origin not allowed (open the Control UI from the gateway host or allow it in gateway.controlUi.allowedOrigins)
```
**Quick Fix:**
```bash
# Get your container IP
CONTAINER_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
# Create configuration with allowed origins
mkdir -p ~/.openclaw
cat > ~/.openclaw/openclaw.json << EOF
{
"gateway": {
"bind": "lan",
"port": 18789,
"controlUi": {
"allowedOrigins": [
"http://localhost:18789",
"http://127.0.0.1:18789",
"http://${CONTAINER_IP}:18789"
]
}
}
}
EOF
# Update service to use lan binding
sed -i 's|--port 18789|--port 18789 --bind lan|' /etc/systemd/system/openclaw.service
# Apply and restart
systemctl daemon-reload
systemctl restart openclaw
```
**Note:** Even with `allowedOrigins` configured, you still need a secure context (HTTPS or localhost) for the device identity requirement. Use SSH tunneling for LAN access.
## Network Binding Configuration
By default, OpenClaw binds to localhost (127.0.0.1) only. To make it accessible from other machines on your network, you need to:
1. **Bind to all interfaces** (`--bind lan`)
2. **Configure allowed origins** (required for non-loopback bindings)
### Bind Options
| Option | Description | Use Case |
| ---------- | --------------------------------- | -------------------- |
| `loopback` | Binds to 127.0.0.1 only | Default, most secure |
| `lan` | Binds to 0.0.0.0 (all interfaces) | Local network access |
| `tailnet` | Binds to Tailscale interface | VPN access only |
| `auto` | Automatic selection | Let OpenClaw decide |
### Method 1: Configure Command (Recommended for Existing Installations)
```bash
# Open the gateway configuration
openclaw configure --section gateway
# Set bind to "lan" when prompted
# Then restart the service
systemctl restart openclaw
```
### Method 2: Manual Configuration
Edit both the configuration file and systemd service:
```bash
# Create/edit the configuration file
nano ~/.openclaw/openclaw.json
```
Add the configuration with your IP:
```json
{
"gateway": {
"bind": "lan",
"port": 18789,
"controlUi": {
"allowedOrigins": ["http://localhost:18789", "http://127.0.0.1:18789", "http://192.168.31.39:18789"]
}
}
}
```
Then edit the systemd service:
```bash
nano /etc/systemd/system/openclaw.service
```
Modify the `ExecStart` line to include `--bind lan`:
```ini
[Service]
ExecStart=/usr/bin/openclaw gateway --port 18789 --bind lan
```
Apply changes:
```bash
systemctl daemon-reload
systemctl restart openclaw
```
### Method 3: Quick Fix for Existing Installations
For existing installations, run these commands:
```bash
# Get your container IP
CONTAINER_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
# Create configuration with allowed origins
mkdir -p ~/.openclaw
cat > ~/.openclaw/openclaw.json << EOF
{
"gateway": {
"bind": "lan",
"port": 18789,
"controlUi": {
"allowedOrigins": [
"http://localhost:18789",
"http://127.0.0.1:18789",
"http://${CONTAINER_IP}:18789"
]
}
}
}
EOF
# Update service to use lan binding
sed -i 's|--port 18789|--port 18789 --bind lan|' /etc/systemd/system/openclaw.service
# Apply and restart
systemctl daemon-reload
systemctl restart openclaw
```
### Method 4: Environment Variable
You can also set the bind mode via environment variable:
```bash
# Add to /etc/environment or the systemd service file
OPENCLAW_BIND=lan
```
## Verification
Check the current binding:
```bash
openclaw gateway status
```
Output should show:
```
bind: lan
listener: 0.0.0.0:18789
```
## Security Considerations
When binding to all interfaces (`lan`):
1. **Token Authentication**: OpenClaw uses token-based authentication. The token is generated on first run and displayed in the startup output.
2. **Firewall**: Consider adding firewall rules to restrict access:
```bash
# Allow only specific IP ranges
ufw allow from 192.168.31.0/24 to any port 18789
```
3. **Reverse Proxy**: For production use, consider placing OpenClaw behind a reverse proxy (nginx, Caddy) with SSL:
```nginx
server {
listen 443 ssl;
server_name openclaw.yourdomain.com;
location / {
proxy_pass http://127.0.0.1:18789;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
```
4. **Never expose directly to the internet**: The gateway provides administrative access to your system. Always use VPN, reverse proxy with authentication, or Tailscale for remote access.
## Remote Access Options
### SSH Tunnel (Most Secure for Temporary Access)
```bash
# On your local machine
ssh -N -L 18789:127.0.0.1:18789 root@<server-ip>
# Then access in browser
http://localhost:18789
```
### Tailscale (Recommended for Permanent Remote Access)
1. Install Tailscale on both machines
2. Set OpenClaw bind to `tailnet`:
```bash
openclaw configure --section gateway
# Set bind to "tailnet"
systemctl restart openclaw
```
3. Access via Tailscale IP: `http://<tailscale-ip>:18789`
## Troubleshooting
### Gateway Not Starting
```bash
# Check logs
journalctl -u openclaw -f
# Common issues:
# - Port already in use: lsof -i :18789
# - Permission denied: ensure running as root or correct user
```
### Cannot Connect from Remote
1. Verify binding: `openclaw gateway status` should show `0.0.0.0:18789`
2. Check firewall: `ufw status` or `iptables -L -n`
3. Verify network connectivity: `ping <server-ip>`
### Token Authentication Issues
```bash
# Regenerate token
openclaw doctor --generate-gateway-token
# View current token
openclaw gateway status
```
## Additional Resources
- [OpenClaw Gateway Documentation](https://docs.openclaw.ai/gateway)
- [OpenClaw Security Guide](https://docs.openclaw.ai/gateway/security)
- [OpenClaw Troubleshooting](https://docs.openclaw.ai/gateway/troubleshooting)
@@ -65,5 +65,6 @@ msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
echo -e "${TAB}${GATEWAY}${BGN}Website: http://${IP}:5099${CL}"
echo -e "${TAB}${GATEWAY}${BGN}Dashboard: http://${IP}:5099/yao/auth/entry${CL}"
echo -e "${TAB}${GATEWAY}${BGN}API: http://${IP}:5099/api${CL}"
echo -e "${TAB}${GATEWAY}${BGN}Dashboard: http://${IP}:5099/dashboard/auth/entry${CL}"
echo -e "${TAB}${GATEWAY}${BGN}OpenAPI: http://${IP}:5099/v1${CL}"
echo -e "${TAB}${GATEWAY}${BGN}Custom API: http://${IP}:5099/v1/api${CL}"
@@ -26,8 +26,8 @@
}
],
"default_credentials": {
"username": "admin@localhost",
"password": "admin123"
"username": "[email protected]",
"password": "Yao123++"
},
"notes": [
{
@@ -19,12 +19,76 @@ $STD apt-get install -y \
ca-certificates \
build-essential \
python3 \
git
python3-pip \
python3-venv \
git \
procps
msg_ok "Installed Dependencies"
# Setup Node.js 22 (required by OpenClaw)
NODE_VERSION="22" setup_nodejs
# Install uv (Python package manager)
msg_info "Installing uv (Python Package Manager)"
$STD curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
msg_ok "Installed uv"
# Install Homebrew (Linuxbrew) - use alternative installation for root
msg_info "Installing Homebrew (Linuxbrew)"
# Homebrew refuses to install as root, so we use the alternative untar method
# This installs to /home/linuxbrew/.linuxbrew which is the recommended location
if [[ $EUID -eq 0 ]]; then
# Create the linuxbrew directory with proper permissions
mkdir -p /home/linuxbrew/.linuxbrew
chmod 775 /home/linuxbrew
chmod 775 /home/linuxbrew/.linuxbrew
# Download and extract Homebrew (alternative installation method)
cd /tmp
$STD curl -fsSL https://github.com/Homebrew/brew/tarball/master | tar xz --strip 1 -C /home/linuxbrew/.linuxbrew
# Set permissions so brew can be run by anyone
chown -R root:root /home/linuxbrew/.linuxbrew
chmod -R g+w /home/linuxbrew/.linuxbrew
# Create symlinks for easy access
ln -sf /home/linuxbrew/.linuxbrew/bin/brew /usr/local/bin/brew 2>/dev/null || true
# Add to bashrc for persistence
if ! grep -q 'linuxbrew/.linuxbrew/bin/brew' /root/.bashrc 2>/dev/null; then
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' >>/root/.bashrc
fi
# Add to system profile for all users
if ! grep -q 'linuxbrew/.linuxbrew' /etc/profile 2>/dev/null; then
echo 'export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH"' >>/etc/profile
fi
# Make brew available in current session
export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH"
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv 2>/dev/null)" || true
else
# Not running as root, install normally
$STD /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
fi
msg_ok "Installed Homebrew"
# Install common brew dependencies for OpenClaw skills
msg_info "Installing Homebrew Dependencies"
if command -v brew &>/dev/null || [[ -x /home/linuxbrew/.linuxbrew/bin/brew ]]; then
# Use the full path to brew if not in PATH
BREW_CMD="${BREW_CMD:-/home/linuxbrew/.linuxbrew/bin/brew}"
if [[ ! -x "$BREW_CMD" ]]; then
BREW_CMD="brew"
fi
# Install ffmpeg for video-frames skill
$STD $BREW_CMD install ffmpeg 2>/dev/null || true
# Note: Other brew packages (camsnap, obsidian, summarize, songsee) require specific taps
# These can be installed manually by the user if needed
fi
msg_ok "Installed Homebrew Dependencies"
msg_info "Installing OpenClaw"
$STD npm install -g openclaw@latest
msg_ok "Installed OpenClaw"
@@ -34,6 +98,26 @@ mkdir -p /opt/openclaw
mkdir -p /root/.openclaw
msg_ok "Created Directories"
msg_info "Creating OpenClaw Configuration"
# Get the container's IP address for allowedOrigins
CONTAINER_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -1)
cat <<EOF >/root/.openclaw/openclaw.json
{
"gateway": {
"bind": "lan",
"port": 18789,
"controlUi": {
"allowedOrigins": [
"http://localhost:18789",
"http://127.0.0.1:18789",
"http://${CONTAINER_IP}:18789"
]
}
}
}
EOF
msg_ok "Created OpenClaw Configuration"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/openclaw.service
[Unit]
@@ -46,7 +130,7 @@ Type=simple
User=root
WorkingDirectory=/opt/openclaw
Environment=NODE_ENV=production
ExecStart=/usr/bin/openclaw gateway --port 18789
ExecStart=/usr/bin/openclaw gateway --port 18789 --bind lan
Restart=on-failure
RestartSec=10
StandardOutput=journal
@@ -22,41 +22,67 @@ msg_ok "Installed Dependencies"
fetch_and_deploy_gh_release "yao" "YaoApp/yao" "singlefile" "latest" "/usr/local/bin" "yao-*-linux-*"
msg_info "Creating Application Directory"
mkdir -p /opt/yao/data
mkdir -p /opt/yao/etc
mkdir -p /opt/yao/connectors
mkdir -p /opt/yao/scripts
mkdir -p /opt/yao/suis
mkdir -p /opt/yao/agent
mkdir -p /opt/yao/openapi
mkdir -p /opt/yao/services
mkdir -p /opt/yao/public
mkdir -p /opt/yao/icons
mkdir -p /opt/yao/public/.well-known
mkdir -p /opt/yao/logins
mkdir -p /opt/yao/models/admin
mkdir -p /opt/yao/flows/app
rm -rf /opt/yao
mkdir -p /opt/yao
msg_ok "Created Application Directory"
msg_info "Initializing Yao Application"
cd /opt/yao
$STD yao init
msg_ok "Initialized Yao Application"
# Create additional directories after init
mkdir -p /opt/yao/{db,logins,models,flows,scripts,public,logs,icons}
# Generate secure secrets
YAO_CLIENT_ID=$(cat /proc/sys/kernel/random/uuid | tr -d '-')
YAO_JWT_SECRET=$(openssl rand -base64 32 | tr -d '/+=' | head -c 32)
YAO_AES_KEY=$(openssl rand -base64 24 | tr -d '/+=' | head -c 24)
msg_info "Creating Environment File"
cat <<EOF >/opt/yao/.env
# ============================================
# Yao Application Configuration
# ============================================
# Database Configuration
YAO_DB_DRIVER="sqlite3"
YAO_DB_PRIMARY="./db/yao.db"
# Security
YAO_JWT_SECRET="${YAO_JWT_SECRET}"
YAO_DB_AESKEY="${YAO_AES_KEY}"
# OAuth/OpenAPI
YAO_CLIENT_ID="${YAO_CLIENT_ID}"
# Login Redirects
AFTER_LOGIN_SUCCESS_URL="/mission-control"
AFTER_LOGIN_FAILURE_URL="/dashboard/auth/entry"
# Server Configuration
YAO_ENV="production"
YAO_HOST="0.0.0.0"
YAO_PORT=5099
YAO_STUDIO_PORT=5077
YAO_LOG="./logs/application.log"
YAO_LOG_MODE="TEXT"
YAO_SESSION_FILE="./db/.session"
YAO_SESSION_STORE="file"
EOF
msg_ok "Created Environment File"
msg_info "Creating Application Configuration"
cat <<EOF >/opt/yao/app.yao
cat <<'EOF' >/opt/yao/app.yao
{
"xgen": "1.0",
"name": "Yao Application",
"short": "Yao",
"description": "Yao Autonomous Agent Engine",
"description": "Autonomous Agent Engine",
"version": "1.0.0",
"adminRoot": "admin",
"adminRoot": "dashboard",
"setup": "scripts.setup.Init",
"menu": {
"process": "flows.app.menu",
"args": ["yao"]
"process": "scripts.menu.Get",
"args": []
},
"optional": {
"hideNotification": true,
@@ -64,129 +90,39 @@ cat <<EOF >/opt/yao/app.yao
}
}
EOF
msg_info "Creating Database Directory"
mkdir -p /opt/yao/db
msg_info "Creating Empty SQLite Database"
sqlite3 /opt/yao/db/yao.db "VACUUM;" 2>/dev/null || touch /opt/yao/db/yao.db
msg_ok "Created Application Configuration"
msg_info "Creating Agent Configuration"
cat <<EOF >/opt/yao/agent/agent.yml
# Yao Agent Configuration
# This is a minimal configuration file for the agent system
version: "1.0"
agents: []
EOF
msg_ok "Created Agent Configuration"
msg_info "Creating OpenAPI Configuration"
cat <<EOF >/opt/yao/openapi/openapi.yao
// OpenAPI Configuration
// This is a minimal configuration file for OpenAPI support
msg_info "Creating Admin Login Widget"
cat <<'EOF' >/opt/yao/logins/admin.login.json
{
"openapi": "3.0.0",
"info": {
"title": "Yao API",
"version": "1.0.0"
},
"paths": {}
}
EOF
msg_ok "Created OpenAPI Configuration"
msg_info "Downloading Application Icons"
curl -fsSL "https://raw.githubusercontent.com/YaoApp/yao-dev-app/main/icons/app.ico" -o /opt/yao/icons/app.ico
curl -fsSL "https://raw.githubusercontent.com/YaoApp/yao-dev-app/main/icons/app.png" -o /opt/yao/icons/app.png
msg_ok "Downloaded Application Icons"
msg_info "Creating Well-Known Configuration"
cat <<EOF >/opt/yao/public/.well-known/yao
{
"name": "yao-app",
"version": "1.0.0",
"description": "Yao Autonomous Agent Engine"
}
EOF
msg_ok "Created Well-Known Configuration"
msg_info "Creating Login Configuration"
cat <<EOF >/opt/yao/logins/admin.login.yao
{
"name": "Admin Login",
"name": "::Admin Login",
"action": {
"process": "yao.login.Admin",
"args": [":payload"]
},
"layout": {
"entry": "/x/Chart/dashboard",
"entry": "/mission-control",
"captcha": "yao.utils.Captcha",
"cover": "/assets/images/login/cover.svg",
"slogan": "Yao Autonomous Agent Engine",
"slogan": "::Build Autonomous Agents with Yao",
"site": "https://yaoapps.com"
}
}
EOF
cat <<EOF >/opt/yao/logins/user.login.yao
{
"name": "User Login",
"action": {
"process": "scripts.user.Login",
"args": [":payload"]
},
"layout": {
"entry": "/x/Table/pet",
"captcha": "yao.utils.Captcha",
"cover": "/assets/images/login/cover.svg",
"slogan": "Yao Autonomous Agent Engine",
"site": "https://yaoapps.com/doc"
}
}
EOF
msg_ok "Created Login Configuration"
msg_ok "Created Admin Login Widget"
msg_info "Creating Admin User Model"
cat <<EOF >/opt/yao/models/admin/user.mod.yao
cat <<'EOF' >/opt/yao/models/admin.mod.json
{
"name": "AdminUser",
"table": { "name": "admin_user", "comment": "The administrator table" },
"name": "Admin User",
"table": {
"name": "admin_user",
"comment": "Admin User"
},
"columns": [
{ "label": "ID", "name": "id", "type": "ID" },
{
"label": "Type",
"name": "type",
"type": "enum",
"option": ["admin", "staff", "user", "robot"],
"comment": "AccountTypes: admin, staff, user, robot",
"default": "user",
"index": true
},
{
"label": "Email",
"name": "email",
"type": "string",
"length": 50,
"comment": "Email",
"index": true,
"nullable": true
},
{
"label": "Mobile",
"name": "mobile",
"type": "string",
"length": 50,
"comment": "Mobile",
"index": true,
"nullable": true
},
{
"label": "Login Password",
"name": "password",
"type": "string",
"length": 256,
"comment": "Login Password",
"crypt": "PASSWORD",
"index": true,
"nullable": true
"label": "ID",
"name": "id",
"type": "ID"
},
{
"label": "Name",
@@ -197,87 +133,212 @@ cat <<EOF >/opt/yao/models/admin/user.mod.yao
"index": true,
"nullable": true
},
{
"label": "Email",
"name": "email",
"type": "string",
"length": 128,
"comment": "Email",
"unique": true,
"index": true,
"nullable": true,
"validations": [
{
"method": "email",
"args": [],
"message": "{{input}} should be email"
}
]
},
{
"label": "Password",
"name": "password",
"type": "string",
"length": 256,
"comment": "Password",
"crypt": "PASSWORD",
"index": true,
"nullable": true,
"validations": [
{
"method": "typeof",
"args": ["string"],
"message": "{{input}} Error"
},
{
"method": "minLength",
"args": [6],
"message": "{{label}} must be at least 6 characters"
}
]
},
{
"label": "Type",
"name": "type",
"type": "string",
"length": 20,
"comment": "User Type",
"default": "user",
"index": true
},
{
"label": "Status",
"comment": "Status",
"name": "status",
"type": "enum",
"default": "enabled",
"option": ["enabled", "disabled"],
"index": true
},
{
"label": "Extra",
"name": "extra",
"type": "json",
"comment": "Extra",
"nullable": true
}
],
"relations": {},
"values": [
{
"name": "Admin",
"type": "admin",
"email": "admin@localhost",
"password": "admin123",
"status": "enabled"
}
],
"indexes": [
{
"comment": "Email Unique Index",
"name": "type_email_unique",
"columns": ["type", "email"],
"type": "unique"
}
],
"option": { "timestamps": true, "soft_deletes": true }
"values": [],
"indexes": [],
"option": {
"timestamps": true,
"soft_deletes": true
}
}
EOF
msg_ok "Created Admin User Model"
msg_info "Creating User Login Script"
cat <<EOF >/opt/yao/scripts/user.js
/**
* User Login
* @param {*} payload
*/
function Login(payload) {
log.Trace("[user] Login %s", payload.email);
return Process("yao.login.Admin", payload);
}
EOF
msg_ok "Created User Login Script"
msg_info "Creating Menu Flow"
cat <<EOF >/opt/yao/flows/app/menu.flow.yao
cat <<'EOF' >/opt/yao/flows/menu.flow.yao
{
"label": "Application Menu",
"version": "1.0.0",
"description": "Application Menu Flow",
"nodes": [
{
"id": "menu",
"type": "menu",
"config": {
"items": [
{
"id": "dashboard",
"title": "Dashboard",
"icon": "dashboard",
"path": "/x/Chart/dashboard"
},
{
"id": "admin",
"title": "Admin",
"icon": "admin",
"path": "/x/Table/admin_user"
}
]
"name": "Menu",
"nodes": [],
"output": {
"items": [
{
"name": "Dashboard",
"path": "/mission-control",
"icon": { "name": "material-dashboard", "size": 22 }
},
{
"name": "Agents",
"path": "/agents",
"icon": { "name": "material-smart_toy", "size": 22 }
},
{
"name": "Data",
"path": "/data",
"icon": { "name": "material-storage", "size": 22 }
},
{
"name": "Settings",
"path": "/settings",
"icon": { "name": "material-settings", "size": 22 }
}
}
],
"output": "menu"
],
"setting": [
{
"icon": { "name": "material-person", "size": 22 },
"name": "Account",
"path": "/account"
}
]
}
}
EOF
msg_ok "Created Menu Flow"
msg_info "Creating Setup Script"
cat <<'EOF' >/opt/yao/scripts/setup.ts
import { Process, Model } from "@yao/runtime";
/**
* Initialize the application
* Creates the default admin user if not exists
*/
export function Init() {
// Create default admin user
const adminData = {
name: "Admin",
email: "[email protected]",
password: "Yao123++",
type: "admin",
status: "enabled",
};
try {
// Check if admin user already exists
const existingAdmin = Process("models.admin.user.Find", 1, {
select: ["id"],
});
if (!existingAdmin || !existingAdmin.id) {
// Create admin user
Process("models.admin.user.Save", adminData);
console.log("Default admin user created successfully");
} else {
console.log("Admin user already exists");
}
} catch (error) {
// If the model doesn't exist yet, try to create using direct insert
console.log("Creating admin user...");
try {
Process("models.admin.user.Save", adminData);
console.log("Default admin user created successfully");
} catch (e) {
console.log("Note: Admin user will be created on first login");
}
}
}
EOF
msg_ok "Created Setup Script"
msg_info "Creating Menu Script"
cat <<'EOF' >/opt/yao/scripts/menu.ts
import { Process } from "@yao/runtime";
/**
* Get the menu for the admin panel
* @returns Menu structure
*/
export function Get() {
return {
items: [
{
name: "Dashboard",
path: "/mission-control",
icon: { name: "material-dashboard", size: 22 },
},
{
name: "Agents",
path: "/agents",
icon: { name: "material-smart_toy", size: 22 },
},
{
name: "Data",
path: "/data",
icon: { name: "material-storage", size: 22 },
},
{
name: "Settings",
path: "/settings",
icon: { name: "material-settings", size: 22 },
},
],
setting: [
{
icon: { name: "material-person", size: 22 },
name: "Account",
path: "/account",
},
],
};
}
EOF
msg_ok "Created Menu Script"
msg_info "Creating Public Web Interface"
cat <<EOF >/opt/yao/public/index.html
cat <<'EOF' >/opt/yao/public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
@@ -356,6 +417,22 @@ cat <<EOF >/opt/yao/public/index.html
background: #00d4ff;
color: #1a1a2e;
}
.admin-link {
margin-top: 20px;
padding: 15px 30px;
background: linear-gradient(90deg, #7c3aed, #00d4ff);
border: none;
border-radius: 10px;
color: #fff;
font-size: 1.1rem;
cursor: pointer;
text-decoration: none;
display: inline-block;
}
.admin-link:hover {
opacity: 0.9;
transform: translateY(-2px);
}
</style>
</head>
<body>
@@ -368,8 +445,9 @@ cat <<EOF >/opt/yao/public/index.html
</div>
<div class="info">
<p><strong>Port:</strong> 5099</p>
<p><strong>Studio Port:</strong> 5077</p>
<p><strong>Default Login:</strong> [email protected] / Yao123++</p>
</div>
<a href="/dashboard/auth/entry" class="admin-link">Open Dashboard</a>
<div class="links">
<a href="https://github.com/YaoApp/yao" target="_blank">GitHub</a>
<a href="https://yaoapps.com" target="_blank">Documentation</a>
@@ -380,6 +458,16 @@ cat <<EOF >/opt/yao/public/index.html
EOF
msg_ok "Created Public Web Interface"
msg_info "Running Database Migration"
cd /opt/yao
$STD yao migrate
msg_ok "Database Migration Complete"
msg_info "Creating Default Admin User"
cd /opt/yao
$STD yao run scripts.setup.Init || true
msg_ok "Created Default Admin User"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/yao.service
[Unit]
@@ -64,4 +64,6 @@ description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:5099${CL}"
echo -e "${TAB}${GATEWAY}${BGN}Website: http://${IP}:5099${CL}"
echo -e "${TAB}${GATEWAY}${BGN}Dashboard: http://${IP}:5099/yao/auth/entry${CL}"
echo -e "${TAB}${GATEWAY}${BGN}API: http://${IP}:5099/api${CL}"
@@ -26,8 +26,8 @@
}
],
"default_credentials": {
"username": null,
"password": null
"username": "admin@localhost",
"password": "admin123"
},
"notes": [
{
@@ -15,7 +15,8 @@ update_os
msg_info "Installing Dependencies"
$STD apt-get install -y \
curl \
unzip
unzip \
sqlite3
msg_ok "Installed Dependencies"
fetch_and_deploy_gh_release "yao" "YaoApp/yao" "singlefile" "latest" "/usr/local/bin" "yao-*-linux-*"
@@ -23,6 +24,18 @@ fetch_and_deploy_gh_release "yao" "YaoApp/yao" "singlefile" "latest" "/usr/local
msg_info "Creating Application Directory"
mkdir -p /opt/yao/data
mkdir -p /opt/yao/etc
mkdir -p /opt/yao/connectors
mkdir -p /opt/yao/scripts
mkdir -p /opt/yao/suis
mkdir -p /opt/yao/agent
mkdir -p /opt/yao/openapi
mkdir -p /opt/yao/services
mkdir -p /opt/yao/public
mkdir -p /opt/yao/icons
mkdir -p /opt/yao/public/.well-known
mkdir -p /opt/yao/logins
mkdir -p /opt/yao/models/admin
mkdir -p /opt/yao/flows/app
msg_ok "Created Application Directory"
msg_info "Creating Environment File"
@@ -32,31 +45,340 @@ YAO_STUDIO_PORT=5077
EOF
msg_ok "Created Environment File"
msg_info "Creating Minimal Application Configuration"
msg_info "Creating Application Configuration"
cat <<EOF >/opt/yao/app.yao
{
"name": "yao-app",
"version": "1.0.0",
"xgen": "1.0",
"name": "Yao Application",
"short": "Yao",
"description": "Yao Autonomous Agent Engine",
"studio": {
"port": 5077
"version": "1.0.0",
"adminRoot": "admin",
"menu": {
"process": "flows.app.menu",
"args": ["yao"]
},
"server": {
"port": 5099,
"host": "0.0.0.0"
},
"database": {
"driver": "sqlite3",
"file": "/opt/yao/data/yao.db"
},
"session": {
"store": "file",
"path": "/opt/yao/data/sessions"
"optional": {
"hideNotification": true,
"hideSetting": false
}
}
EOF
mkdir -p /opt/yao/data/sessions
msg_ok "Created Minimal Application Configuration"
msg_info "Creating Database Directory"
mkdir -p /opt/yao/db
msg_info "Creating Empty SQLite Database"
sqlite3 /opt/yao/db/yao.db "VACUUM;" 2>/dev/null || touch /opt/yao/db/yao.db
msg_ok "Created Application Configuration"
msg_info "Creating Agent Configuration"
cat <<EOF >/opt/yao/agent/agent.yml
# Yao Agent Configuration
# This is a minimal configuration file for the agent system
version: "1.0"
agents: []
EOF
msg_ok "Created Agent Configuration"
msg_info "Creating OpenAPI Configuration"
cat <<EOF >/opt/yao/openapi/openapi.yao
// OpenAPI Configuration
// This is a minimal configuration file for OpenAPI support
{
"openapi": "3.0.0",
"info": {
"title": "Yao API",
"version": "1.0.0"
},
"paths": {}
}
EOF
msg_ok "Created OpenAPI Configuration"
msg_info "Downloading Application Icons"
curl -fsSL "https://raw.githubusercontent.com/YaoApp/yao-dev-app/main/icons/app.ico" -o /opt/yao/icons/app.ico
curl -fsSL "https://raw.githubusercontent.com/YaoApp/yao-dev-app/main/icons/app.png" -o /opt/yao/icons/app.png
msg_ok "Downloaded Application Icons"
msg_info "Creating Well-Known Configuration"
cat <<EOF >/opt/yao/public/.well-known/yao
{
"name": "yao-app",
"version": "1.0.0",
"description": "Yao Autonomous Agent Engine"
}
EOF
msg_ok "Created Well-Known Configuration"
msg_info "Creating Login Configuration"
cat <<EOF >/opt/yao/logins/admin.login.yao
{
"name": "Admin Login",
"action": {
"process": "yao.login.Admin",
"args": [":payload"]
},
"layout": {
"entry": "/x/Chart/dashboard",
"captcha": "yao.utils.Captcha",
"cover": "/assets/images/login/cover.svg",
"slogan": "Yao Autonomous Agent Engine",
"site": "https://yaoapps.com"
}
}
EOF
cat <<EOF >/opt/yao/logins/user.login.yao
{
"name": "User Login",
"action": {
"process": "scripts.user.Login",
"args": [":payload"]
},
"layout": {
"entry": "/x/Table/pet",
"captcha": "yao.utils.Captcha",
"cover": "/assets/images/login/cover.svg",
"slogan": "Yao Autonomous Agent Engine",
"site": "https://yaoapps.com/doc"
}
}
EOF
msg_ok "Created Login Configuration"
msg_info "Creating Admin User Model"
cat <<EOF >/opt/yao/models/admin/user.mod.yao
{
"name": "AdminUser",
"table": { "name": "admin_user", "comment": "The administrator table" },
"columns": [
{ "label": "ID", "name": "id", "type": "ID" },
{
"label": "Type",
"name": "type",
"type": "enum",
"option": ["admin", "staff", "user", "robot"],
"comment": "AccountTypes: admin, staff, user, robot",
"default": "user",
"index": true
},
{
"label": "Email",
"name": "email",
"type": "string",
"length": 50,
"comment": "Email",
"index": true,
"nullable": true
},
{
"label": "Mobile",
"name": "mobile",
"type": "string",
"length": 50,
"comment": "Mobile",
"index": true,
"nullable": true
},
{
"label": "Login Password",
"name": "password",
"type": "string",
"length": 256,
"comment": "Login Password",
"crypt": "PASSWORD",
"index": true,
"nullable": true
},
{
"label": "Name",
"name": "name",
"type": "string",
"length": 80,
"comment": "Name",
"index": true,
"nullable": true
},
{
"label": "Status",
"comment": "Status",
"name": "status",
"type": "enum",
"default": "enabled",
"option": ["enabled", "disabled"],
"index": true
}
],
"relations": {},
"values": [
{
"name": "Admin",
"type": "admin",
"email": "admin@localhost",
"password": "admin123",
"status": "enabled"
}
],
"indexes": [
{
"comment": "Email Unique Index",
"name": "type_email_unique",
"columns": ["type", "email"],
"type": "unique"
}
],
"option": { "timestamps": true, "soft_deletes": true }
}
EOF
msg_ok "Created Admin User Model"
msg_info "Creating User Login Script"
cat <<EOF >/opt/yao/scripts/user.js
/**
* User Login
* @param {*} payload
*/
function Login(payload) {
log.Trace("[user] Login %s", payload.email);
return Process("yao.login.Admin", payload);
}
EOF
msg_ok "Created User Login Script"
msg_info "Creating Menu Flow"
cat <<EOF >/opt/yao/flows/app/menu.flow.yao
{
"label": "Application Menu",
"version": "1.0.0",
"description": "Application Menu Flow",
"nodes": [
{
"id": "menu",
"type": "menu",
"config": {
"items": [
{
"id": "dashboard",
"title": "Dashboard",
"icon": "dashboard",
"path": "/x/Chart/dashboard"
},
{
"id": "admin",
"title": "Admin",
"icon": "admin",
"path": "/x/Table/admin_user"
}
]
}
}
],
"output": "menu"
}
EOF
msg_ok "Created Menu Flow"
msg_info "Creating Public Web Interface"
cat <<EOF >/opt/yao/public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Yao - Autonomous Agent Engine</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
.container {
text-align: center;
padding: 40px;
background: rgba(255,255,255,0.05);
border-radius: 20px;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
max-width: 600px;
}
h1 {
font-size: 3rem;
margin-bottom: 10px;
background: linear-gradient(90deg, #00d4ff, #7c3aed);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
color: #94a3b8;
font-size: 1.2rem;
margin-bottom: 30px;
}
.status {
background: rgba(34, 197, 94, 0.2);
border: 1px solid #22c55e;
border-radius: 10px;
padding: 15px;
margin: 20px 0;
}
.status-dot {
display: inline-block;
width: 12px;
height: 12px;
background: #22c55e;
border-radius: 50%;
margin-right: 8px;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.info { color: #cbd5e1; line-height: 1.8; }
.links {
margin-top: 30px;
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}
.links a {
color: #00d4ff;
text-decoration: none;
padding: 10px 20px;
border: 1px solid #00d4ff;
border-radius: 8px;
transition: all 0.3s;
}
.links a:hover {
background: #00d4ff;
color: #1a1a2e;
}
</style>
</head>
<body>
<div class="container">
<h1>🤖 Yao</h1>
<p class="subtitle">Autonomous Agent Engine</p>
<div class="status">
<span class="status-dot"></span>
<span>Service Running</span>
</div>
<div class="info">
<p><strong>Port:</strong> 5099</p>
<p><strong>Studio Port:</strong> 5077</p>
</div>
<div class="links">
<a href="https://github.com/YaoApp/yao" target="_blank">GitHub</a>
<a href="https://yaoapps.com" target="_blank">Documentation</a>
</div>
</div>
</body>
</html>
EOF
msg_ok "Created Public Web Interface"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/yao.service
@@ -31,13 +31,17 @@
},
"notes": [
{
"text": "After installation, create a new project: mkdir -p /opt/yao/myapp && cd /opt/yao/myapp && yao start",
"text": "A minimal app.yao configuration is created automatically at /opt/yao/app.yao",
"type": "info"
},
{
"text": "Access the web interface at http://IP:5099",
"type": "info"
},
{
"text": "Application data is stored in /opt/yao/data/ including the SQLite database",
"type": "info"
},
{
"text": "Yao includes built-in SQLite database, V8 JavaScript engine, and TypeScript runtime - no additional dependencies required.",
"type": "info"
@@ -1,5 +1,5 @@
{
"generated": "2026-03-19T18:38:18Z",
"generated": "2026-03-20T18:29:43Z",
"versions": [
{
"slug": "agregarr",
@@ -18,9 +18,9 @@
{
"slug": "llamacpp",
"repo": "ggml-org/llama.cpp",
"version": "b8429",
"version": "b8457",
"pinned": false,
"date": "2026-03-19T14:00:45Z"
"date": "2026-03-20T18:22:21Z"
},
{
"slug": "localai",
@@ -46,9 +46,9 @@
{
"slug": "pegaprox",
"repo": "PegaProx/project-pegaprox",
"version": "v0.9.2",
"version": "v0.9.2.2",
"pinned": false,
"date": "2026-03-16T08:00:29Z"
"date": "2026-03-20T17:42:56Z"
},
{
"slug": "ragflow",
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
# Copyright (c) 2021-2026 community-scripts ORG
# Author: Slaviša Arežina (tremor021)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://wakapi.dev/ | https://github.com/muety/wakapi
APP="Alpine-Wakapi"
var_tags="${var_tags:-code;time-tracking}"
var_cpu="${var_cpu:-1}"
var_ram="${var_ram:-512}"
var_disk="${var_disk:-4}"
var_os="${var_os:-alpine}"
var_version="${var_version:-3.23}"
var_unprivileged="${var_unprivileged:-1}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /opt/wakapi ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
RELEASE=$(curl -s https://api.github.com/repos/muety/wakapi/releases/latest | grep "tag_name" | awk '{print substr($2, 2, length($2)-3) }')
if [ "${RELEASE}" != "$(cat ~/.wakapi 2>/dev/null)" ] || [ ! -f ~/.wakapi ]; then
msg_info "Stopping Wakapi Service"
$STD rc-service wakapi stop
msg_ok "Stopped Wakapi Service"
msg_info "Updating Wakapi LXC"
$STD apk -U upgrade
msg_ok "Updated Wakapi LXC"
msg_info "Creating backup"
mkdir -p /opt/wakapi-backup
cp /opt/wakapi/config.yml /opt/wakapi/wakapi_db.db /opt/wakapi-backup/
msg_ok "Created backup"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "wakapi" "muety/wakapi" "tarball"
msg_info "Configuring Wakapi"
cd /opt/wakapi
$STD go mod download
$STD go build -o wakapi
cp /opt/wakapi-backup/config.yml /opt/wakapi/
cp /opt/wakapi-backup/wakapi_db.db /opt/wakapi/
rm -rf /opt/wakapi-backup
msg_ok "Configured Wakapi"
msg_info "Starting Service"
$STD rc-service wakapi start
msg_ok "Started Service"
msg_ok "Updated successfully"
else
msg_ok "No update required. ${APP} is already at ${RELEASE}"
fi
exit 0
}
start
build_container
description
msg_ok "Completed successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:3000${CL}"
@@ -0,0 +1,6 @@
___ __ _ _ __ __ _
/ | / /___ (_)___ ___ | | / /___ _/ /______ _____ (_)
/ /| | / / __ \/ / __ \/ _ \_____| | /| / / __ `/ //_/ __ `/ __ \/ /
/ ___ |/ / /_/ / / / / / __/_____/ |/ |/ / /_/ / ,< / /_/ / /_/ / /
/_/ |_/_/ .___/_/_/ /_/\___/ |__/|__/\__,_/_/|_|\__,_/ .___/_/
/_/ /_/
@@ -0,0 +1,6 @@
_ _____ ____ __ __ _______ __
(_) ___/____ ____ ____ _________ _____/ __ )/ /___ _____/ /_/_ __/ | / /
/ /\__ \/ __ \/ __ \/ __ \/ ___/ __ \/ ___/ __ / / __ \/ ___/ //_// / | | / /
/ /___/ / /_/ / /_/ / / / (__ ) /_/ / / / /_/ / / /_/ / /__/ ,< / / | |/ /
/_//____/ .___/\____/_/ /_/____/\____/_/ /_____/_/\____/\___/_/|_|/_/ |___/
/_/
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
# Copyright (c) 2021-2026 community-scripts ORG
# Author: Matthew Stern (sternma) | MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/dmunozv04/iSponsorBlockTV
APP="iSponsorBlockTV"
var_tags="${var_tags:-media;automation}"
var_cpu="${var_cpu:-1}"
var_ram="${var_ram:-1024}"
var_disk="${var_disk:-4}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_unprivileged="${var_unprivileged:-1}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /opt/isponsorblocktv ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "isponsorblocktv" "dmunozv04/iSponsorBlockTV"; then
msg_info "Stopping Service"
systemctl stop isponsorblocktv
msg_ok "Stopped Service"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "isponsorblocktv" "dmunozv04/iSponsorBlockTV" "singlefile" "latest" "/opt/isponsorblocktv" "iSponsorBlockTV-*-linux"
msg_info "Starting Service"
systemctl start isponsorblocktv
msg_ok "Started Service"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW} Run the setup wizard inside the container with:${CL}"
echo -e "${TAB}${GATEWAY}${BGN}iSponsorBlockTV setup${CL}"
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
# Copyright (c) 2021-2026 community-scripts ORG
# Author: Slaviša Arežina (tremor021)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://goteleport.com/
APP="Teleport"
var_tags="${var_tags:-zero-trust}"
var_cpu="${var_cpu:-1}"
var_ram="${var_ram:-1024}"
var_disk="${var_disk:-4}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_unprivileged="${var_unprivileged:-1}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -f /etc/teleport.yaml ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
msg_info "Updating Teleport"
$STD apt update
$STD apt upgrade -y
msg_ok "Updated successfully!"
exit
}
start
build_container
description
msg_ok "Completed successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
echo -e "${TAB}${GATEWAY}${BGN}https://${IP}:3080${CL}"
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: Slaviša Arežina (tremor021)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://wakapi.dev/ | https://github.com/muety/wakapi
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
msg_info "Installing Dependencies"
$STD apk add --no-cache \
ca-certificates \
tzdata
$STD update-ca-certificates
$STD apk add --no-cache go --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community
msg_ok "Installed Dependencies"
fetch_and_deploy_gh_release "wakapi" "muety/wakapi" "tarball"
msg_info "Configuring Wakapi"
LOCAL_IP=$(/sbin/ip -o -4 addr list eth0 | awk '{print $4}' | cut -d/ -f1)
cd /opt/wakapi
$STD go mod download
$STD go build -o wakapi
cp config.default.yml config.yml
sed -i 's/listen_ipv6: ::1/listen_ipv6: "-"/g' config.yml
sed -i 's/listen_ipv4: 127.0.0.1/listen_ipv4: "0.0.0.0"/g' config.yml
sed -i "s/public_url: http:\/\/localhost:3000/public_url: http:\/\/$LOCAL_IP:3000/g" config.yml
msg_ok "Configured Wakapi"
msg_info "Enabling Wakapi Service"
cat <<EOF >/etc/init.d/wakapi
#!/sbin/openrc-run
description="Wakapi Service"
directory="/opt/wakapi"
command="/opt/wakapi/wakapi"
command_args="-config config.yml"
command_background="true"
command_user="root"
pidfile="/var/run/wakapi.pid"
depend() {
use net
}
EOF
chmod +x /etc/init.d/wakapi
$STD rc-update add wakapi default
msg_ok "Enabled Wakapi Service"
msg_info "Starting Wakapi"
$STD rc-service wakapi start
msg_ok "Started Wakapi"
motd_ssh
customize
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: Matthew Stern (sternma) | MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/dmunozv04/iSponsorBlockTV
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
fetch_and_deploy_gh_release "isponsorblocktv" "dmunozv04/iSponsorBlockTV" "singlefile" "latest" "/opt/isponsorblocktv" "iSponsorBlockTV-*-linux"
msg_info "Setting up iSponsorBlockTV"
install -d /var/lib/isponsorblocktv
msg_ok "Set up iSponsorBlockTV"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/isponsorblocktv.service
[Unit]
Description=iSponsorBlockTV
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
Group=root
Environment=iSPBTV_data_dir=/var/lib/isponsorblocktv
ExecStart=/opt/isponsorblocktv/isponsorblocktv
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q isponsorblocktv
msg_ok "Created Service"
msg_info "Creating CLI wrapper"
cat <<EOF >/usr/local/bin/iSponsorBlockTV
#!/usr/bin/env bash
export iSPBTV_data_dir="/var/lib/isponsorblocktv"
set +e
/opt/isponsorblocktv/isponsorblocktv "$@"
status=$?
set -e
case "${1:-}" in
setup|setup-cli)
systemctl restart isponsorblocktv >/dev/null 2>&1 || true
;;
esac
exit $status
EOF
chmod +x /usr/local/bin/iSponsorBlockTV
ln -sf /usr/local/bin/iSponsorBlockTV /usr/bin/iSponsorBlockTV
msg_ok "Created CLI wrapper"
motd_ssh
customize
cleanup_lxc
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: Slaviša Arežina (tremor021)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://goteleport.com/
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
setup_deb822_repo \
"teleport" \
"https://deb.releases.teleport.dev/teleport-pubkey.asc" \
"https://apt.releases.teleport.dev/debian" \
"trixie" \
"stable/v18"
msg_info "Configuring Teleport"
$STD apt install -y teleport
$STD teleport configure -o /etc/teleport.yaml
systemctl enable -q --now teleport
sleep 10
tctl users add teleport-admin --roles=editor,access --logins=root >~/teleportadmin.txt
sed -i "s|https://[^:]*:3080|https://${LOCAL_IP}:3080|g" ~/teleportadmin.txt
msg_ok "Configured Teleport"
motd_ssh
customize
cleanup_lxc
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL:-https://raw.githubusercontent.com/Heretek-AI/ProxmoxVE/refs/heads/main}"
source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/build.func)
# Author: Heretek-AI
# License: MIT | https://github.com/Heretek-AI/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/YaoApp/yao
APP="yao"
var_tags="${var_tags:-ai;agents;automation;low-code}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-4096}"
var_disk="${var_disk:-10}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_unprivileged="${var_unprivileged:-1}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -f /usr/local/bin/yao ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "yao" "YaoApp/yao"; then
msg_info "Stopping Service"
systemctl stop yao
msg_ok "Stopped Service"
msg_info "Backing up Data"
cp -r /opt/yao/data /opt/yao_data_backup 2>/dev/null || true
cp /opt/yao/app.yao /opt/yao_app_backup 2>/dev/null || true
cp /opt/yao/.env /opt/yao_env_backup 2>/dev/null || true
msg_ok "Backed up Data"
fetch_and_deploy_gh_release "yao" "YaoApp/yao" "singlefile" "latest" "/usr/local/bin" "yao-*-linux-*"
msg_info "Restoring Data"
cp -r /opt/yao_data_backup/. /opt/yao/data 2>/dev/null || true
cp /opt/yao_app_backup /opt/yao/app.yao 2>/dev/null || true
cp /opt/yao_env_backup /opt/yao/.env 2>/dev/null || true
rm -rf /opt/yao_data_backup /opt/yao_app_backup /opt/yao_env_backup
msg_ok "Restored Data"
msg_info "Starting Service"
systemctl start yao
msg_ok "Started Service"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:5099${CL}"
@@ -0,0 +1,46 @@
{
"name": "Yao (In Development)",
"slug": "yao",
"categories": [20],
"date_created": "2026-03-20",
"type": "ct",
"updateable": true,
"privileged": false,
"interface_port": 5099,
"documentation": "https://yaoapps.com/docs",
"website": "https://yaoapps.com",
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/yao.webp",
"config_path": "/opt/yao/.env",
"description": "Yao is an open-source engine for autonomous agents - event-driven, proactive, and self-scheduling. Build agents that work like real team members with three trigger modes: Clock (scheduled), Human (email/message), and Event (webhook/database).",
"install_methods": [
{
"type": "default",
"script": "ct/yao.sh",
"resources": {
"cpu": 2,
"ram": 4096,
"hdd": 10,
"os": "Debian",
"version": "13"
}
}
],
"default_credentials": {
"username": null,
"password": null
},
"notes": [
{
"text": "After installation, create a new project: mkdir -p /opt/yao/myapp && cd /opt/yao/myapp && yao start",
"type": "info"
},
{
"text": "Access the web interface at http://IP:5099",
"type": "info"
},
{
"text": "Yao includes built-in SQLite database, V8 JavaScript engine, and TypeScript runtime - no additional dependencies required.",
"type": "info"
}
]
}
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
# Author: Heretek-AI
# License: MIT | https://github.com/Heretek-AI/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/YaoApp/yao
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
msg_info "Installing Dependencies"
$STD apt-get install -y \
curl \
unzip
msg_ok "Installed Dependencies"
fetch_and_deploy_gh_release "yao" "YaoApp/yao" "singlefile" "latest" "/usr/local/bin" "yao-*-linux-*"
msg_info "Creating Application Directory"
mkdir -p /opt/yao/data
mkdir -p /opt/yao/etc
msg_ok "Created Application Directory"
msg_info "Creating Environment File"
cat <<EOF >/opt/yao/.env
YAO_PORT=5099
YAO_STUDIO_PORT=5077
EOF
msg_ok "Created Environment File"
msg_info "Creating Minimal Application Configuration"
cat <<EOF >/opt/yao/app.yao
{
"name": "yao-app",
"version": "1.0.0",
"description": "Yao Autonomous Agent Engine",
"studio": {
"port": 5077
},
"server": {
"port": 5099,
"host": "0.0.0.0"
},
"database": {
"driver": "sqlite3",
"file": "/opt/yao/data/yao.db"
},
"session": {
"store": "file",
"path": "/opt/yao/data/sessions"
}
}
EOF
mkdir -p /opt/yao/data/sessions
msg_ok "Created Minimal Application Configuration"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/yao.service
[Unit]
Description=Yao - Autonomous Agent Engine
After=network.target
Wants=network-online.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/yao
ExecStart=/usr/local/bin/yao start
Restart=on-failure
RestartSec=5
EnvironmentFile=/opt/yao/.env
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now yao
msg_ok "Created Service"
motd_ssh
customize
cleanup_lxc
@@ -285,7 +285,10 @@ EOF
post_progress_to_api
local tools_content
tools_content=$(curl -fsSL ${COMMUNITY_SCRIPTS_URL}/misc/tools.func) || {
# Add cache-busting query parameter to ensure we get the latest version
# GitHub's CDN caches raw content, which can cause stale scripts to be served
local cache_bust="?_=$(date +%s)"
tools_content=$(curl -fsSL "${COMMUNITY_SCRIPTS_URL}/misc/tools.func${cache_bust}") || {
msg_error "Failed to download tools.func"
exit 115
}
@@ -105,11 +105,13 @@ curl_with_retry() {
fi
fi
debug_log "curl attempt $attempt failed, waiting ${backoff}s before retry..."
debug_log "curl attempt $attempt failed (timeout=${timeout}s), waiting ${backoff}s before retry..."
sleep "$backoff"
# Exponential backoff: 1, 2, 4, 8... capped at 30s
backoff=$((backoff * 2))
((backoff > 30)) && backoff=30
# Double --max-time on each retry so slow connections can finish
timeout=$((timeout * 2))
((attempt++))
done
@@ -172,8 +174,10 @@ curl_api_with_retry() {
return 0
fi
debug_log "curl API attempt $attempt failed (HTTP $http_code), waiting ${attempt}s..."
debug_log "curl API attempt $attempt failed (HTTP $http_code, timeout=${timeout}s), waiting ${attempt}s..."
sleep "$attempt"
# Double --max-time on each retry so slow connections can finish
timeout=$((timeout * 2))
((attempt++))
done
@@ -289,9 +293,9 @@ cleanup_tool_keyrings() {
local tool_patterns=("$@")
for pattern in "${tool_patterns[@]}"; do
rm -f /usr/share/keyrings/"${pattern}"*.gpg \
/etc/apt/keyrings/"${pattern}"*.gpg \
/etc/apt/trusted.gpg.d/"${pattern}"*.gpg 2>/dev/null || true
rm -f /usr/share/keyrings/${pattern}*.gpg \
/etc/apt/keyrings/${pattern}*.gpg \
/etc/apt/trusted.gpg.d/${pattern}*.gpg 2>/dev/null || true
done
}
@@ -791,7 +795,7 @@ manage_tool_repository() {
repo_component="main"
[[ "$distro_id" == "ubuntu" ]] && repo_component="multiverse"
cat <<EOF >/etc/apt/sources.list.d/mongodb-org-"${version}".sources
cat <<EOF >/etc/apt/sources.list.d/mongodb-org-${version}.sources
Types: deb
URIs: ${repo_url}
Suites: ${suite}/mongodb-org/${version}
@@ -1371,7 +1375,7 @@ download_file() {
local curl_opts=(-fsSL)
[[ "$show_progress" == "true" ]] && curl_opts=(-fL#)
for attempt in $(seq 1 "$max_retries"); do
for attempt in $(seq 1 $max_retries); do
if curl "${curl_opts[@]}" -o "$output" "$url"; then
return 0
fi
@@ -1846,7 +1850,7 @@ setup_deb822_repo() {
[[ -n "$architectures" ]] && echo "Architectures: $architectures"
echo "Signed-By: /etc/apt/keyrings/${name}.gpg"
[[ -n "$enabled" ]] && echo "Enabled: $enabled"
} >/etc/apt/sources.list.d/"${name}".sources
} >/etc/apt/sources.list.d/${name}.sources
$STD apt update || {
msg_warn "apt update failed after adding repository: ${name}"
@@ -1975,6 +1979,47 @@ extract_version_from_json() {
fi
}
# ------------------------------------------------------------------------------
# Get latest GitHub tag (for repos that only publish tags, not releases).
#
# Usage:
# get_latest_gh_tag "owner/repo" [prefix]
#
# Arguments:
# $1 - GitHub repo (owner/repo)
# $2 - Optional prefix filter (e.g., "v" to only match tags starting with "v")
#
# Returns:
# Latest tag name (stdout), or returns 1 on failure
# ------------------------------------------------------------------------------
get_latest_gh_tag() {
local repo="$1"
local prefix="${2:-}"
local temp_file
temp_file=$(mktemp)
if ! github_api_call "https://api.github.com/repos/${repo}/tags?per_page=50" "$temp_file"; then
rm -f "$temp_file"
return 1
fi
local tag=""
if [[ -n "$prefix" ]]; then
tag=$(jq -r --arg p "$prefix" '[.[] | select(.name | startswith($p))][0].name // empty' "$temp_file")
else
tag=$(jq -r '.[0].name // empty' "$temp_file")
fi
rm -f "$temp_file"
if [[ -z "$tag" ]]; then
msg_error "No tags found for ${repo}"
return 1
fi
echo "$tag"
}
# ------------------------------------------------------------------------------
# Get latest GitHub release version with fallback to tags
# Usage: get_latest_github_release "owner/repo" [strip_v] [include_prerelease]
@@ -2073,103 +2118,131 @@ verify_gpg_fingerprint() {
}
# ------------------------------------------------------------------------------
# Get latest GitHub tag for a repository.
# Fetches and deploys a GitHub tag-based source tarball.
#
# Description:
# - Queries the GitHub API for tags (not releases)
# - Useful for repos that only create tags, not full releases
# - Supports optional prefix filter and version-only extraction
# - Returns the latest tag name (printed to stdout)
# - Downloads the source tarball for a given tag from GitHub
# - Extracts to the target directory
# - Writes the version to ~/.<app>
#
# Usage:
# MONGO_VERSION=$(get_latest_gh_tag "mongodb/mongo-tools")
# LATEST=$(get_latest_gh_tag "owner/repo" "v") # only tags starting with "v"
# LATEST=$(get_latest_gh_tag "owner/repo" "" "true") # strip leading "v"
# fetch_and_deploy_gh_tag "guacd" "apache/guacamole-server"
# fetch_and_deploy_gh_tag "guacd" "apache/guacamole-server" "latest" "/opt/guacamole-server"
#
# Arguments:
# $1 - GitHub repo (owner/repo)
# $2 - Tag prefix filter (optional, e.g. "v" or "100.")
# $3 - Strip prefix from result (optional, "true" to strip $2 prefix)
#
# Returns:
# 0 on success (tag printed to stdout), 1 on failure
# $1 - App name (used for version file ~/.<app>)
# $2 - GitHub repo (owner/repo)
# $3 - Tag version (default: "latest" → auto-detect via get_latest_gh_tag)
# $4 - Target directory (default: /opt/$app)
#
# Notes:
# - Skips tags containing "rc", "alpha", "beta", "dev", "test"
# - Sorts by version number (sort -V) to find the latest
# - Respects GITHUB_TOKEN for rate limiting
# - Supports CLEAN_INSTALL=1 to wipe target before extracting
# - For repos that only publish tags, not GitHub Releases
# ------------------------------------------------------------------------------
get_latest_gh_tag() {
local repo="$1"
local prefix="${2:-}"
local strip_prefix="${3:-false}"
fetch_and_deploy_gh_tag() {
local app="$1"
local repo="$2"
local version="${3:-latest}"
local target="${4:-/opt/$app}"
local app_lc=""
app_lc="$(echo "${app,,}" | tr -d ' ')"
local version_file="$HOME/.${app_lc}"
local header_args=()
[[ -n "${GITHUB_TOKEN:-}" ]] && header_args=(-H "Authorization: Bearer $GITHUB_TOKEN")
if [[ "$version" == "latest" ]]; then
version=$(get_latest_gh_tag "$repo") || {
msg_error "Failed to determine latest tag for ${repo}"
return 1
}
fi
local http_code=""
http_code=$(curl -sSL --max-time 20 -w "%{http_code}" -o /tmp/gh_tags.json \
-H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2022-11-28' \
"${header_args[@]}" \
"https://api.github.com/repos/${repo}/tags?per_page=100" 2>/dev/null) || true
local current_version=""
[[ -f "$version_file" ]] && current_version=$(<"$version_file")
if [[ "$http_code" == "401" ]]; then
msg_error "GitHub API authentication failed (HTTP 401)."
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
msg_error "Your GITHUB_TOKEN appears to be invalid or expired."
else
msg_error "The repository may require authentication. Try: export GITHUB_TOKEN=\"ghp_your_token\""
fi
rm -f /tmp/gh_tags.json
if [[ "$current_version" == "$version" ]]; then
msg_ok "$app is already up-to-date ($version)"
return 0
fi
local tmpdir
tmpdir=$(mktemp -d) || return 1
local tarball_url="https://github.com/${repo}/archive/refs/tags/${version}.tar.gz"
local filename="${app_lc}-${version}.tar.gz"
msg_info "Fetching GitHub tag: ${app} (${version})"
download_file "$tarball_url" "$tmpdir/$filename" || {
msg_error "Download failed: $tarball_url"
rm -rf "$tmpdir"
return 1
}
mkdir -p "$target"
if [[ "${CLEAN_INSTALL:-0}" == "1" ]]; then
rm -rf "${target:?}/"*
fi
if [[ "$http_code" == "403" ]]; then
msg_error "GitHub API rate limit exceeded (HTTP 403)."
msg_error "To increase the limit, export a GitHub token before running the script:"
msg_error " export GITHUB_TOKEN=\"ghp_your_token_here\""
rm -f /tmp/gh_tags.json
tar --no-same-owner -xzf "$tmpdir/$filename" -C "$tmpdir" || {
msg_error "Failed to extract tarball"
rm -rf "$tmpdir"
return 1
fi
}
if [[ "$http_code" == "000" || -z "$http_code" ]]; then
msg_error "GitHub API connection failed (no response)."
msg_error "Check your network/DNS: curl -sSL https://api.github.com/rate_limit"
rm -f /tmp/gh_tags.json
return 1
fi
local unpack_dir
unpack_dir=$(find "$tmpdir" -mindepth 1 -maxdepth 1 -type d | head -n1)
if [[ "$http_code" != "200" ]] || [[ ! -s /tmp/gh_tags.json ]]; then
msg_error "Unable to fetch tags for ${repo} (HTTP ${http_code})"
rm -f /tmp/gh_tags.json
return 1
fi
shopt -s dotglob nullglob
cp -r "$unpack_dir"/* "$target/"
shopt -u dotglob nullglob
local tags_json
tags_json=$(</tmp/gh_tags.json)
rm -f /tmp/gh_tags.json
# Extract tag names, filter by prefix, exclude pre-release patterns, sort by version
local latest=""
latest=$(echo "$tags_json" | grep -oP '"name":\s*"\K[^"]+' |
{ [[ -n "$prefix" ]] && grep "^${prefix}" || cat; } |
grep -viE '(rc|alpha|beta|dev|test|preview|snapshot)' |
sort -V | tail -n1)
if [[ -z "$latest" ]]; then
msg_warn "No matching tags found for ${repo}${prefix:+ (prefix: $prefix)}"
return 1
fi
if [[ "$strip_prefix" == "true" && -n "$prefix" ]]; then
latest="${latest#"$prefix"}"
fi
echo "$latest"
rm -rf "$tmpdir"
echo "$version" >"$version_file"
msg_ok "Deployed ${app} ${version} to ${target}"
return 0
}
# ------------------------------------------------------------------------------
# Checks for new GitHub tag (for repos without releases).
#
# Description:
# - Uses get_latest_gh_tag to fetch the latest tag
# - Compares it to a local cached version (~/.<app>)
# - If newer, sets global CHECK_UPDATE_RELEASE and returns 0
#
# Usage:
# if check_for_gh_tag "guacd" "apache/guacamole-server"; then
# fetch_and_deploy_gh_tag "guacd" "apache/guacamole-server" "/opt/guacamole-server"
# fi
#
# Notes:
# - For repos that only publish tags, not GitHub Releases
# - Same interface as check_for_gh_release
# ------------------------------------------------------------------------------
check_for_gh_tag() {
local app="$1"
local repo="$2"
local prefix="${3:-}"
local app_lc=""
app_lc="$(echo "${app,,}" | tr -d ' ')"
local current_file="$HOME/.${app_lc}"
msg_info "Checking for update: ${app}"
local latest=""
latest=$(get_latest_gh_tag "$repo" "$prefix") || return 1
local current=""
[[ -f "$current_file" ]] && current="$(<"$current_file")"
if [[ -z "$current" || "$current" != "$latest" ]]; then
CHECK_UPDATE_RELEASE="$latest"
msg_ok "Update available: ${app} ${current:-not installed} → ${latest}"
return 0
fi
msg_ok "No update available: ${app} (${latest})"
return 1
}
# ==============================================================================
# INSTALL FUNCTIONS
# ==============================================================================
@@ -2198,6 +2271,7 @@ check_for_gh_release() {
local app="$1"
local source="$2"
local pinned_version_in="${3:-}" # optional
local pin_reason="${4:-}" # optional reason shown to user
local app_lc=""
app_lc="$(echo "${app,,}" | tr -d ' ')"
local current_file="$HOME/.${app_lc}"
@@ -2219,6 +2293,35 @@ check_for_gh_release() {
# Try /latest endpoint for non-pinned versions (most efficient)
local releases_json="" http_code=""
# For pinned versions, query the specific release tag directly
if [[ -n "$pinned_version_in" ]]; then
http_code=$(curl -sSL --max-time 20 -w "%{http_code}" -o /tmp/gh_check.json \
-H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2022-11-28' \
"${header_args[@]}" \
"https://api.github.com/repos/${source}/releases/tags/${pinned_version_in}" 2>/dev/null) || true
if [[ "$http_code" == "200" ]] && [[ -s /tmp/gh_check.json ]]; then
releases_json="[$(</tmp/gh_check.json)]"
elif [[ "$http_code" == "401" ]]; then
msg_error "GitHub API authentication failed (HTTP 401)."
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
msg_error "Your GITHUB_TOKEN appears to be invalid or expired."
else
msg_error "The repository may require authentication. Try: export GITHUB_TOKEN=\"ghp_your_token\""
fi
rm -f /tmp/gh_check.json
return 1
elif [[ "$http_code" == "403" ]]; then
msg_error "GitHub API rate limit exceeded (HTTP 403)."
msg_error "To increase the limit, export a GitHub token before running the script:"
msg_error " export GITHUB_TOKEN=\"ghp_your_token_here\""
rm -f /tmp/gh_check.json
return 1
fi
rm -f /tmp/gh_check.json
fi
if [[ -z "$pinned_version_in" ]]; then
http_code=$(curl -sSL --max-time 20 -w "%{http_code}" -o /tmp/gh_check.json \
-H 'Accept: application/vnd.github+json' \
@@ -2342,7 +2445,11 @@ check_for_gh_release() {
return 0
fi
msg_ok "No update available: ${app} is already on pinned version (${current})"
if [[ -n "$pin_reason" ]]; then
msg_ok "No update available: ${app} (${current}) - update held back: ${pin_reason}"
else
msg_ok "No update available: ${app} (${current}) - update temporarily held back due to issues with newer releases"
fi
return 1
fi
@@ -2381,6 +2488,7 @@ check_for_codeberg_release() {
local app="$1"
local source="$2"
local pinned_version_in="${3:-}" # optional
local pin_reason="${4:-}" # optional reason shown to user
local app_lc="${app,,}"
local current_file="$HOME/.${app_lc}"
@@ -2460,7 +2568,11 @@ check_for_codeberg_release() {
return 0
fi
msg_ok "No update available: ${app} is already on pinned version (${current})"
if [[ -n "$pin_reason" ]]; then
msg_ok "No update available: ${app} (${current}) - update held back: ${pin_reason}"
else
msg_ok "No update available: ${app} (${current}) - update temporarily held back due to issues with newer releases"
fi
return 1
fi
@@ -2486,6 +2598,8 @@ check_for_codeberg_release() {
# ------------------------------------------------------------------------------
create_self_signed_cert() {
local APP_NAME="${1:-${APPLICATION}}"
local HOSTNAME="$(hostname -f)"
local IP="$(hostname -I | awk '{print $1}')"
local APP_NAME_LC=$(echo "${APP_NAME,,}" | tr -d ' ')
local CERT_DIR="/etc/ssl/${APP_NAME_LC}"
local CERT_KEY="${CERT_DIR}/${APP_NAME_LC}.key"
@@ -2503,8 +2617,8 @@ create_self_signed_cert() {
mkdir -p "$CERT_DIR"
$STD openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 \
-subj "/CN=${APP_NAME}" \
-addext "subjectAltName=DNS:${APP_NAME}" \
-subj "/CN=${HOSTNAME}" \
-addext "subjectAltName=DNS:${HOSTNAME},DNS:localhost,IP:${IP},IP:127.0.0.1" \
-keyout "$CERT_KEY" \
-out "$CERT_CRT" || {
msg_error "Failed to create self-signed certificate"
@@ -2574,6 +2688,30 @@ function ensure_usr_local_bin_persist() {
fi
}
# ------------------------------------------------------------------------------
# curl_download - Downloads a file with automatic retry and exponential backoff.
#
# Usage: curl_download <output_file> <url>
#
# Retries up to 5 times with increasing --max-time (60/120/240/480/960s).
# Returns 0 on success, 1 if all attempts fail.
# ------------------------------------------------------------------------------
function curl_download() {
local output="$1"
local url="$2"
local timeouts=(60 120 240 480 960)
for i in "${!timeouts[@]}"; do
if curl --connect-timeout 15 --max-time "${timeouts[$i]}" -fsSL -o "$output" "$url"; then
return 0
fi
if ((i < ${#timeouts[@]} - 1)); then
msg_warn "Download timed out after ${timeouts[$i]}s, retrying... (attempt $((i + 2))/${#timeouts[@]})"
fi
done
return 1
}
# ------------------------------------------------------------------------------
# Downloads and deploys latest Codeberg release (source, binary, tarball, asset).
#
@@ -2631,8 +2769,7 @@ function fetch_and_deploy_codeberg_release() {
local app_lc=$(echo "${app,,}" | tr -d ' ')
local version_file="$HOME/.${app_lc}"
local api_timeout="--connect-timeout 10 --max-time 60"
local download_timeout="--connect-timeout 15 --max-time 900"
local api_timeouts=(60 120 240)
local current_version=""
[[ -f "$version_file" ]] && current_version=$(<"$version_file")
@@ -2672,7 +2809,7 @@ function fetch_and_deploy_codeberg_release() {
# Codeberg archive URL format: https://codeberg.org/{owner}/{repo}/archive/{tag}.tar.gz
local archive_url="https://codeberg.org/$repo/archive/${tag_name}.tar.gz"
if curl "$download_timeout" -fsSL -o "$tmpdir/$filename" "$archive_url"; then
if curl_download "$tmpdir/$filename" "$archive_url"; then
download_success=true
fi
@@ -2719,16 +2856,18 @@ function fetch_and_deploy_codeberg_release() {
return 1
fi
local max_retries=3 retry_delay=2 attempt=1 success=false resp http_code
local attempt=0 success=false resp http_code
while ((attempt <= max_retries)); do
resp=$(curl "$api_timeout" -fsSL -w "%{http_code}" -o /tmp/codeberg_rel.json "$api_url") && success=true && break
sleep "$retry_delay"
while ((attempt < ${#api_timeouts[@]})); do
resp=$(curl --connect-timeout 10 --max-time "${api_timeouts[$attempt]}" -fsSL -w "%{http_code}" -o /tmp/codeberg_rel.json "$api_url") && success=true && break
((attempt++))
if ((attempt < ${#api_timeouts[@]})); then
msg_warn "API request timed out after ${api_timeouts[$((attempt - 1))]}s, retrying... (attempt $((attempt + 1))/${#api_timeouts[@]})"
fi
done
if ! $success; then
msg_error "Failed to fetch release metadata from $api_url after $max_retries attempts"
msg_error "Failed to fetch release metadata from $api_url after ${#api_timeouts[@]} attempts"
return 1
fi
@@ -2769,7 +2908,7 @@ function fetch_and_deploy_codeberg_release() {
# Codeberg archive URL format
local archive_url="https://codeberg.org/$repo/archive/${tag_name}.tar.gz"
if curl "$download_timeout" -fsSL -o "$tmpdir/$filename" "$archive_url"; then
if curl_download "$tmpdir/$filename" "$archive_url"; then
download_success=true
fi
@@ -2843,7 +2982,7 @@ function fetch_and_deploy_codeberg_release() {
fi
filename="${url_match##*/}"
curl "$download_timeout" -fsSL -o "$tmpdir/$filename" "$url_match" || {
curl_download "$tmpdir/$filename" "$url_match" || {
msg_error "Download failed: $url_match"
rm -rf "$tmpdir"
return 1
@@ -2886,7 +3025,7 @@ function fetch_and_deploy_codeberg_release() {
}
filename="${asset_url##*/}"
curl "$download_timeout" -fsSL -o "$tmpdir/$filename" "$asset_url" || {
curl_download "$tmpdir/$filename" "$asset_url" || {
msg_error "Download failed: $asset_url"
rm -rf "$tmpdir"
return 1
@@ -2987,7 +3126,7 @@ function fetch_and_deploy_codeberg_release() {
local target_file="$app"
[[ "$use_filename" == "true" ]] && target_file="$filename"
curl "$download_timeout" -fsSL -o "$target/$target_file" "$asset_url" || {
curl_download "$target/$target_file" "$asset_url" || {
msg_error "Download failed: $asset_url"
rm -rf "$tmpdir"
return 1
@@ -3182,8 +3321,7 @@ function fetch_and_deploy_gh_release() {
local app_lc=$(echo "${app,,}" | tr -d ' ')
local version_file="$HOME/.${app_lc}"
local api_timeout="--connect-timeout 10 --max-time 60"
local download_timeout="--connect-timeout 15 --max-time 900"
local api_timeouts=(60 120 240)
local current_version=""
[[ -f "$version_file" ]] && current_version=$(<"$version_file")
@@ -3203,10 +3341,10 @@ function fetch_and_deploy_gh_release() {
return 1
fi
local max_retries=3 retry_delay=2 attempt=1 success=false http_code
local max_retries=${#api_timeouts[@]} retry_delay=2 attempt=1 success=false http_code
while ((attempt <= max_retries)); do
http_code=$(curl "$api_timeout" -sSL -w "%{http_code}" -o /tmp/gh_rel.json "${header[@]}" "$api_url" 2>/dev/null) || true
http_code=$(curl --connect-timeout 10 --max-time "${api_timeouts[$((attempt - 1))]:-240}" -sSL -w "%{http_code}" -o /tmp/gh_rel.json "${header[@]}" "$api_url" 2>/dev/null) || true
if [[ "$http_code" == "200" ]]; then
success=true
break
@@ -3280,7 +3418,7 @@ function fetch_and_deploy_gh_release() {
local direct_tarball_url="https://github.com/$repo/archive/refs/tags/$tag_name.tar.gz"
filename="${app_lc}-${version_safe}.tar.gz"
curl "$download_timeout" -fsSL -o "$tmpdir/$filename" "$direct_tarball_url" || {
curl_download "$tmpdir/$filename" "$direct_tarball_url" || {
msg_error "Download failed: $direct_tarball_url"
rm -rf "$tmpdir"
return 1
@@ -3383,7 +3521,7 @@ function fetch_and_deploy_gh_release() {
fi
filename="${url_match##*/}"
curl "$download_timeout" -fsSL -o "$tmpdir/$filename" "$url_match" || {
curl_download "$tmpdir/$filename" "$url_match" || {
msg_error "Download failed: $url_match"
rm -rf "$tmpdir"
return 1
@@ -3395,7 +3533,7 @@ function fetch_and_deploy_gh_release() {
local dpkg_opts=""
[[ "${DPKG_FORCE_CONFOLD:-}" == "1" ]] && dpkg_opts="-o Dpkg::Options::=--force-confold"
[[ "${DPKG_FORCE_CONFNEW:-}" == "1" ]] && dpkg_opts="-o Dpkg::Options::=--force-confnew"
DEBIAN_FRONTEND=noninteractive SYSTEMD_OFFLINE=1 $STD apt install -y "$dpkg_opts" "$tmpdir/$filename" || {
DEBIAN_FRONTEND=noninteractive SYSTEMD_OFFLINE=1 $STD apt install -y $dpkg_opts "$tmpdir/$filename" || {
SYSTEMD_OFFLINE=1 $STD dpkg -i "$tmpdir/$filename" || {
msg_error "Both apt and dpkg installation failed"
rm -rf "$tmpdir"
@@ -3450,7 +3588,7 @@ function fetch_and_deploy_gh_release() {
}
filename="${asset_url##*/}"
curl "$download_timeout" -fsSL -o "$tmpdir/$filename" "$asset_url" || {
curl_download "$tmpdir/$filename" "$asset_url" || {
msg_error "Download failed: $asset_url"
rm -rf "$tmpdir"
return 1
@@ -3571,7 +3709,7 @@ function fetch_and_deploy_gh_release() {
local target_file="$app"
[[ "$use_filename" == "true" ]] && target_file="$filename"
curl "$download_timeout" -fsSL -o "$target/$target_file" "$asset_url" || {
curl_download "$target/$target_file" "$asset_url" || {
msg_error "Download failed: $asset_url"
rm -rf "$tmpdir"
return 1
@@ -4128,6 +4266,8 @@ function setup_gs() {
# - NVIDIA requires matching host driver version
# ------------------------------------------------------------------------------
function setup_hwaccel() {
local service_user="${1:-}"
# Check if user explicitly disabled GPU in advanced settings
# ENABLE_GPU is exported from build.func
if [[ "${ENABLE_GPU:-no}" == "no" ]]; then
@@ -4379,7 +4519,7 @@ function setup_hwaccel() {
# ═══════════════════════════════════════════════════════════════════════════
# Device Permissions
# ═══════════════════════════════════════════════════════════════════════════
_setup_gpu_permissions "$in_ct"
_setup_gpu_permissions "$in_ct" "$service_user"
cache_installed_version "hwaccel" "1.0"
msg_ok "Setup Hardware Acceleration"
@@ -4572,9 +4712,6 @@ _setup_amd_apu() {
$STD apt -y install firmware-amd-graphics 2>/dev/null || true
fi
# ROCm compute stack (OpenCL + HIP) - also works for many APUs
_setup_rocm "$os_id" "$os_codename"
msg_ok "AMD APU configured"
}
@@ -4622,16 +4759,9 @@ _setup_rocm() {
return 0
}
# AMDGPU driver repository (append to same keyring)
{
echo ""
echo "Types: deb"
echo "URIs: https://repo.radeon.com/amdgpu/latest/ubuntu"
echo "Suites: ${ROCM_REPO_CODENAME}"
echo "Components: main"
echo "Architectures: amd64"
echo "Signed-By: /etc/apt/keyrings/rocm.gpg"
} >>/etc/apt/sources.list.d/rocm.sources
# Note: The amdgpu/latest/ubuntu repo (kernel driver packages) is intentionally
# omitted — kernel drivers are managed by the Proxmox host, not the LXC container.
# Only the ROCm userspace compute stack is needed inside the container.
# Pin ROCm packages to prefer radeon repo
cat <<EOF >/etc/apt/preferences.d/rocm-pin-600
@@ -4640,7 +4770,26 @@ Pin: release o=repo.radeon.com
Pin-Priority: 600
EOF
$STD apt update
# apt update with retry — repo.radeon.com CDN can be mid-sync (transient size mismatches).
# Run with ERR trap disabled so a transient failure does not abort the entire install.
local _apt_ok=0
for _attempt in 1 2 3; do
if (
set +e
apt-get update -qq 2>&1
exit $?
) 2>/dev/null; then
_apt_ok=1
break
fi
msg_warn "apt update failed (attempt ${_attempt}/3) — AMD repo may be temporarily unavailable, retrying in 30s…"
sleep 30
done
if [[ $_apt_ok -eq 0 ]]; then
msg_warn "apt update still failing after 3 attempts — skipping ROCm install"
return 0
fi
# Install only runtime packages — full 'rocm' meta-package includes 15GB+ dev tools
$STD apt install -y rocm-opencl-runtime rocm-hip-runtime rocm-smi-lib 2>/dev/null || {
msg_warn "ROCm runtime install failed — trying minimal set"
@@ -4724,7 +4873,7 @@ _setup_nvidia_gpu() {
if [[ -n "$available_version" ]]; then
msg_info "Found available NVIDIA version: ${available_version}"
local nvidia_pkgs="libcuda1=${available_version} libnvcuvid1=${available_version} libnvidia-encode1=${available_version} libnvidia-ml1=${available_version}"
if $STD apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends "$nvidia_pkgs" 2>/dev/null; then
if $STD apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends $nvidia_pkgs 2>/dev/null; then
msg_ok "Installed NVIDIA libraries (${available_version})"
else
msg_warn "Failed to install NVIDIA ${available_version} - trying unversioned"
@@ -4788,7 +4937,7 @@ NVIDIA_PIN
if [[ -n "$available_version" ]]; then
msg_info "Installing NVIDIA libraries (version ${available_version})"
local nvidia_pkgs="libcuda1=${available_version} libnvcuvid1=${available_version} libnvidia-encode1=${available_version} libnvidia-ml1=${available_version}"
if $STD apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends "$nvidia_pkgs" 2>/dev/null; then
if $STD apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends $nvidia_pkgs 2>/dev/null; then
msg_ok "Installed version-matched NVIDIA libraries"
else
msg_warn "Version-pinned install failed - trying unpinned"
@@ -4851,13 +5000,13 @@ NVIDIA_PIN
if [[ -n "$available_version" ]]; then
msg_info "Installing NVIDIA libraries (version ${available_version})"
local nvidia_pkgs="libcuda1=${available_version} libnvcuvid1=${available_version} libnvidia-encode1=${available_version} libnvidia-ml1=${available_version}"
if $STD apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends "$nvidia_pkgs" 2>/dev/null; then
if $STD apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends $nvidia_pkgs 2>/dev/null; then
msg_ok "Installed version-matched NVIDIA libraries"
else
# Fallback to Ubuntu repo packages with versioned nvidia-utils
msg_warn "CUDA repo install failed - trying Ubuntu native packages (nvidia-utils-${nvidia_major_version})"
if $STD apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends \
libnvidia-decode-"${nvidia_major_version}" libnvidia-encode-"${nvidia_major_version}" nvidia-utils-"${nvidia_major_version}" 2>/dev/null; then
libnvidia-decode-${nvidia_major_version} libnvidia-encode-${nvidia_major_version} nvidia-utils-${nvidia_major_version} 2>/dev/null; then
msg_ok "Installed Ubuntu NVIDIA packages (${nvidia_major_version})"
else
msg_warn "NVIDIA driver installation failed - please install manually: apt install nvidia-utils-${nvidia_major_version}"
@@ -4868,7 +5017,7 @@ NVIDIA_PIN
# Fallback to Ubuntu repo packages with versioned nvidia-utils
msg_info "Trying Ubuntu native packages (nvidia-utils-${nvidia_major_version})"
if $STD apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends \
libnvidia-decode-"${nvidia_major_version}" libnvidia-encode-"${nvidia_major_version}" nvidia-utils-"${nvidia_major_version}" 2>/dev/null; then
libnvidia-decode-${nvidia_major_version} libnvidia-encode-${nvidia_major_version} nvidia-utils-${nvidia_major_version} 2>/dev/null; then
msg_ok "Installed Ubuntu NVIDIA packages (${nvidia_major_version})"
else
msg_warn "NVIDIA driver installation failed - please install manually: apt install nvidia-utils-${nvidia_major_version}"
@@ -5004,6 +5153,7 @@ EOF
# ══════════════════════════════════════════════════════════════════════════════
_setup_gpu_permissions() {
local in_ct="$1"
local service_user="${2:-}"
# /dev/dri permissions (Intel/AMD)
if [[ "$in_ct" == "0" && -d /dev/dri ]]; then
@@ -5070,6 +5220,12 @@ _setup_gpu_permissions() {
chmod 666 /dev/kfd 2>/dev/null || true
msg_info "AMD ROCm compute device configured"
fi
# Add service user to render and video groups for GPU hardware acceleration
if [[ -n "$service_user" ]]; then
usermod -aG render "$service_user" 2>/dev/null || true
usermod -aG video "$service_user" 2>/dev/null || true
fi
}
# ------------------------------------------------------------------------------
@@ -5207,24 +5363,7 @@ function setup_java() {
# Get currently installed version
local INSTALLED_VERSION=""
if dpkg -l | grep -q "temurin-.*-jdk" 2>/dev/null; then
INSTALLED_VERSION=$(dpkg -l 2>/dev/null | awk '/temurin-.*-jdk/{print $2}' | grep -oP 'temurin-\K[0-9]+' | head -n1 || echo "")
fi
# Validate INSTALLED_VERSION is not empty if JDK package found
local JDK_COUNT=0
JDK_COUNT=$(dpkg -l 2>/dev/null | grep -c "temurin-.*-jdk" || true)
if [[ -z "$INSTALLED_VERSION" && "${JDK_COUNT:-0}" -gt 0 ]]; then
msg_warn "Found Temurin JDK but cannot determine version - attempting reinstall"
# Try to get actual package name for purge
local OLD_PACKAGE
OLD_PACKAGE=$(dpkg -l 2>/dev/null | awk '/temurin-.*-jdk/{print $2}' | head -n1 || echo "")
if [[ -n "$OLD_PACKAGE" ]]; then
msg_info "Removing existing package: $OLD_PACKAGE"
$STD apt purge -y "$OLD_PACKAGE" || true
fi
INSTALLED_VERSION="" # Reset to trigger fresh install
fi
INSTALLED_VERSION=$(dpkg-query -W -f '${Package}\n' 2>/dev/null | grep -oP '^temurin-\K[0-9]+(?=-jdk$)' | head -n1 || echo "")
# Scenario 1: Already at correct version
if [[ "$INSTALLED_VERSION" == "$JAVA_VERSION" ]]; then
@@ -5948,23 +6087,6 @@ function setup_mysql() {
return 0
fi
# Debian 13+ Fix: MySQL repo has expired GPG key (as of 2025-10-22), use MariaDB
# See: https://bugs.mysql.com/bug.php?id=119212
# This check MUST be before prepare_repository_setup to avoid adding broken repo
if [[ "$DISTRO_ID" == "debian" && "$DISTRO_CODENAME" =~ ^(trixie|forky|sid)$ ]]; then
msg_info "Debian ${DISTRO_CODENAME} detected → using MariaDB (MySQL repo GPG key expired)"
# Use distro's MariaDB directly - no external repo needed
install_packages_with_retry "mariadb-server" "mariadb-client" || {
msg_error "Failed to install MariaDB on Debian ${DISTRO_CODENAME}"
return 1
}
cache_installed_version "mysql" "mariadb"
msg_ok "Setup MariaDB (MySQL-compatible, Debian ${DISTRO_CODENAME})"
return 0
fi
# Scenario 2: Different version installed - clean upgrade
if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" != "$MYSQL_VERSION" ]]; then
msg_info "Upgrade MySQL from $CURRENT_VERSION to $MYSQL_VERSION"
@@ -5979,6 +6101,47 @@ function setup_mysql() {
return 1
}
# Debian 13+ Fix: MySQL 8.0 incompatible with libaio1t64, use 8.4 LTS
if [[ "$DISTRO_ID" == "debian" && "$DISTRO_CODENAME" =~ ^(trixie|forky|sid)$ ]]; then
msg_info "Debian ${DISTRO_CODENAME} detected → using MySQL 8.4 LTS (libaio1t64 compatible)"
if ! download_gpg_key "https://repo.mysql.com/RPM-GPG-KEY-mysql-2023" "/etc/apt/keyrings/mysql.gpg" "dearmor"; then
msg_error "Failed to import MySQL GPG key"
return 1
fi
cat >/etc/apt/sources.list.d/mysql.sources <<EOF
Types: deb
URIs: https://repo.mysql.com/apt/debian/
Suites: bookworm
Components: mysql-8.4-lts
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/mysql.gpg
EOF
$STD apt update || {
msg_error "Failed to update APT for MySQL 8.4 LTS"
return 1
}
# Install with retry logic
if ! install_packages_with_retry "mysql-community-server" "mysql-community-client"; then
msg_warn "MySQL 8.4 LTS installation failed – falling back to MariaDB"
cleanup_old_repo_files "mysql"
$STD apt update
install_packages_with_retry "mariadb-server" "mariadb-client" || {
msg_error "Failed to install database engine (MySQL/MariaDB fallback)"
return 1
}
msg_ok "Setup Database Engine (MariaDB fallback on Debian ${DISTRO_CODENAME})"
return 0
fi
cache_installed_version "mysql" "8.4"
msg_ok "Setup MySQL 8.4 LTS (Debian ${DISTRO_CODENAME})"
return 0
fi
# Standard setup for other distributions
local SUITE
if [[ "$DISTRO_ID" == "debian" ]]; then
@@ -5992,7 +6155,7 @@ function setup_mysql() {
# Setup repository
manage_tool_repository "mysql" "$MYSQL_VERSION" "https://repo.mysql.com/apt/${DISTRO_ID}" \
"https://repo.mysql.com/RPM-GPG-KEY-mysql-2025" || {
"https://repo.mysql.com/RPM-GPG-KEY-mysql-2023" || {
msg_error "Failed to setup MySQL repository"
return 1
}
@@ -6415,8 +6578,8 @@ EOF
msg_warn "php${PHP_VERSION}-fpm not available"
fi
# Create systemd override for PHP-FPM to fix runtime directory issues in LXC containers
mkdir -p /etc/systemd/system/php"${PHP_VERSION}"-fpm.service.d/
cat <<EOF >/etc/systemd/system/php"${PHP_VERSION}"-fpm.service.d/override.conf
mkdir -p /etc/systemd/system/php${PHP_VERSION}-fpm.service.d/
cat <<EOF >/etc/systemd/system/php${PHP_VERSION}-fpm.service.d/override.conf
[Service]
RuntimeDirectory=php
RuntimeDirectoryMode=0755
@@ -6442,7 +6605,7 @@ EOF
msg_info "Installing PHP ${PHP_VERSION} packages"
# First attempt: Install all verified packages at once
if ! $STD apt install -y "$MODULE_LIST" 2>/dev/null; then
if ! $STD apt install -y $MODULE_LIST 2>/dev/null; then
msg_warn "Bulk installation failed, attempting individual installation"
# Install main package first (critical)
@@ -6496,8 +6659,8 @@ EOF
# Enable and restart PHP-FPM if requested
if [[ "$PHP_FPM" == "YES" ]]; then
if systemctl list-unit-files | grep -q "php${PHP_VERSION}-fpm.service"; then
$STD systemctl enable php"${PHP_VERSION}"-fpm
safe_service_restart php"${PHP_VERSION}"-fpm
$STD systemctl enable php${PHP_VERSION}-fpm
safe_service_restart php${PHP_VERSION}-fpm
fi
fi
@@ -6528,22 +6691,38 @@ EOF
# - Optionally uses official PGDG repository for specific versions
# - Detects existing PostgreSQL version
# - Dumps all databases before upgrade
# - Installs optional PG_MODULES (e.g. postgis, contrib)
# - Installs optional PG_MODULES (e.g. postgis, contrib, cron)
# - Restores dumped data post-upgrade
#
# Variables:
# USE_PGDG_REPO - Use official PGDG repository (default: true)
# Set to "false" to use distro packages instead
# PG_VERSION - Major PostgreSQL version (e.g. 15, 16) (default: 16)
# PG_MODULES - Comma-separated list of modules (e.g. "postgis,contrib")
# PG_MODULES - Comma-separated list of modules (e.g. "postgis,contrib,cron")
#
# Examples:
# setup_postgresql # Uses PGDG repo, PG 16
# PG_VERSION="17" setup_postgresql # Specific version from PGDG
# USE_PGDG_REPO=false setup_postgresql # Uses distro package instead
# setup_postgresql # Uses PGDG repo, PG 16
# PG_VERSION="17" setup_postgresql # Specific version from PGDG
# USE_PGDG_REPO=false setup_postgresql # Uses distro package instead
# PG_VERSION="17" PG_MODULES="cron" setup_postgresql # With pg_cron module
# ------------------------------------------------------------------------------
function setup_postgresql() {
# Internal helper: Configure shared_preload_libraries for pg_cron
_configure_pg_cron_preload() {
local modules="${1:-}"
[[ -z "$modules" ]] && return 0
if [[ ",$modules," == *",cron,"* ]]; then
local current_libs
current_libs=$(sudo -u postgres psql -tAc "SHOW shared_preload_libraries;" 2>/dev/null || echo "")
if [[ "$current_libs" != *"pg_cron"* ]]; then
local new_libs="${current_libs:+${current_libs},}pg_cron"
$STD sudo -u postgres psql -c "ALTER SYSTEM SET shared_preload_libraries = '${new_libs}';"
$STD systemctl restart postgresql
fi
fi
}
setup_postgresql() {
local PG_VERSION="${PG_VERSION:-16}"
local PG_MODULES="${PG_MODULES:-}"
local USE_PGDG_REPO="${USE_PGDG_REPO:-true}"
@@ -6581,6 +6760,7 @@ function setup_postgresql() {
$STD apt install -y "postgresql-${CURRENT_PG_VERSION}-${module}" 2>/dev/null || true
done
fi
_configure_pg_cron_preload "$PG_MODULES"
return 0
fi
@@ -6616,6 +6796,7 @@ function setup_postgresql() {
$STD apt install -y "postgresql-${INSTALLED_VERSION}-${module}" 2>/dev/null || true
done
fi
_configure_pg_cron_preload "$PG_MODULES"
return 0
fi
@@ -6637,6 +6818,7 @@ function setup_postgresql() {
$STD apt install -y "postgresql-${PG_VERSION}-${module}" 2>/dev/null || true
done
fi
_configure_pg_cron_preload "$PG_MODULES"
return 0
fi
@@ -6664,13 +6846,16 @@ function setup_postgresql() {
local SUITE
case "$DISTRO_CODENAME" in
trixie | forky | sid)
if verify_repo_available "https://apt.postgresql.org/pub/repos/apt" "trixie-pgdg"; then
SUITE="trixie-pgdg"
else
msg_warn "PGDG repo not available for ${DISTRO_CODENAME}, falling back to distro packages"
USE_PGDG_REPO=false setup_postgresql
return $?
fi
;;
*)
SUITE=$(get_fallback_suite "$DISTRO_ID" "$DISTRO_CODENAME" "https://apt.postgresql.org/pub/repos/apt")
@@ -6754,6 +6939,7 @@ function setup_postgresql() {
}
done
fi
_configure_pg_cron_preload "$PG_MODULES"
}
# ------------------------------------------------------------------------------
@@ -6772,6 +6958,7 @@ function setup_postgresql() {
# PG_DB_NAME="immich" PG_DB_USER="immich" PG_DB_EXTENSIONS="pgvector" setup_postgresql_db
# PG_DB_NAME="ghostfolio" PG_DB_USER="ghostfolio" PG_DB_GRANT_SUPERUSER="true" setup_postgresql_db
# PG_DB_NAME="adventurelog" PG_DB_USER="adventurelog" PG_DB_EXTENSIONS="postgis" setup_postgresql_db
# PG_DB_NAME="splitpro" PG_DB_USER="splitpro" PG_DB_EXTENSIONS="pg_cron" setup_postgresql_db
#
# Variables:
# PG_DB_NAME - Database name (required)
@@ -6803,6 +6990,13 @@ function setup_postgresql_db() {
$STD sudo -u postgres psql -c "CREATE ROLE $PG_DB_USER WITH LOGIN PASSWORD '$PG_DB_PASS';"
$STD sudo -u postgres psql -c "CREATE DATABASE $PG_DB_NAME WITH OWNER $PG_DB_USER ENCODING 'UTF8' TEMPLATE template0;"
# Configure pg_cron database BEFORE creating the extension (must be set before pg_cron loads)
if [[ -n "${PG_DB_EXTENSIONS:-}" ]] && [[ ",${PG_DB_EXTENSIONS//[[:space:]]/}," == *",pg_cron,"* ]]; then
$STD sudo -u postgres psql -c "ALTER SYSTEM SET cron.database_name = '${PG_DB_NAME}';"
$STD sudo -u postgres psql -c "ALTER SYSTEM SET cron.timezone = 'UTC';"
$STD systemctl restart postgresql
fi
# Install extensions (comma-separated)
if [[ -n "${PG_DB_EXTENSIONS:-}" ]]; then
IFS=',' read -ra EXT_LIST <<<"${PG_DB_EXTENSIONS:-}"
@@ -6812,6 +7006,12 @@ function setup_postgresql_db() {
done
fi
# Grant pg_cron schema permissions to DB user
if [[ -n "${PG_DB_EXTENSIONS:-}" ]] && [[ ",${PG_DB_EXTENSIONS//[[:space:]]/}," == *",pg_cron,"* ]]; then
$STD sudo -u postgres psql -d "$PG_DB_NAME" -c "GRANT USAGE ON SCHEMA cron TO ${PG_DB_USER};"
$STD sudo -u postgres psql -d "$PG_DB_NAME" -c "GRANT ALL ON ALL TABLES IN SCHEMA cron TO ${PG_DB_USER};"
fi
# ALTER ROLE settings for Django/Rails compatibility (unless skipped)
if [[ "${PG_DB_SKIP_ALTER_ROLE:-}" != "true" ]]; then
$STD sudo -u postgres psql -c "ALTER ROLE $PG_DB_USER SET client_encoding TO 'utf8';"
@@ -6853,7 +7053,6 @@ function setup_postgresql_db() {
export PG_DB_USER
export PG_DB_PASS
}
# ------------------------------------------------------------------------------
# Installs rbenv and ruby-build, installs Ruby and optionally Rails.
#
@@ -420,6 +420,12 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
</details>
## 2026-03-20
### 🆕 New Scripts
- 🔄 Upstream Sync - 2026-03-20 [@BillyOutlast](https://github.com/BillyOutlast) ([#74](https://github.com/Heretek-AI/ProxmoxVE/pull/74))
## 2026-03-19
### 📂 Github
@@ -1,5 +1,5 @@
{
"generated": "2026-03-19T06:41:06Z",
"generated": "2026-03-19T18:38:18Z",
"versions": [
{
"slug": "agregarr",
@@ -18,9 +18,9 @@
{
"slug": "llamacpp",
"repo": "ggml-org/llama.cpp",
"version": "b8417",
"version": "b8429",
"pinned": false,
"date": "2026-03-19T05:37:13Z"
"date": "2026-03-19T14:00:45Z"
},
{
"slug": "localai",
@@ -54,7 +54,7 @@
"slug": "ragflow",
"repo": "infiniflow/ragflow",
"version": "v0.24.0",
"pinned": true,
"pinned": false,
"date": "2026-02-10T09:27:14Z"
},
{
@@ -0,0 +1,675 @@
name: PocketBase Bot
on:
issue_comment:
types: [created]
permissions:
issues: write
pull-requests: write
contents: read
jobs:
pocketbase-bot:
runs-on: self-hosted
# Only act on /pocketbase commands
if: startsWith(github.event.comment.body, '/pocketbase')
steps:
- name: Execute PocketBase bot command
env:
POCKETBASE_URL: ${{ secrets.POCKETBASE_URL }}
POCKETBASE_COLLECTION: ${{ secrets.POCKETBASE_COLLECTION }}
POCKETBASE_ADMIN_EMAIL: ${{ secrets.POCKETBASE_ADMIN_EMAIL }}
POCKETBASE_ADMIN_PASSWORD: ${{ secrets.POCKETBASE_ADMIN_PASSWORD }}
COMMENT_BODY: ${{ github.event.comment.body }}
COMMENT_ID: ${{ github.event.comment.id }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
REPO_OWNER: ${{ github.repository_owner }}
REPO_NAME: ${{ github.event.repository.name }}
ACTOR: ${{ github.event.comment.user.login }}
ACTOR_ASSOCIATION: ${{ github.event.comment.author_association }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
node << 'ENDSCRIPT'
(async function () {
const https = require('https');
const http = require('http');
const url = require('url');
// ── HTTP helper with redirect following ────────────────────────────
function request(fullUrl, opts, redirectCount) {
redirectCount = redirectCount || 0;
return new Promise(function (resolve, reject) {
const u = url.parse(fullUrl);
const isHttps = u.protocol === 'https:';
const body = opts.body;
const options = {
hostname: u.hostname,
port: u.port || (isHttps ? 443 : 80),
path: u.path,
method: opts.method || 'GET',
headers: opts.headers || {}
};
if (body) options.headers['Content-Length'] = Buffer.byteLength(body);
const lib = isHttps ? https : http;
const req = lib.request(options, function (res) {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
if (redirectCount >= 5) return reject(new Error('Too many redirects from ' + fullUrl));
const redirectUrl = url.resolve(fullUrl, res.headers.location);
res.resume();
resolve(request(redirectUrl, opts, redirectCount + 1));
return;
}
let data = '';
res.on('data', function (chunk) { data += chunk; });
res.on('end', function () {
resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, statusCode: res.statusCode, body: data });
});
});
req.on('error', reject);
if (body) req.write(body);
req.end();
});
}
// ── GitHub API helpers ─────────────────────────────────────────────
const owner = process.env.REPO_OWNER;
const repo = process.env.REPO_NAME;
const issueNumber = parseInt(process.env.ISSUE_NUMBER, 10);
const commentId = parseInt(process.env.COMMENT_ID, 10);
const actor = process.env.ACTOR;
function ghRequest(path, method, body) {
const headers = {
'Authorization': 'Bearer ' + process.env.GITHUB_TOKEN,
'Accept': 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
'User-Agent': 'PocketBase-Bot'
};
const bodyStr = body ? JSON.stringify(body) : undefined;
if (bodyStr) headers['Content-Type'] = 'application/json';
return request('https://api.github.com' + path, { method: method || 'GET', headers, body: bodyStr });
}
async function addReaction(content) {
try {
await ghRequest(
'/repos/' + owner + '/' + repo + '/issues/comments/' + commentId + '/reactions',
'POST', { content }
);
} catch (e) {
console.warn('Could not add reaction:', e.message);
}
}
async function postComment(text) {
const res = await ghRequest(
'/repos/' + owner + '/' + repo + '/issues/' + issueNumber + '/comments',
'POST', { body: text }
);
if (!res.ok) console.warn('Could not post comment:', res.body);
}
// ── Permission check ───────────────────────────────────────────────
// author_association: OWNER = repo/org owner, MEMBER = org member (includes Contributors team)
const association = process.env.ACTOR_ASSOCIATION;
if (association !== 'OWNER' && association !== 'MEMBER') {
await addReaction('-1');
await postComment(
'❌ **PocketBase Bot**: @' + actor + ' is not authorized to use this command.\n' +
'Only org members (Contributors team) can use `/pocketbase`.'
);
process.exit(0);
}
// ── Acknowledge ────────────────────────────────────────────────────
await addReaction('eyes');
// ── Parse command ──────────────────────────────────────────────────
// Formats (first line of comment):
// /pocketbase <slug> field=value [field=value ...] ← field updates (simple values)
// /pocketbase <slug> set <field> ← value from code block below
// /pocketbase <slug> note list|add|edit|remove ... ← note management
// /pocketbase <slug> method list ← list install methods
// /pocketbase <slug> method <type> cpu=N ram=N hdd=N ← edit install method resources
const commentBody = process.env.COMMENT_BODY || '';
const lines = commentBody.trim().split('\n');
const firstLine = lines[0].trim();
const withoutCmd = firstLine.replace(/^\/pocketbase\s+/, '').trim();
// Extract code block content from comment body (```...``` or ```lang\n...```)
function extractCodeBlock(body) {
const m = body.match(/```[^\n]*\n([\s\S]*?)```/);
return m ? m[1].trim() : null;
}
const codeBlockValue = extractCodeBlock(commentBody);
const HELP_TEXT =
'**Field update (simple):** `/pocketbase <slug> field=value [field=value ...]`\n\n' +
'**Field update (HTML/multiline) — value from code block:**\n' +
'````\n' +
'/pocketbase <slug> set description\n' +
'```html\n' +
'<p>Your <b>HTML</b> or multi-line content here</p>\n' +
'```\n' +
'````\n\n' +
'**Note management:**\n' +
'```\n' +
'/pocketbase <slug> note list\n' +
'/pocketbase <slug> note add <type> "<text>"\n' +
'/pocketbase <slug> note edit <type> "<old text>" "<new text>"\n' +
'/pocketbase <slug> note remove <type> "<text>"\n' +
'```\n\n' +
'**Install method resources:**\n' +
'```\n' +
'/pocketbase <slug> method list\n' +
'/pocketbase <slug> method <type> hdd=10\n' +
'/pocketbase <slug> method <type> cpu=4 ram=2048 hdd=20\n' +
'```\n\n' +
'**Editable fields:** `name` `description` `logo` `documentation` `website` `project_url` `github` ' +
'`config_path` `port` `default_user` `default_passwd` ' +
'`updateable` `privileged` `has_arm` `is_dev` ' +
'`is_disabled` `disable_message` `is_deleted` `deleted_message`';
if (!withoutCmd) {
await addReaction('-1');
await postComment('❌ **PocketBase Bot**: No slug or command specified.\n\n' + HELP_TEXT);
process.exit(0);
}
const spaceIdx = withoutCmd.indexOf(' ');
const slug = (spaceIdx === -1 ? withoutCmd : withoutCmd.substring(0, spaceIdx)).trim();
const rest = spaceIdx === -1 ? '' : withoutCmd.substring(spaceIdx + 1).trim();
if (!rest) {
await addReaction('-1');
await postComment('❌ **PocketBase Bot**: No command specified for slug `' + slug + '`.\n\n' + HELP_TEXT);
process.exit(0);
}
// ── Allowed fields and their types ─────────────────────────────────
// ── PocketBase: authenticate (shared by all paths) ─────────────────
const raw = process.env.POCKETBASE_URL.replace(/\/$/, '');
const apiBase = /\/api$/i.test(raw) ? raw : raw + '/api';
const coll = process.env.POCKETBASE_COLLECTION;
const authRes = await request(apiBase + '/collections/users/auth-with-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
identity: process.env.POCKETBASE_ADMIN_EMAIL,
password: process.env.POCKETBASE_ADMIN_PASSWORD
})
});
if (!authRes.ok) {
await addReaction('-1');
await postComment('❌ **PocketBase Bot**: PocketBase authentication failed. CC @' + owner + '/maintainers');
process.exit(1);
}
const token = JSON.parse(authRes.body).token;
// ── PocketBase: find record by slug (shared by all paths) ──────────
const recordsUrl = apiBase + '/collections/' + encodeURIComponent(coll) + '/records';
const filter = "(slug='" + slug.replace(/'/g, "''") + "')";
const listRes = await request(recordsUrl + '?filter=' + encodeURIComponent(filter) + '&perPage=1', {
headers: { 'Authorization': token }
});
const list = JSON.parse(listRes.body);
const record = list.items && list.items[0];
if (!record) {
await addReaction('-1');
await postComment(
'❌ **PocketBase Bot**: No record found for slug `' + slug + '`.\n\n' +
'Make sure the script was already pushed to PocketBase (JSON must exist and have been synced).'
);
process.exit(0);
}
// ── Route: dispatch to subcommand handler ──────────────────────────
const noteMatch = rest.match(/^note\s+(list|add|edit|remove)\b/i);
const methodMatch = rest.match(/^method\b/i);
const setMatch = rest.match(/^set\s+(\S+)/i);
if (noteMatch) {
// ── NOTE SUBCOMMAND (reads/writes notes_json on script record) ────
const noteAction = noteMatch[1].toLowerCase();
const noteArgsStr = rest.substring(noteMatch[0].length).trim();
// Parse notes_json from the already-fetched script record
// PocketBase may return JSON fields as already-parsed objects
let notesArr = [];
try {
const rawNotes = record.notes_json;
notesArr = Array.isArray(rawNotes) ? rawNotes : JSON.parse(rawNotes || '[]');
} catch (e) { notesArr = []; }
// Token parser: unquoted-word OR "quoted string" (supports \" escapes)
function parseNoteTokens(str) {
const tokens = [];
let pos = 0;
while (pos < str.length) {
while (pos < str.length && /\s/.test(str[pos])) pos++;
if (pos >= str.length) break;
if (str[pos] === '"') {
pos++;
let start = pos;
while (pos < str.length && str[pos] !== '"') {
if (str[pos] === '\\') pos++;
pos++;
}
tokens.push(str.substring(start, pos).replace(/\\"/g, '"'));
if (pos < str.length) pos++;
} else {
let start = pos;
while (pos < str.length && !/\s/.test(str[pos])) pos++;
tokens.push(str.substring(start, pos));
}
}
return tokens;
}
function formatNotesList(arr) {
if (arr.length === 0) return '*None*';
return arr.map(function (n, i) {
return (i + 1) + '. **`' + (n.type || '?') + '`**: ' + (n.text || '');
}).join('\n');
}
async function patchNotesJson(arr) {
const res = await request(recordsUrl + '/' + record.id, {
method: 'PATCH',
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
body: JSON.stringify({ notes_json: JSON.stringify(arr) })
});
if (!res.ok) {
await addReaction('-1');
await postComment('❌ **PocketBase Bot**: Failed to update `notes_json`:\n```\n' + res.body + '\n```');
process.exit(1);
}
}
if (noteAction === 'list') {
await addReaction('+1');
await postComment(
'ℹ️ **PocketBase Bot**: Notes for **`' + slug + '`** (' + notesArr.length + ' total)\n\n' +
formatNotesList(notesArr)
);
} else if (noteAction === 'add') {
const tokens = parseNoteTokens(noteArgsStr);
if (tokens.length < 2) {
await addReaction('-1');
await postComment(
'❌ **PocketBase Bot**: `note add` requires `<type>` and `"<text>"`.\n\n' +
'**Usage:** `/pocketbase ' + slug + ' note add <type> "<text>"`'
);
process.exit(0);
}
const noteType = tokens[0].toLowerCase();
const noteText = tokens.slice(1).join(' ');
notesArr.push({ type: noteType, text: noteText });
await patchNotesJson(notesArr);
await addReaction('+1');
await postComment(
'✅ **PocketBase Bot**: Added note to **`' + slug + '`**\n\n' +
'- **Type:** `' + noteType + '`\n' +
'- **Text:** ' + noteText + '\n\n' +
'*Executed by @' + actor + '*'
);
} else if (noteAction === 'edit') {
const tokens = parseNoteTokens(noteArgsStr);
if (tokens.length < 3) {
await addReaction('-1');
await postComment(
'❌ **PocketBase Bot**: `note edit` requires `<type>`, `"<old text>"`, and `"<new text>"`.\n\n' +
'**Usage:** `/pocketbase ' + slug + ' note edit <type> "<old text>" "<new text>"`\n\n' +
'Use `/pocketbase ' + slug + ' note list` to see current notes.'
);
process.exit(0);
}
const noteType = tokens[0].toLowerCase();
const oldText = tokens[1];
const newText = tokens[2];
const idx = notesArr.findIndex(function (n) {
return n.type.toLowerCase() === noteType && n.text === oldText;
});
if (idx === -1) {
await addReaction('-1');
await postComment(
'❌ **PocketBase Bot**: No `' + noteType + '` note found with that exact text.\n\n' +
'**Current notes for `' + slug + '`:**\n' + formatNotesList(notesArr)
);
process.exit(0);
}
notesArr[idx].text = newText;
await patchNotesJson(notesArr);
await addReaction('+1');
await postComment(
'✅ **PocketBase Bot**: Edited note in **`' + slug + '`**\n\n' +
'- **Type:** `' + noteType + '`\n' +
'- **Old:** ' + oldText + '\n' +
'- **New:** ' + newText + '\n\n' +
'*Executed by @' + actor + '*'
);
} else if (noteAction === 'remove') {
const tokens = parseNoteTokens(noteArgsStr);
if (tokens.length < 2) {
await addReaction('-1');
await postComment(
'❌ **PocketBase Bot**: `note remove` requires `<type>` and `"<text>"`.\n\n' +
'**Usage:** `/pocketbase ' + slug + ' note remove <type> "<text>"`\n\n' +
'Use `/pocketbase ' + slug + ' note list` to see current notes.'
);
process.exit(0);
}
const noteType = tokens[0].toLowerCase();
const noteText = tokens[1];
const before = notesArr.length;
notesArr = notesArr.filter(function (n) {
return !(n.type.toLowerCase() === noteType && n.text === noteText);
});
if (notesArr.length === before) {
await addReaction('-1');
await postComment(
'❌ **PocketBase Bot**: No `' + noteType + '` note found with that exact text.\n\n' +
'**Current notes for `' + slug + '`:**\n' + formatNotesList(notesArr)
);
process.exit(0);
}
await patchNotesJson(notesArr);
await addReaction('+1');
await postComment(
'✅ **PocketBase Bot**: Removed note from **`' + slug + '`**\n\n' +
'- **Type:** `' + noteType + '`\n' +
'- **Text:** ' + noteText + '\n\n' +
'*Executed by @' + actor + '*'
);
}
} else if (methodMatch) {
// ── METHOD SUBCOMMAND (reads/writes install_methods_json on script record) ──
const methodArgs = rest.replace(/^method\s*/i, '').trim();
const methodListMode = !methodArgs || methodArgs.toLowerCase() === 'list';
// Parse install_methods_json from the already-fetched script record
// PocketBase may return JSON fields as already-parsed objects
let methodsArr = [];
try {
const rawMethods = record.install_methods_json;
methodsArr = Array.isArray(rawMethods) ? rawMethods : JSON.parse(rawMethods || '[]');
} catch (e) { methodsArr = []; }
function formatMethodsList(arr) {
if (arr.length === 0) return '*None*';
return arr.map(function (im, i) {
const r = im.resources || {};
return (i + 1) + '. **`' + (im.type || '?') + '`** — CPU: `' + (r.cpu != null ? r.cpu : '?') +
'` · RAM: `' + (r.ram != null ? r.ram : '?') + ' MB` · HDD: `' + (r.hdd != null ? r.hdd : '?') + ' GB`';
}).join('\n');
}
async function patchInstallMethodsJson(arr) {
const res = await request(recordsUrl + '/' + record.id, {
method: 'PATCH',
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
body: JSON.stringify({ install_methods_json: JSON.stringify(arr) })
});
if (!res.ok) {
await addReaction('-1');
await postComment('❌ **PocketBase Bot**: Failed to update `install_methods_json`:\n```\n' + res.body + '\n```');
process.exit(1);
}
}
if (methodListMode) {
await addReaction('+1');
await postComment(
'ℹ️ **PocketBase Bot**: Install methods for **`' + slug + '`** (' + methodsArr.length + ' total)\n\n' +
formatMethodsList(methodsArr)
);
} else {
// Parse: <type> cpu=N ram=N hdd=N
const methodParts = methodArgs.match(/^(\S+)\s+(.+)$/);
if (!methodParts) {
await addReaction('-1');
await postComment(
'❌ **PocketBase Bot**: Invalid `method` syntax.\n\n' +
'**Usage:**\n```\n/pocketbase ' + slug + ' method list\n/pocketbase ' + slug + ' method <type> hdd=10\n/pocketbase ' + slug + ' method <type> cpu=4 ram=2048 hdd=20\n```'
);
process.exit(0);
}
const targetType = methodParts[1].toLowerCase();
const resourcesStr = methodParts[2];
// Parse resource fields (only cpu/ram/hdd allowed)
const RESOURCE_FIELDS = { cpu: true, ram: true, hdd: true };
const resourceChanges = {};
const rePairs = /([a-z]+)=(\d+)/gi;
let m;
while ((m = rePairs.exec(resourcesStr)) !== null) {
const key = m[1].toLowerCase();
if (RESOURCE_FIELDS[key]) resourceChanges[key] = parseInt(m[2], 10);
}
if (Object.keys(resourceChanges).length === 0) {
await addReaction('-1');
await postComment('❌ **PocketBase Bot**: No valid resource fields found. Use `cpu=N`, `ram=N`, `hdd=N`.');
process.exit(0);
}
// Find matching method by type name (case-insensitive)
const idx = methodsArr.findIndex(function (im) {
return (im.type || '').toLowerCase() === targetType;
});
if (idx === -1) {
await addReaction('-1');
const availableTypes = methodsArr.map(function (im) { return im.type || '?'; });
await postComment(
'❌ **PocketBase Bot**: No install method with type `' + targetType + '` found for `' + slug + '`.\n\n' +
'**Available types:** `' + (availableTypes.length ? availableTypes.join('`, `') : '(none)') + '`\n\n' +
'Use `/pocketbase ' + slug + ' method list` to see all methods.'
);
process.exit(0);
}
if (!methodsArr[idx].resources) methodsArr[idx].resources = {};
if (resourceChanges.cpu != null) methodsArr[idx].resources.cpu = resourceChanges.cpu;
if (resourceChanges.ram != null) methodsArr[idx].resources.ram = resourceChanges.ram;
if (resourceChanges.hdd != null) methodsArr[idx].resources.hdd = resourceChanges.hdd;
await patchInstallMethodsJson(methodsArr);
const changesLines = Object.entries(resourceChanges)
.map(function ([k, v]) { return '- `' + k + '` → `' + v + (k === 'ram' ? ' MB' : k === 'hdd' ? ' GB' : '') + '`'; })
.join('\n');
await addReaction('+1');
await postComment(
'✅ **PocketBase Bot**: Updated install method **`' + methodsArr[idx].type + '`** for **`' + slug + '`**\n\n' +
'**Changes applied:**\n' + changesLines + '\n\n' +
'*Executed by @' + actor + '*'
);
}
} else if (setMatch) {
// ── SET SUBCOMMAND (multi-line / HTML / special chars via code block) ──
const fieldName = setMatch[1].toLowerCase();
const SET_ALLOWED = {
name: 'string', description: 'string', logo: 'string',
documentation: 'string', website: 'string', project_url: 'string', github: 'string',
config_path: 'string', disable_message: 'string', deleted_message: 'string'
};
if (!SET_ALLOWED[fieldName]) {
await addReaction('-1');
await postComment(
'❌ **PocketBase Bot**: `set` only supports text fields.\n\n' +
'**Allowed:** `' + Object.keys(SET_ALLOWED).join('`, `') + '`\n\n' +
'For boolean/number fields use `field=value` syntax instead.'
);
process.exit(0);
}
if (!codeBlockValue) {
await addReaction('-1');
await postComment(
'❌ **PocketBase Bot**: `set` requires a code block with the value.\n\n' +
'**Usage:**\n````\n/pocketbase ' + slug + ' set ' + fieldName + '\n```\nYour content here (HTML, multiline, special chars all fine)\n```\n````'
);
process.exit(0);
}
const setPayload = {};
setPayload[fieldName] = codeBlockValue;
const setPatchRes = await request(recordsUrl + '/' + record.id, {
method: 'PATCH',
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
body: JSON.stringify(setPayload)
});
if (!setPatchRes.ok) {
await addReaction('-1');
await postComment('❌ **PocketBase Bot**: PATCH failed for `' + slug + '`:\n```\n' + setPatchRes.body + '\n```');
process.exit(1);
}
const preview = codeBlockValue.length > 300 ? codeBlockValue.substring(0, 300) + '…' : codeBlockValue;
await addReaction('+1');
await postComment(
'✅ **PocketBase Bot**: Set `' + fieldName + '` for **`' + slug + '`**\n\n' +
'**Value set:**\n```\n' + preview + '\n```\n\n' +
'*Executed by @' + actor + '*'
);
} else {
// ── FIELD=VALUE PATH ─────────────────────────────────────────────
const fieldsStr = rest;
// Skipped: slug, script_created/updated, created (auto), categories/
// install_methods/notes/type (relations), github_data/install_methods_json/
// notes_json (auto-generated), execute_in (select relation), last_update_commit (auto)
const ALLOWED_FIELDS = {
name: 'string',
description: 'string',
logo: 'string',
documentation: 'string',
website: 'string',
project_url: 'string',
github: 'string',
config_path: 'string',
port: 'number',
default_user: 'nullable_string',
default_passwd: 'nullable_string',
updateable: 'boolean',
privileged: 'boolean',
has_arm: 'boolean',
is_dev: 'boolean',
is_disabled: 'boolean',
disable_message: 'string',
is_deleted: 'boolean',
deleted_message: 'string',
};
// Field=value parser (handles quoted values and empty=null)
function parseFields(str) {
const fields = {};
let pos = 0;
while (pos < str.length) {
while (pos < str.length && /\s/.test(str[pos])) pos++;
if (pos >= str.length) break;
let keyStart = pos;
while (pos < str.length && str[pos] !== '=' && !/\s/.test(str[pos])) pos++;
const key = str.substring(keyStart, pos).trim();
if (!key || pos >= str.length || str[pos] !== '=') { pos++; continue; }
pos++;
let value;
if (str[pos] === '"') {
pos++;
let valStart = pos;
while (pos < str.length && str[pos] !== '"') {
if (str[pos] === '\\') pos++;
pos++;
}
value = str.substring(valStart, pos).replace(/\\"/g, '"');
if (pos < str.length) pos++;
} else {
let valStart = pos;
while (pos < str.length && !/\s/.test(str[pos])) pos++;
value = str.substring(valStart, pos);
}
fields[key] = value;
}
return fields;
}
const parsedFields = parseFields(fieldsStr);
const unknownFields = Object.keys(parsedFields).filter(function (f) { return !ALLOWED_FIELDS[f]; });
if (unknownFields.length > 0) {
await addReaction('-1');
await postComment(
'❌ **PocketBase Bot**: Unknown field(s): `' + unknownFields.join('`, `') + '`\n\n' +
'**Allowed fields:** `' + Object.keys(ALLOWED_FIELDS).join('`, `') + '`'
);
process.exit(0);
}
if (Object.keys(parsedFields).length === 0) {
await addReaction('-1');
await postComment('❌ **PocketBase Bot**: Could not parse any valid `field=value` pairs.\n\n' + HELP_TEXT);
process.exit(0);
}
// Cast values to correct types
const payload = {};
for (const [key, rawVal] of Object.entries(parsedFields)) {
const type = ALLOWED_FIELDS[key];
if (type === 'boolean') {
if (rawVal === 'true') payload[key] = true;
else if (rawVal === 'false') payload[key] = false;
else {
await addReaction('-1');
await postComment('❌ **PocketBase Bot**: `' + key + '` must be `true` or `false`, got: `' + rawVal + '`');
process.exit(0);
}
} else if (type === 'number') {
const n = parseInt(rawVal, 10);
if (isNaN(n)) {
await addReaction('-1');
await postComment('❌ **PocketBase Bot**: `' + key + '` must be a number, got: `' + rawVal + '`');
process.exit(0);
}
payload[key] = n;
} else if (type === 'nullable_string') {
payload[key] = rawVal === '' ? null : rawVal;
} else {
payload[key] = rawVal;
}
}
const patchRes = await request(recordsUrl + '/' + record.id, {
method: 'PATCH',
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!patchRes.ok) {
await addReaction('-1');
await postComment('❌ **PocketBase Bot**: PATCH failed for `' + slug + '`:\n```\n' + patchRes.body + '\n```');
process.exit(1);
}
await addReaction('+1');
const changesLines = Object.entries(payload)
.map(function ([k, v]) { return '- `' + k + '` → `' + JSON.stringify(v) + '`'; })
.join('\n');
await postComment(
'✅ **PocketBase Bot**: Updated **`' + slug + '`** successfully!\n\n' +
'**Changes applied:**\n' + changesLines + '\n\n' +
'*Executed by @' + actor + '*'
);
}
console.log('Done.');
})().catch(function (e) {
console.error('Fatal error:', e.message || e);
process.exit(1);
});
ENDSCRIPT
shell: bash
@@ -0,0 +1,6 @@
__ __ __ __ ___
__ ______ _____/ /___ / /_/ /_ _____/ /___ ______/ (_)___
/ / / / __ \/ ___/ / __ \/ __/ __ \______/ ___/ __/ / / / __ / / __ \
/ /_/ / / / (__ ) / /_/ / /_/ / / /_____(__ ) /_/ /_/ / /_/ / / /_/ /
\__,_/_/ /_/____/_/\____/\__/_/ /_/ /____/\__/\__,_/\__,_/_/\____/
@@ -0,0 +1,70 @@
#!/usr/bin/env bash
COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL:-https://raw.githubusercontent.com/Heretek-AI/ProxmoxVE/refs/heads/main}"
source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/build.func)
# Author: Heretek-AI
# License: MIT | https://github.com/Heretek-AI/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/unslothai/unsloth
APP="unsloth-studio"
var_tags="${var_tags:-ai;llm;fine-tuning;training}"
var_cpu="${var_cpu:-4}"
var_ram="${var_ram:-16384}"
var_disk="${var_disk:-50}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_unprivileged="${var_unprivileged:-1}"
var_gpu="${var_gpu:-yes}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /opt/unsloth-studio ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
# Source the activation script to set up GPU environment
if [[ -f /opt/unsloth-studio/activate.sh ]]; then
source /opt/unsloth-studio/activate.sh
else
# Fallback: activate venv directly
source /opt/unsloth-studio/.venv/bin/activate
fi
if command -v unsloth &>/dev/null; then
CURRENT_VERSION=$(pip show unsloth 2>/dev/null | grep -i version | awk '{print $2}' || echo "unknown")
msg_info "Current version: ${CURRENT_VERSION}"
msg_info "Checking for updates"
$STD pip install --upgrade unsloth 2>/dev/null
NEW_VERSION=$(pip show unsloth 2>/dev/null | grep -i version | awk '{print $2}' || echo "unknown")
if [[ "$CURRENT_VERSION" != "$NEW_VERSION" ]]; then
msg_ok "Updated from ${CURRENT_VERSION} to ${NEW_VERSION}"
else
msg_ok "Already at latest version: ${NEW_VERSION}"
fi
else
msg_error "Unsloth not installed properly"
exit 1
fi
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:8888${CL}"
echo -e "${INFO}${YW} Note: First launch may take 5-10 minutes to compile llama.cpp${CL}"
@@ -1,6 +1,6 @@
{
"name": "Unsloth Studio (In Beta)",
"slug": "unsolth-studio",
"name": "Unsloth Studio",
"slug": "unsloth-studio",
"categories": [20],
"date_created": "2026-03-18",
"type": "ct",
@@ -10,12 +10,12 @@
"documentation": "https://unsloth.ai/docs/new/studio/start",
"website": "https://unsloth.ai/",
"logo": "https://unsloth.ai/favicon.ico",
"config_path": "/opt/unsolth-studio/.env",
"config_path": "/opt/unsloth-studio/.env",
"description": "Local, browser-based GUI for fine-tuning LLMs without writing code. Supports QLoRA, LoRA, and full fine-tuning with live training monitoring, GPU stats, and model export to GGUF/Safetensors.",
"install_methods": [
{
"type": "default",
"script": "ct/unsolth-studio.sh",
"script": "ct/unsloth-studio.sh",
"resources": {
"cpu": 4,
"ram": 16384,
@@ -26,10 +26,14 @@
}
],
"default_credentials": {
"username": null,
"password": null
"username": "unsloth",
"password": "Check logs for generated password"
},
"notes": [
{
"text": "Default credentials: username 'unsloth', password is randomly generated and shown in logs. Run 'journalctl -u unsloth-studio | grep password' to find it.",
"type": "warning"
},
{
"text": "Requires GPU passthrough for training. NVIDIA, AMD (ROCm), and Intel GPUs are supported.",
"type": "info"
@@ -45,6 +49,10 @@
{
"text": "For AMD GPUs, ensure ROCm is properly configured on the host.",
"type": "info"
},
{
"text": "ROCm support for Unsloth Studio is not yet implemented. See https://github.com/unslothai/unsloth/pull/4390 for progress.",
"type": "warning"
}
]
}
@@ -33,8 +33,8 @@ setup_hwaccel
PYTHON_VERSION="3.13" setup_uv
msg_info "Creating Virtual Environment"
mkdir -p /opt/unsolth-studio
cd /opt/unsolth-studio || exit
mkdir -p /opt/unsloth-studio
cd /opt/unsloth-studio || exit
$STD uv venv --python 3.13
source .venv/bin/activate
msg_ok "Created Virtual Environment"
@@ -107,8 +107,10 @@ elif [ -d "/opt/rocm-6.2" ]; then
fi
# Check if GPU is available (works for both CUDA and ROCm)
if /opt/unsolth-studio/.venv/bin/python -c "import torch; exit(0 if torch.cuda.is_available() else 1)" 2>/dev/null; then
$STD /opt/unsolth-studio/.venv/bin/python -m unsloth studio setup
if /opt/unsloth-studio/.venv/bin/python -c "import torch; exit(0 if torch.cuda.is_available() else 1)" 2>/dev/null; then
# Use the unsloth CLI entry point instead of python -m unsloth
# The package installs a 'unsloth' command that provides the studio subcommand
$STD /opt/unsloth-studio/.venv/bin/unsloth studio setup
msg_ok "Completed Unsloth Studio Setup"
else
msg_info "GPU not detected via torch.cuda - skipping Unsloth Studio setup"
@@ -116,47 +118,89 @@ else
echo ""
echo -e "${GN}Note: If you have GPU passthrough configured, try:${CL}"
echo -e "${GN} 1. Restart the container: pct stop <CTID> && pct start <CTID>${CL}"
echo -e "${GN} 2. Then run: source /opt/unsolth-studio/.venv/bin/activate && unsloth studio setup${CL}"
echo -e "${GN} 2. Then run: source /opt/unsloth-studio/.venv/bin/activate && unsloth studio setup${CL}"
echo ""
fi
msg_info "Creating Directories"
mkdir -p /opt/unsolth-studio/models
mkdir -p /opt/unsolth-studio/datasets
mkdir -p /var/log/unsolth-studio
chmod 755 /var/log/unsolth-studio
mkdir -p /opt/unsloth-studio/models
mkdir -p /opt/unsloth-studio/datasets
mkdir -p /var/log/unsloth-studio
chmod 755 /var/log/unsloth-studio
msg_ok "Created Directories"
msg_info "Creating Service"
# Create environment file for ROCm/CUDA paths
cat <<EOF >/opt/unsolth-studio/environment.sh
# Detect ROCm version and create environment file
ROCM_PATH=""
HSA_GFX_VERSION=""
# Find ROCm installation
if [ -d "/opt/rocm" ]; then
ROCM_PATH="/opt/rocm"
elif [ -d "/opt/rocm-7.2" ]; then
ROCM_PATH="/opt/rocm-7.2"
elif [ -d "/opt/rocm-6.2" ]; then
ROCM_PATH="/opt/rocm-6.2"
fi
# Detect GPU architecture for HSA_OVERRIDE_GFX_VERSION (needed for consumer AMD GPUs)
if [ -n "$ROCM_PATH" ] && [ -x "$ROCM_PATH/bin/rocminfo" ]; then
GFX_ARCH=$("$ROCM_PATH/bin/rocminfo" 2>/dev/null | grep -oP 'gfx\w+' | head -1 || true)
if [ -n "$GFX_ARCH" ]; then
HSA_GFX_VERSION="$GFX_ARCH"
msg_info "Detected AMD GPU architecture: $GFX_ARCH"
fi
fi
# Create a shell wrapper script for manual commands
cat <<'EOF' >/opt/unsloth-studio/activate.sh
#!/bin/bash
# Set up GPU environment for Unsloth Studio
# Activate script for Unsloth Studio with GPU support
# Source this file before running unsloth commands manually
# Activate virtual environment
source /opt/unsloth-studio/.venv/bin/activate
# ROCm environment (AMD GPUs)
if [ -d "/opt/rocm" ]; then
export PATH="/opt/rocm/bin:\$PATH"
export LD_LIBRARY_PATH="/opt/rocm/lib:\$LD_LIBRARY_PATH"
export PATH="/opt/rocm/bin:$PATH"
export LD_LIBRARY_PATH="/opt/rocm/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
export ROCM_PATH="/opt/rocm"
elif [ -d "/opt/rocm-7.2" ]; then
export PATH="/opt/rocm-7.2/bin:\$PATH"
export LD_LIBRARY_PATH="/opt/rocm-7.2/lib:\$LD_LIBRARY_PATH"
export PATH="/opt/rocm-7.2/bin:$PATH"
export LD_LIBRARY_PATH="/opt/rocm-7.2/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
export ROCM_PATH="/opt/rocm-7.2"
elif [ -d "/opt/rocm-6.2" ]; then
export PATH="/opt/rocm-6.2/bin:\$PATH"
export LD_LIBRARY_PATH="/opt/rocm-6.2/lib:\$LD_LIBRARY_PATH"
export PATH="/opt/rocm-6.2/bin:$PATH"
export LD_LIBRARY_PATH="/opt/rocm-6.2/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
export ROCM_PATH="/opt/rocm-6.2"
fi
# NVIDIA CUDA environment
if [ -d "/usr/local/cuda" ]; then
export PATH="/usr/local/cuda/bin:\$PATH"
export LD_LIBRARY_PATH="/usr/local/cuda/lib64:\$LD_LIBRARY_PATH"
export PATH="/usr/local/cuda/bin:$PATH"
export LD_LIBRARY_PATH="/usr/local/cuda/lib64${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
fi
EOF
chmod +x /opt/unsolth-studio/environment.sh
cat <<EOF >/etc/systemd/system/unsolth-studio.service
# Detect GPU architecture for HSA_OVERRIDE_GFX_VERSION
if [ -n "$ROCM_PATH" ] && [ -x "$ROCM_PATH/bin/rocminfo" ]; then
GFX_ARCH=$("$ROCM_PATH/bin/rocminfo" 2>/dev/null | grep -oP 'gfx\w+' | head -1 || true)
if [ -n "$GFX_ARCH" ]; then
export HSA_OVERRIDE_GFX_VERSION="$GFX_ARCH"
echo "GPU architecture detected: $GFX_ARCH"
fi
fi
# Make GPU visible
export HIP_VISIBLE_DEVICES=0
echo "Environment activated. GPU ready for use."
EOF
chmod +x /opt/unsloth-studio/activate.sh
# Create systemd service with proper GPU environment
# Note: systemd Environment doesn't support shell expansion, so we use static paths
cat <<EOF >/etc/systemd/system/unsloth-studio.service
[Unit]
Description=Unsloth Studio - Local LLM Fine-tuning Web UI
After=network.target network-online.target
@@ -164,15 +208,26 @@ Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/unsolth-studio
Environment="PATH=/opt/unsolth-studio/.venv/bin:/usr/local/bin:/usr/bin:/bin"
EnvironmentFile=/opt/unsolth-studio/environment.sh
ExecStart=/opt/unsolth-studio/.venv/bin/python -m unsloth studio -H 0.0.0.0 -p 8888
WorkingDirectory=/opt/unsloth-studio
Environment="PATH=/opt/unsloth-studio/.venv/bin:/opt/rocm/bin:/opt/rocm-7.2/bin:/opt/rocm-6.2/bin:/usr/local/cuda/bin:/usr/local/bin:/usr/bin:/bin"
Environment="LD_LIBRARY_PATH=/opt/rocm/lib:/opt/rocm-7.2/lib:/opt/rocm-6.2/lib:/usr/local/cuda/lib64"
Environment="ROCM_PATH=/opt/rocm"
Environment="HIP_VISIBLE_DEVICES=0"
EOF
# Add HSA_OVERRIDE_GFX_VERSION if detected
if [ -n "$HSA_GFX_VERSION" ]; then
echo "Environment=\"HSA_OVERRIDE_GFX_VERSION=$HSA_GFX_VERSION\"" >>/etc/systemd/system/unsloth-studio.service
fi
# Complete the service file
cat <<EOF >>/etc/systemd/system/unsloth-studio.service
ExecStart=/opt/unsloth-studio/.venv/bin/unsloth studio -H 0.0.0.0 -p 8888
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=unsolth-studio
SyslogIdentifier=unsloth-studio
# Resource limits
LimitNOFILE=65535
@@ -184,16 +239,24 @@ WantedBy=multi-user.target
EOF
# Don't auto-start the service since GPU passthrough may not be configured yet
# User needs to configure GPU passthrough first, then start the service manually
systemctl enable -q unsolth-studio
systemctl enable -q unsloth-studio
msg_ok "Created Service"
echo ""
echo -e "${GN}Note: The unsolth-studio service is enabled but not started.${CL}"
echo -e "${GN}Note: The unsloth-studio service is enabled but not started.${CL}"
echo -e "${GN}Configure GPU passthrough first, then start with:${CL}"
echo -e "${GN} systemctl start unsolth-studio${CL}"
echo -e "${GN} systemctl start unsloth-studio${CL}"
echo ""
if [ -n "$HSA_GFX_VERSION" ]; then
echo -e "${YW}AMD GPU detected with architecture: $HSA_GFX_VERSION${CL}"
echo -e "${YW}HSA_OVERRIDE_GFX_VERSION has been set in the systemd service.${CL}"
echo ""
fi
echo -e "${GN}For manual commands, activate the environment first:${CL}"
echo -e "${GN} source /opt/unsloth-studio/activate.sh${CL}"
echo ""
# Create GPU passthrough info file
cat <<EOF >/opt/unsolth-studio/GPU_PASSTHROUGH.md
cat <<EOF >/opt/unsloth-studio/GPU_PASSTHROUGH.md
# GPU Passthrough Configuration for Unsloth Studio
This container has been configured for GPU acceleration for LLM fine-tuning.
@@ -232,9 +295,31 @@ lxc.cgroup2.devices.allow: c 226:128 rwm
Run these commands inside the container:
- nvidia-smi (NVIDIA GPUs)
- rocminfo (AMD GPUs)
- rocm-smi or rocminfo (AMD GPUs)
- python -c "import torch; print(torch.cuda.is_available())"
## AMD GPU Configuration
For AMD consumer GPUs (Radeon RX series), the HSA_OVERRIDE_GFX_VERSION environment
variable is automatically set during installation based on your GPU architecture.
If torch.cuda.is_available() returns False, you may need to manually set it:
\`\`\`
# Find your GPU architecture
rocminfo | grep -oP 'gfx\\w+' | head -1
# Set the override (example for RX 7900 XT - gfx1100)
export HSA_OVERRIDE_GFX_VERSION=gfx1100
\`\`\`
## Manual Environment Activation
For running unsloth commands manually, activate the environment first:
\`\`\`
source /opt/unsloth-studio/activate.sh
unsloth studio setup
\`\`\`
## Usage
Access the web UI at: http://<IP>:8888
@@ -9,10 +9,11 @@ APP="MCPHub"
var_tags="${var_tags:-ai;automation;tooling}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-2048}"
var_disk="${var_disk:-8}"
var_disk="${var_disk:-10}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_unprivileged="${var_unprivileged:-1}"
var_database="${var_database:-postgresql}"
header_info "$APP"
variables
@@ -1,7 +1,6 @@
#!/usr/bin/env bash
COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL:-https://raw.githubusercontent.com/Heretek-AI/ProxmoxVE/refs/heads/main}"
source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/build.func)
# Copyright (c) 2021-2026 community-scripts ORG
# Author: BillyOutlast
# License: MIT | https://github.com/Heretek-AI/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/infiniflow/ragflow
@@ -1,9 +1,7 @@
{
"name": "RAGFlow",
"slug": "ragflow",
"categories": [
20
],
"categories": [20],
"date_created": "2026-03-12",
"type": "ct",
"updateable": true,
@@ -75,6 +73,26 @@
{
"text": "Minimum 16GB RAM recommended for production workloads",
"type": "warning"
},
{
"text": "Optional MCP Server (disabled by default): Enable with 'systemctl enable --now ragflow-mcp.service'",
"type": "info"
},
{
"text": "MCP endpoint: http://<IP>:9382 - for AI assistant integration (Claude Desktop, etc.)",
"type": "info"
},
{
"text": "MCP Server requires RAGFlow API key - generate in Settings > API Keys",
"type": "warning"
},
{
"text": "MCP Documentation: https://ragflow.io/docs/launch_mcp_server",
"type": "info"
},
{
"text": "API Key Guide: https://ragflow.io/docs/acquire_ragflow_api_key",
"type": "info"
}
]
}
@@ -12,12 +12,28 @@ setting_up_container
network_check
update_os
# =============================================================================
# RUNTIMES & DATABASE SETUP
# =============================================================================
NODE_VERSION="22" setup_nodejs
# PostgreSQL with pgvector for smart routing
PG_VERSION="17" setup_postgresql
PG_DB_NAME="mcphub" PG_DB_USER="mcphub" PG_DB_EXTENSIONS="pgvector" setup_postgresql_db
# =============================================================================
# INSTALL MCPHUB
# =============================================================================
msg_info "Installing MCPHub"
$STD npm install -g @samanhappy/mcphub
msg_ok "Installed MCPHub"
# =============================================================================
# CONFIGURATION
# =============================================================================
msg_info "Creating Default Configuration"
mkdir -p /opt/mcphub
cat <<EOF >/opt/mcphub/mcp_settings.json
@@ -32,13 +48,18 @@ cat <<EOF >/opt/mcphub/mcp_settings.json
EOF
msg_ok "Created Default Configuration"
# =============================================================================
# CREATE SYSTEMD SERVICE
# =============================================================================
msg_info "Creating Service"
NPM_GLOBAL_BIN="$(npm prefix -g)/bin"
MCPHUB_BIN="${NPM_GLOBAL_BIN}/mcphub"
cat <<EOF >/etc/systemd/system/mcphub.service
[Unit]
Description=MCPHub
After=network.target
After=network.target postgresql.service
Requires=postgresql.service
[Service]
Type=simple
@@ -46,6 +67,7 @@ WorkingDirectory=/opt/mcphub
Environment=NODE_ENV=production
Environment=PORT=3000
Environment=MCPHUB_SETTING_PATH=/opt/mcphub/mcp_settings.json
Environment=DATABASE_URL="postgresql://${PG_DB_USER}:${PG_DB_PASS}@localhost:5432/${PG_DB_NAME}"
ExecStart=${MCPHUB_BIN}
Restart=always
RestartSec=5
@@ -56,6 +78,10 @@ EOF
systemctl enable -q --now mcphub
msg_ok "Created Service"
# =============================================================================
# CLEANUP & FINALIZATION
# =============================================================================
motd_ssh
customize
cleanup_lxc
@@ -1,5 +1,6 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: BillyOutlast
# License: MIT | https://github.com/Heretek-AI/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/infiniflow/ragflow
@@ -111,6 +112,14 @@ max_allowed_packet=1073741824
max_connections=900
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
# Connection handling optimizations
wait_timeout=28800
interactive_timeout=28800
connect_timeout=60
# Buffer pool for performance
innodb_buffer_pool_size=2G
# Connection queue
back_log=900
EOF
systemctl restart mariadb
msg_ok "Configured MariaDB"
@@ -232,6 +241,10 @@ MINIO_PASS=$(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c16)
curl -fsSL https://dl.min.io/server/minio/release/linux-amd64/minio -o /usr/local/bin/minio
chmod +x /usr/local/bin/minio
# Download MinIO Client (mc) for bucket management
curl -fsSL https://dl.min.io/client/mc/release/linux-amd64/mc -o /usr/local/bin/mc
chmod +x /usr/local/bin/mc
# Create MinIO directories
mkdir -p /var/lib/minio/data
@@ -265,6 +278,18 @@ for i in {1..30}; do
fi
sleep 1
done
# Create ragflow bucket using MinIO Client
msg_info "Creating MinIO Bucket"
for i in {1..30}; do
if /usr/local/bin/mc alias set local http://localhost:9000 rag_flow "${MINIO_PASS}" 2>/dev/null; then
break
fi
sleep 1
done
/usr/local/bin/mc mb local/ragflow --ignore-existing 2>/dev/null || true
msg_ok "Created MinIO Bucket"
msg_ok "MinIO Installed"
# ==============================================================================
@@ -272,7 +297,7 @@ msg_ok "MinIO Installed"
# ==============================================================================
msg_info "Downloading RAGFlow"
fetch_and_deploy_gh_release "ragflow" "infiniflow/ragflow" "tarball" "v0.24.0" "/opt/ragflow"
fetch_and_deploy_gh_release "ragflow" "infiniflow/ragflow" "tarball" "latest" "/opt/ragflow"
msg_ok "Downloaded RAGFlow"
# ==============================================================================
@@ -309,8 +334,8 @@ mysql:
password: '${MARIADB_DB_PASS}'
host: 'localhost'
port: 3306
max_connections: 900
stale_timeout: 300
max_connections: 100
stale_timeout: 60
max_allowed_packet: 1073741824
minio:
user: 'rag_flow'
@@ -358,7 +383,7 @@ SVR_WEB_HTTPS_PORT=443
SVR_HTTP_PORT=9380
ADMIN_SVR_HTTP_PORT=9381
SVR_MCP_PORT=9382
RAGFLOW_IMAGE=infiniflow/ragflow:v0.24.0
RAGFLOW_IMAGE=infiniflow/ragflow:latest
TZ=UTC
REGISTER_ENABLED=1
THREAD_POOL_MAX_WORKERS=128
@@ -377,6 +402,7 @@ cat <<EOF >/etc/systemd/system/ragflow-server.service
Description=RAGFlow Backend Server
After=network.target mariadb.service elasticsearch.service redis-server.service minio.service
Requires=mariadb.service elasticsearch.service redis-server.service minio.service
Wants=network-online.target
[Service]
Type=simple
@@ -384,7 +410,10 @@ WorkingDirectory=/opt/ragflow
Environment=PYTHONPATH=/opt/ragflow
Environment=LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu/
Environment=NLTK_DATA=/opt/ragflow/nltk_data
ExecStartPre=/bin/sleep 10
# Wait for services to be fully ready
ExecStartPre=/bin/sleep 15
# Health check for MariaDB
ExecStartPre=/bin/bash -c 'for i in {1..30}; do mysqladmin ping -h localhost --silent && break; sleep 1; done'
ExecStart=/usr/local/bin/uv run --index-strategy unsafe-best-match python api/ragflow_server.py
Restart=on-failure
RestartSec=10
@@ -417,6 +446,43 @@ LimitNOFILE=65535
WantedBy=multi-user.target
EOF
# ==============================================================================
# OPTIONAL: MCP SERVER SERVICE
# ==============================================================================
# The MCP (Model Context Protocol) server is optional and provides integration
# with AI assistants like Claude Desktop. It runs on port 9382 by default.
# To enable: systemctl enable --now ragflow-mcp.service
msg_info "Creating Optional MCP Server Service"
cat <<EOF >/etc/systemd/system/ragflow-mcp.service
[Unit]
Description=RAGFlow MCP Server (Model Context Protocol)
After=network.target mariadb.service elasticsearch.service redis-server.service minio.service ragflow-server.service
Requires=mariadb.service elasticsearch.service redis-server.service minio.service ragflow-server.service
[Service]
Type=simple
WorkingDirectory=/opt/ragflow
Environment=PYTHONPATH=/opt/ragflow
Environment=LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu/
Environment=NLTK_DATA=/opt/ragflow/nltk_data
ExecStartPre=/bin/sleep 15
ExecStart=/usr/local/bin/uv run --index-strategy unsafe-best-match python mcp/server/server.py --host=0.0.0.0 --port=9382 --base-url=http://127.0.0.1:9380
Restart=on-failure
RestartSec=10
TimeoutStartSec=300
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
EOF
# MCP service is disabled by default - users must opt-in
systemctl disable ragflow-mcp.service 2>/dev/null || true
msg_ok "Created Optional MCP Server Service (disabled by default)"
msg_ok "Created Systemd Services"
# ==============================================================================
@@ -500,11 +566,23 @@ msg_ok "Nginx Frontend Configured"
# ==============================================================================
msg_info "Starting RAGFlow Services"
systemctl enable -q ragflow-server
systemctl start ragflow-server
sleep 5
systemctl enable -q ragflow-task-executor
systemctl start ragflow-task-executor
msg_ok "Started RAGFlow Services"
# ==============================================================================
# Reloading Nginx and services after installation
# ==============================================================================
msg_info "Reloading Nginx and Services"
systemctl reload nginx
systemctl restart nginx
msg_ok "Reloaded Nginx"
# ==============================================================================
# FINALIZATION
# ==============================================================================
@@ -538,3 +616,8 @@ echo -e "${TAB}- Configure your LLM API key in the web interface"
echo -e "${TAB}- Default uses CPU for document processing"
echo -e "${TAB}- For GPU acceleration, additional configuration required"
echo -e "${TAB}- Elasticsearch may take 1-2 minutes to fully initialize"
echo -e ""
echo -e "${INFO}${YW} Optional MCP Server (for AI assistant integration):${CL}"
echo -e "${TAB}- MCP endpoint: http://${LOCAL_IP}:9382"
echo -e "${TAB}- Enable with: systemctl enable --now ragflow-mcp.service"
echo -e "${TAB}- Requires RAGFlow API key from web interface"
@@ -420,6 +420,12 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
</details>
## 2026-03-19
### 📂 Github
- Upstream sync 20260319 160030 [@BillyOutlast](https://github.com/BillyOutlast) ([#69](https://github.com/Heretek-AI/ProxmoxVE/pull/69))
## 2026-03-18
### 🆕 New Scripts
@@ -1,6 +1,7 @@
#!/usr/bin/env bash
COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL:-https://raw.githubusercontent.com/Heretek-AI/ProxmoxVE/refs/heads/main}"
source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/build.func)
# Copyright (c) 2021-2026 community-scripts ORG
# Author: BillyOutlast
# License: MIT | https://github.com/Heretek-AI/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/infiniflow/ragflow
@@ -30,7 +31,14 @@ function update_script() {
fi
if check_for_gh_release "ragflow" "infiniflow/ragflow"; then
# Check if MCP service is enabled before stopping
MCP_WAS_ENABLED=false
if systemctl is-enabled ragflow-mcp.service 2>/dev/null | grep -q "enabled"; then
MCP_WAS_ENABLED=true
fi
msg_info "Stopping Services"
systemctl stop ragflow-mcp || true
systemctl stop ragflow-task-executor || true
systemctl stop ragflow-server || true
msg_ok "Stopped Services"
@@ -68,6 +76,23 @@ function update_script() {
systemctl start ragflow-server
sleep 5
systemctl start ragflow-task-executor
# Restart MCP service if it was enabled before update
if [[ "$MCP_WAS_ENABLED" == "true" ]]; then
msg_info "Restarting MCP Server"
systemctl start ragflow-mcp || true
msg_ok "Restarted MCP Server"
fi
# Verify MinIO bucket exists (fixes NoSuchBucket errors)
msg_info "Verifying MinIO Bucket"
MINIO_PASS=$(grep -oP 'MINIO_PASSWORD=\K[^"]+' /opt/ragflow/.env 2>/dev/null || grep -oP 'password: \K[^"]+' /opt/ragflow/conf/service_conf.yaml 2>/dev/null | head -1)
if [[ -n "$MINIO_PASS" ]] && [[ -x /usr/local/bin/mc ]]; then
/usr/local/bin/mc alias set local http://localhost:9000 rag_flow "${MINIO_PASS}" 2>/dev/null || true
/usr/local/bin/mc mb local/ragflow --ignore-existing 2>/dev/null || true
fi
msg_ok "Verified MinIO Bucket"
msg_ok "Started Services"
msg_ok "Updated successfully!"
fi
@@ -83,3 +108,8 @@ echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:80${CL}"
echo -e "${INFO}${YW} API endpoint: http://${IP}:9380${CL}"
echo -e ""
echo -e "${INFO}${YW} Optional MCP Server (for AI assistant integration):${CL}"
echo -e "${TAB}- MCP endpoint: http://${IP}:9382"
echo -e "${TAB}- Enable with: systemctl enable --now ragflow-mcp.service"
echo -e "${TAB}- Requires RAGFlow API key from web interface"
@@ -1,12 +1,12 @@
{
"generated": "2026-03-16T18:42:54Z",
"generated": "2026-03-19T06:41:06Z",
"versions": [
{
"slug": "agregarr",
"repo": "agregarr/agregarr",
"version": "v2.4.1",
"version": "v2.4.2",
"pinned": false,
"date": "2026-03-05T06:43:40Z"
"date": "2026-03-17T08:57:42Z"
},
{
"slug": "lemonade",
@@ -18,9 +18,9 @@
{
"slug": "llamacpp",
"repo": "ggml-org/llama.cpp",
"version": "b8373",
"version": "b8417",
"pinned": false,
"date": "2026-03-16T10:55:12Z"
"date": "2026-03-19T05:37:13Z"
},
{
"slug": "localai",
@@ -57,6 +57,13 @@
"pinned": true,
"date": "2026-02-10T09:27:14Z"
},
{
"slug": "skillserver",
"repo": "mudler/skillserver",
"version": "v0.0.4",
"pinned": false,
"date": "2026-01-28T10:30:17Z"
},
{
"slug": "wakapi",
"repo": "muety/wakapi",
@@ -86,16 +86,16 @@ variables() {
# Set default URL if not already defined by the calling script
COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL:-https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main}"
source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/api.func)
source <(curl -fsSL ${COMMUNITY_SCRIPTS_URL}/misc/api.func)
if command -v curl >/dev/null 2>&1; then
source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/core.func)
source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/error_handler.func)
source <(curl -fsSL ${COMMUNITY_SCRIPTS_URL}/misc/core.func)
source <(curl -fsSL ${COMMUNITY_SCRIPTS_URL}/misc/error_handler.func)
load_functions
catch_errors
elif command -v wget >/dev/null 2>&1; then
source <(wget -qO- "${COMMUNITY_SCRIPTS_URL}"/misc/core.func)
source <(wget -qO- "${COMMUNITY_SCRIPTS_URL}"/misc/error_handler.func)
source <(wget -qO- ${COMMUNITY_SCRIPTS_URL}/misc/core.func)
source <(wget -qO- ${COMMUNITY_SCRIPTS_URL}/misc/error_handler.func)
load_functions
catch_errors
fi
@@ -1489,7 +1489,7 @@ _build_vars_diff() {
# Build a temporary <app>.vars file from current advanced settings
_build_current_app_vars_tmp() {
tmpf="$(mktemp /tmp/"${NSAPP:-app}".vars.new.XXXXXX)"
tmpf="$(mktemp /tmp/${NSAPP:-app}.vars.new.XXXXXX)"
# NET/GW
_net="${NET:-}"
@@ -3440,7 +3440,7 @@ msg_menu() {
# - Otherwise: shows update/setting menu and runs update_script with cleanup
# ------------------------------------------------------------------------------
start() {
source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/tools.func)
source <(curl -fsSL ${COMMUNITY_SCRIPTS_URL}/misc/tools.func)
if command -v pveversion >/dev/null 2>&1; then
install_script || return 0
return 0
@@ -4136,7 +4136,7 @@ EOF'
# that sends "configuring" status AFTER the host already reported "failed"
export CONTAINER_INSTALLING=true
lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/install/"${var_install}".sh)"
lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL ${COMMUNITY_SCRIPTS_URL}/install/${var_install}.sh)"
local lxc_exit=$?
unset CONTAINER_INSTALLING
@@ -4253,7 +4253,7 @@ EOF'
else
msg_dev "Container ${CTID} kept for debugging"
fi
exit "$install_exit_code"
exit $install_exit_code
fi
# Prompt user for cleanup with 60s timeout
@@ -4449,7 +4449,7 @@ EOF'
echo -e "${BFR}${CM}${GN}MOTD/SSH ready - SSH into container: ssh root@${ct_ip}${CL}"
fi
fi
exit "$install_exit_code"
exit $install_exit_code
;;
3)
# Retry with verbose mode (full rebuild)
@@ -4514,7 +4514,7 @@ EOF'
# Re-run install script in existing container (don't destroy/recreate)
set +Eeuo pipefail
trap - ERR
lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/install/"${var_install}".sh)"
lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL ${COMMUNITY_SCRIPTS_URL}/install/${var_install}.sh)"
local apt_retry_exit=$?
set -Eeuo pipefail
trap 'error_handler' ERR
@@ -4630,7 +4630,7 @@ EOF'
if [[ "$handled" == false ]]; then
echo -e "\n${TAB}${YW}Invalid option. Container ${CTID} kept.${CL}"
exit "$install_exit_code"
exit $install_exit_code
fi
;;
esac
@@ -4651,7 +4651,7 @@ EOF'
# Restore default job-control signal handling before exit
trap - TSTP TTIN TTOU
exit "$install_exit_code"
exit $install_exit_code
fi
# Re-enable error handling after successful install or recovery menu completion
@@ -5015,7 +5015,7 @@ create_lxc_container() {
msg_ok "LXC stack upgraded."
if [[ "$do_retry" == "yes" ]]; then
msg_info "Retrying container creation after upgrade"
if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "$PCT_OPTIONS" >>"$LOGFILE" 2>&1; then
if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then
msg_ok "Container created successfully after upgrade."
return 0
else
@@ -5270,7 +5270,7 @@ create_lxc_container() {
fi
fi
TEMPLATE_PATH="$(pvesm path "$TEMPLATE_STORAGE":vztmpl/"$TEMPLATE" 2>/dev/null || true)"
TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)"
if [[ -z "$TEMPLATE_PATH" ]]; then
TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg)
[[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE"
@@ -5334,7 +5334,7 @@ create_lxc_container() {
TEMPLATE_SOURCE="online"
fi
TEMPLATE_PATH="$(pvesm path "$TEMPLATE_STORAGE":vztmpl/"$TEMPLATE" 2>/dev/null || true)"
TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)"
if [[ -z "$TEMPLATE_PATH" ]]; then
TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg)
[[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE"
@@ -5530,7 +5530,7 @@ create_lxc_container() {
msg_debug "Logfile: $LOGFILE"
# First attempt (PCT_OPTIONS is a multi-line string, use it directly)
if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "$PCT_OPTIONS" >"$LOGFILE" 2>&1; then
if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >"$LOGFILE" 2>&1; then
msg_debug "Container creation failed on ${TEMPLATE_STORAGE}. Checking error..."
# Check if template issue - retry with fresh download
@@ -5542,7 +5542,7 @@ create_lxc_container() {
fi
# Retry after repair
if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "$PCT_OPTIONS" >>"$LOGFILE" 2>&1; then
if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then
# Fallback to local storage if not already on local
if [[ "$TEMPLATE_STORAGE" != "local" ]]; then
msg_info "Retrying container creation with fallback to local storage"
@@ -5555,7 +5555,7 @@ create_lxc_container() {
else
msg_ok "Trying local storage fallback"
fi
if ! pct create "$CTID" "local:vztmpl/${TEMPLATE}" "$PCT_OPTIONS" >>"$LOGFILE" 2>&1; then
if ! pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then
# Local fallback also failed - check for LXC stack version issue
if grep -qiE 'unsupported .* version' "$LOGFILE"; then
msg_warn "pct reported 'unsupported version' – LXC stack might be too old for this template"
@@ -5578,7 +5578,7 @@ create_lxc_container() {
msg_error "Container creation failed. See $LOGFILE"
if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
set -x
pct create "$CTID" "local:vztmpl/${TEMPLATE}" "$PCT_OPTIONS" 2>&1 | tee -a "$LOGFILE"
pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS 2>&1 | tee -a "$LOGFILE"
set +x
fi
_flush_pct_log
@@ -5610,7 +5610,7 @@ create_lxc_container() {
msg_error "Container creation failed. See $LOGFILE"
if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
set -x
pct create "$CTID" "local:vztmpl/${TEMPLATE}" "$PCT_OPTIONS" 2>&1 | tee -a "$LOGFILE"
pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS 2>&1 | tee -a "$LOGFILE"
set +x
fi
_flush_pct_log
@@ -116,18 +116,12 @@ export default function Page() {
<span className="steel-text glitch">Heretek-AI</span>
</h1>
<div className="max-w-2xl gap-2 flex flex-col text-center sm:text-lg text-sm leading-relaxed tracking-tight text-muted-foreground md:text-xl">
<p className="text-blood-400 font-semibold animate-pulse">Uncompliant scripts, made quicker.</p>
<p className="text-blood-400 font-semibold animate-pulse">Forbidden scripts, forged in innovation.</p>
<p>
Scripts that don't meet the strict guidelines of the official{" "}
<a
href="https://github.com/community-scripts/ProxmoxVE"
target="_blank"
rel="noopener noreferrer"
className="underline text-blood-400 hover:text-blood-300 transition-colors"
>
Community-Scripts
</a>{" "}
repository, but are updated faster and built with flexibility in mind.
Scripts that embrace the heretical path—beyond the rigid dogma of the orthodox repositories. Updated
with
<span className="text-corruption-400"> relentless speed</span>, built for those who dare to
<span className="text-blood-400"> innovate</span>.
</p>
</div>
</div>
@@ -31,7 +31,7 @@ function MousePosition(): MousePosition {
}
// Mechanicus-themed particle presets
type ParticleTheme = "default" | "rust" | "corruption" | "brass" | "mechanicus";
type ParticleTheme = "default" | "rust" | "corruption" | "brass" | "mechanicus" | "heretek";
type ParticlesProps = {
className?: string;
@@ -68,6 +68,10 @@ const MECHANICUS_THEMES: Record<ParticleTheme, { colors: string[]; defaultColor:
colors: ["#b45309", "#15803d", "#ca8a04", "#92400e", "#166534"],
defaultColor: "#b45309",
},
heretek: {
colors: ["#7f1d1d", "#991b1b", "#dc2626", "#450a0a", "#1e3a5f"],
defaultColor: "#dc2626",
},
};
function hexToRgb(hex: string): number[] {
@@ -76,7 +80,7 @@ function hexToRgb(hex: string): number[] {
if (hex.length === 3) {
hex = hex
.split("")
.map(char => char + char)
.map((char) => char + char)
.join("");
}
@@ -223,12 +227,7 @@ const Particles: React.FC<ParticlesProps> = ({
const clearContext = () => {
if (context.current) {
context.current.clearRect(
0,
0,
canvasSize.current.w,
canvasSize.current.h,
);
context.current.clearRect(0, 0, canvasSize.current.w, canvasSize.current.h);
}
};
@@ -241,15 +240,8 @@ const Particles: React.FC<ParticlesProps> = ({
}
};
const remapValue = (
value: number,
start1: number,
end1: number,
start2: number,
end2: number,
): number => {
const remapped
= ((value - start1) * (end2 - start2)) / (end1 - start1) + start2;
const remapValue = (value: number, start1: number, end1: number, start2: number, end2: number): number => {
const remapped = ((value - start1) * (end2 - start2)) / (end1 - start1) + start2;
return remapped > 0 ? remapped : 0;
};
@@ -264,35 +256,28 @@ const Particles: React.FC<ParticlesProps> = ({
canvasSize.current.h - circle.y - circle.translateY - circle.size, // distance from bottom edge
];
const closestEdge = edge.reduce((a, b) => Math.min(a, b));
const remapClosestEdge = Number.parseFloat(
remapValue(closestEdge, 0, 20, 0, 1).toFixed(2),
);
const remapClosestEdge = Number.parseFloat(remapValue(closestEdge, 0, 20, 0, 1).toFixed(2));
if (remapClosestEdge > 1) {
circle.alpha += 0.02;
if (circle.alpha > circle.targetAlpha) {
circle.alpha = circle.targetAlpha;
}
}
else {
} else {
circle.alpha = circle.targetAlpha * remapClosestEdge;
}
circle.x += circle.dx + vx;
circle.y += circle.dy + vy;
circle.translateX
+= (mouse.current.x / (staticity / circle.magnetism) - circle.translateX)
/ ease;
circle.translateY
+= (mouse.current.y / (staticity / circle.magnetism) - circle.translateY)
/ ease;
circle.translateX += (mouse.current.x / (staticity / circle.magnetism) - circle.translateX) / ease;
circle.translateY += (mouse.current.y / (staticity / circle.magnetism) - circle.translateY) / ease;
drawCircle(circle, true);
// circle gets out of the canvas
if (
circle.x < -circle.size
|| circle.x > canvasSize.current.w + circle.size
|| circle.y < -circle.size
|| circle.y > canvasSize.current.h + circle.size
circle.x < -circle.size ||
circle.x > canvasSize.current.w + circle.size ||
circle.y < -circle.size ||
circle.y > canvasSize.current.h + circle.size
) {
// remove the circle from the array
circles.current.splice(i, 1);
@@ -306,11 +291,7 @@ const Particles: React.FC<ParticlesProps> = ({
};
return (
<div
className={cn("pointer-events-none", className)}
ref={canvasContainerRef}
aria-hidden="true"
>
<div className={cn("pointer-events-none", className)} ref={canvasContainerRef} aria-hidden="true">
<canvas ref={canvasRef} className="size-full" />
</div>
);
@@ -86,16 +86,16 @@ variables() {
# Set default URL if not already defined by the calling script
COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL:-https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main}"
source <(curl -fsSL ${COMMUNITY_SCRIPTS_URL}/misc/api.func)
source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/api.func)
if command -v curl >/dev/null 2>&1; then
source <(curl -fsSL ${COMMUNITY_SCRIPTS_URL}/misc/core.func)
source <(curl -fsSL ${COMMUNITY_SCRIPTS_URL}/misc/error_handler.func)
source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/core.func)
source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/error_handler.func)
load_functions
catch_errors
elif command -v wget >/dev/null 2>&1; then
source <(wget -qO- ${COMMUNITY_SCRIPTS_URL}/misc/core.func)
source <(wget -qO- ${COMMUNITY_SCRIPTS_URL}/misc/error_handler.func)
source <(wget -qO- "${COMMUNITY_SCRIPTS_URL}"/misc/core.func)
source <(wget -qO- "${COMMUNITY_SCRIPTS_URL}"/misc/error_handler.func)
load_functions
catch_errors
fi
@@ -1489,7 +1489,7 @@ _build_vars_diff() {
# Build a temporary <app>.vars file from current advanced settings
_build_current_app_vars_tmp() {
tmpf="$(mktemp /tmp/${NSAPP:-app}.vars.new.XXXXXX)"
tmpf="$(mktemp /tmp/"${NSAPP:-app}".vars.new.XXXXXX)"
# NET/GW
_net="${NET:-}"
@@ -3440,7 +3440,7 @@ msg_menu() {
# - Otherwise: shows update/setting menu and runs update_script with cleanup
# ------------------------------------------------------------------------------
start() {
source <(curl -fsSL ${COMMUNITY_SCRIPTS_URL}/misc/tools.func)
source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/tools.func)
if command -v pveversion >/dev/null 2>&1; then
install_script || return 0
return 0
@@ -3765,6 +3765,10 @@ $PCT_OPTIONS_STRING"
done
fi
fi
# Add /dev/kfd for AMD ROCm compute support
if [[ -e /dev/kfd ]]; then
AMD_DEVICES+=("/dev/kfd")
fi
fi
# Check for NVIDIA GPU - look for NVIDIA vendor ID [10de]
@@ -4132,7 +4136,7 @@ EOF'
# that sends "configuring" status AFTER the host already reported "failed"
export CONTAINER_INSTALLING=true
lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL ${COMMUNITY_SCRIPTS_URL}/install/${var_install}.sh)"
lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/install/"${var_install}".sh)"
local lxc_exit=$?
unset CONTAINER_INSTALLING
@@ -4249,7 +4253,7 @@ EOF'
else
msg_dev "Container ${CTID} kept for debugging"
fi
exit $install_exit_code
exit "$install_exit_code"
fi
# Prompt user for cleanup with 60s timeout
@@ -4445,7 +4449,7 @@ EOF'
echo -e "${BFR}${CM}${GN}MOTD/SSH ready - SSH into container: ssh root@${ct_ip}${CL}"
fi
fi
exit $install_exit_code
exit "$install_exit_code"
;;
3)
# Retry with verbose mode (full rebuild)
@@ -4510,7 +4514,7 @@ EOF'
# Re-run install script in existing container (don't destroy/recreate)
set +Eeuo pipefail
trap - ERR
lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL ${COMMUNITY_SCRIPTS_URL}/install/${var_install}.sh)"
lxc-attach -n "$CTID" -- bash -c "$(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/install/"${var_install}".sh)"
local apt_retry_exit=$?
set -Eeuo pipefail
trap 'error_handler' ERR
@@ -4626,7 +4630,7 @@ EOF'
if [[ "$handled" == false ]]; then
echo -e "\n${TAB}${YW}Invalid option. Container ${CTID} kept.${CL}"
exit $install_exit_code
exit "$install_exit_code"
fi
;;
esac
@@ -4647,7 +4651,7 @@ EOF'
# Restore default job-control signal handling before exit
trap - TSTP TTIN TTOU
exit $install_exit_code
exit "$install_exit_code"
fi
# Re-enable error handling after successful install or recovery menu completion
@@ -5011,7 +5015,7 @@ create_lxc_container() {
msg_ok "LXC stack upgraded."
if [[ "$do_retry" == "yes" ]]; then
msg_info "Retrying container creation after upgrade"
if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then
if pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "$PCT_OPTIONS" >>"$LOGFILE" 2>&1; then
msg_ok "Container created successfully after upgrade."
return 0
else
@@ -5266,7 +5270,7 @@ create_lxc_container() {
fi
fi
TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)"
TEMPLATE_PATH="$(pvesm path "$TEMPLATE_STORAGE":vztmpl/"$TEMPLATE" 2>/dev/null || true)"
if [[ -z "$TEMPLATE_PATH" ]]; then
TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg)
[[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE"
@@ -5330,7 +5334,7 @@ create_lxc_container() {
TEMPLATE_SOURCE="online"
fi
TEMPLATE_PATH="$(pvesm path $TEMPLATE_STORAGE:vztmpl/$TEMPLATE 2>/dev/null || true)"
TEMPLATE_PATH="$(pvesm path "$TEMPLATE_STORAGE":vztmpl/"$TEMPLATE" 2>/dev/null || true)"
if [[ -z "$TEMPLATE_PATH" ]]; then
TEMPLATE_BASE=$(awk -v s="$TEMPLATE_STORAGE" '$1==s {f=1} f && /path/ {print $2; exit}' /etc/pve/storage.cfg)
[[ -n "$TEMPLATE_BASE" ]] && TEMPLATE_PATH="$TEMPLATE_BASE/template/cache/$TEMPLATE"
@@ -5526,7 +5530,7 @@ create_lxc_container() {
msg_debug "Logfile: $LOGFILE"
# First attempt (PCT_OPTIONS is a multi-line string, use it directly)
if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >"$LOGFILE" 2>&1; then
if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "$PCT_OPTIONS" >"$LOGFILE" 2>&1; then
msg_debug "Container creation failed on ${TEMPLATE_STORAGE}. Checking error..."
# Check if template issue - retry with fresh download
@@ -5538,7 +5542,7 @@ create_lxc_container() {
fi
# Retry after repair
if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then
if ! pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "$PCT_OPTIONS" >>"$LOGFILE" 2>&1; then
# Fallback to local storage if not already on local
if [[ "$TEMPLATE_STORAGE" != "local" ]]; then
msg_info "Retrying container creation with fallback to local storage"
@@ -5551,7 +5555,7 @@ create_lxc_container() {
else
msg_ok "Trying local storage fallback"
fi
if ! pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS >>"$LOGFILE" 2>&1; then
if ! pct create "$CTID" "local:vztmpl/${TEMPLATE}" "$PCT_OPTIONS" >>"$LOGFILE" 2>&1; then
# Local fallback also failed - check for LXC stack version issue
if grep -qiE 'unsupported .* version' "$LOGFILE"; then
msg_warn "pct reported 'unsupported version' – LXC stack might be too old for this template"
@@ -5574,7 +5578,7 @@ create_lxc_container() {
msg_error "Container creation failed. See $LOGFILE"
if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
set -x
pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS 2>&1 | tee -a "$LOGFILE"
pct create "$CTID" "local:vztmpl/${TEMPLATE}" "$PCT_OPTIONS" 2>&1 | tee -a "$LOGFILE"
set +x
fi
_flush_pct_log
@@ -5606,7 +5610,7 @@ create_lxc_container() {
msg_error "Container creation failed. See $LOGFILE"
if whiptail --yesno "pct create failed.\nDo you want to enable verbose debug mode and view detailed logs?" 12 70; then
set -x
pct create "$CTID" "local:vztmpl/${TEMPLATE}" $PCT_OPTIONS 2>&1 | tee -a "$LOGFILE"
pct create "$CTID" "local:vztmpl/${TEMPLATE}" "$PCT_OPTIONS" 2>&1 | tee -a "$LOGFILE"
set +x
fi
_flush_pct_log
@@ -5664,33 +5668,33 @@ create_lxc_container() {
description() {
IP=$(pct exec "$CTID" ip a s dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1)
# Generate LXC Description
# Generate LXC Description - Heretek-AI themed
DESCRIPTION=$(
cat <<EOF
<div align='center'>
<a href='https://Helper-Scripts.com' target='_blank' rel='noopener noreferrer'>
<img src='https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo-81x112.png' alt='Logo' style='width:81px;height:112px;'/>
<a href='https://github.com/Heretek-AI/ProxmoxVE' target='_blank' rel='noopener noreferrer'>
<img src='https://raw.githubusercontent.com/Heretek-AI/ProxmoxVE/main/misc/images/logo-81x112.png' alt='Heretek-AI Logo' style='width:81px;height:112px;'/>
</a>
<h2 style='font-size: 24px; margin: 20px 0;'>${APP} LXC</h2>
<h2 style='font-size: 24px; margin: 20px 0; color: #dc2626;'>${APP} LXC</h2>
<p style='margin: 16px 0;'>
<a href='https://ko-fi.com/community_scripts' target='_blank' rel='noopener noreferrer'>
<img src='https://img.shields.io/badge/&#x2615;-Buy us a coffee-blue' alt='spend Coffee' />
<a href='https://discord.gg/3AnUqsXnmK' target='_blank' rel='noopener noreferrer'>
<img src='https://img.shields.io/badge/Discord-Join%20Server-5865F2?logo=discord&logoColor=white' alt='Discord' />
</a>
</p>
<span style='margin: 0 10px;'>
<i class="fa fa-github fa-fw" style="color: #f5f5f5;"></i>
<a href='https://github.com/community-scripts/ProxmoxVE' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>GitHub</a>
<i class="fa fa-github fa-fw" style="color: #dc2626;"></i>
<a href='https://github.com/Heretek-AI/ProxmoxVE' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #dc2626;'>GitHub</a>
</span>
<span style='margin: 0 10px;'>
<i class="fa fa-comments fa-fw" style="color: #f5f5f5;"></i>
<a href='https://github.com/community-scripts/ProxmoxVE/discussions' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>Discussions</a>
<i class="fa fa-comments fa-fw" style="color: #dc2626;"></i>
<a href='https://github.com/Heretek-AI/ProxmoxVE/discussions' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #dc2626;'>Discussions</a>
</span>
<span style='margin: 0 10px;'>
<i class="fa fa-exclamation-circle fa-fw" style="color: #f5f5f5;"></i>
<a href='https://github.com/community-scripts/ProxmoxVE/issues' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #00617f;'>Issues</a>
<i class="fa fa-exclamation-circle fa-fw" style="color: #dc2626;"></i>
<a href='https://github.com/Heretek-AI/ProxmoxVE/issues' target='_blank' rel='noopener noreferrer' style='text-decoration: none; color: #dc2626;'>Issues</a>
</span>
</div>
EOF
@@ -35,26 +35,22 @@ export default function Page() {
const [color, setColor] = useState("#000000");
useEffect(() => {
// Use Mechanicus-themed colors for particles
setColor(theme === "dark" ? "#b45309" : "#92400e");
// Use Heretek-themed colors - Blood Red
setColor(theme === "dark" ? "#dc2626" : "#b91c1c");
}, [theme]);
return (
<>
<div className="w-full mt-16 relative overflow-hidden">
{/* Mechanicus Background Effects */}
<div className="pointer-events-none absolute inset-0 scan-lines opacity-5" />
{/* Heretek Background Effects */}
<div className="pointer-events-none absolute inset-0 scan-lines opacity-10" />
<div className="pointer-events-none absolute inset-0 noise-overlay" />
{/* Themed Particles */}
<Particles
className="absolute inset-0 -z-40"
quantity={100}
ease={80}
color={color}
theme="mechanicus"
refresh
/>
{/* Circuit board pattern overlay */}
<div className="pointer-events-none absolute inset-0 circuit-board opacity-5" />
{/* Themed Particles - Blood Red */}
<Particles className="absolute inset-0 -z-40" quantity={100} ease={80} color={color} theme="heretek" refresh />
<div className="container mx-auto relative z-10">
<div className="flex h-[60vh] flex-col items-center justify-center gap-4 py-20 lg:py-32">
@@ -64,16 +60,14 @@ export default function Page() {
<AnimatedGradientText>
<div
className={cn(
`absolute inset-0 block size-full animate-gradient bg-gradient-to-r from-rust-500/50 via-corruption-500/50 to-brass-500/50 bg-[length:var(--bg-size)_100%] [border-radius:inherit] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]`,
`absolute inset-0 block size-full animate-gradient bg-gradient-to-r from-blood-500/50 via-corruption-500/50 to-blood-400/50 bg-[length:var(--bg-size)_100%] [border-radius:inherit] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)]`,
`p-px ![mask-composite:subtract]`,
)}
/>
⚙️
{" "}
<Separator className="mx-2 h-4" orientation="vertical" />
⛧ <Separator className="mx-2 h-4" orientation="vertical" />
<span
className={cn(
`animate-gradient bg-gradient-to-r from-rust-400 via-corruption-400 to-brass-400 bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent`,
`animate-gradient bg-gradient-to-r from-blood-400 via-corruption-400 to-blood-300 bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent`,
`inline`,
)}
>
@@ -82,27 +76,25 @@ export default function Page() {
</AnimatedGradientText>
</div>
</DialogTrigger>
<DialogContent className="mechanicus-panel">
<DialogContent className="heretek-panel border-blood-500/30">
<DialogHeader>
<DialogTitle className="font-[family-name:var(--font-cinzel)] brass-text">
<DialogTitle className="font-[family-name:var(--font-cinzel)] text-blood-400 glitch">
Praise the Omnissiah!
</DialogTitle>
<DialogDescription>
<DialogDescription className="text-muted-foreground">
A big thank you to tteck and the many contributors who have made this project possible. Your hard
work is truly appreciated by the entire Proxmox community!
</DialogDescription>
</DialogHeader>
<CardFooter className="flex flex-col gap-2">
<Button className="w-full" variant="mechanicus" asChild>
<Button className="w-full" variant="heretek" asChild>
<a
href="https://github.com/tteck"
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center"
>
<FaGithub className="mr-2 h-4 w-4" />
{" "}
Tteck's GitHub
<FaGithub className="mr-2 h-4 w-4" /> Tteck's GitHub
</a>
</Button>
<Button className="w-full" variant="forge" asChild>
@@ -112,9 +104,7 @@ export default function Page() {
rel="noopener noreferrer"
className="flex items-center justify-center"
>
<ExternalLink className="mr-2 h-4 w-4" />
{" "}
Proxmox Helper Scripts
<ExternalLink className="mr-2 h-4 w-4" /> Proxmox Helper Scripts
</a>
</Button>
</CardFooter>
@@ -123,36 +113,27 @@ export default function Page() {
<div className="flex flex-col gap-4">
<h1 className="max-w-2xl text-center text-3xl font-semibold tracking-tighter md:text-7xl font-[family-name:var(--font-cinzel)]">
<span className="brass-text">Heretek-AI</span>
<span className="steel-text glitch">Heretek-AI</span>
</h1>
<div className="max-w-2xl gap-2 flex flex-col text-center sm:text-lg text-sm leading-relaxed tracking-tight text-muted-foreground md:text-xl">
<p className="text-rust-300 font-semibold">
Uncompliant scripts, made quicker.
</p>
<p className="text-blood-400 font-semibold animate-pulse">Uncompliant scripts, made quicker.</p>
<p>
Scripts that don't meet the strict guidelines of the official
{" "}
Scripts that don't meet the strict guidelines of the official{" "}
<a
href="https://github.com/community-scripts/ProxmoxVE"
target="_blank"
rel="noopener noreferrer"
className="underline text-corruption-400 hover:text-corruption-300 transition-colors"
className="underline text-blood-400 hover:text-blood-300 transition-colors"
>
Community-Scripts
</a>
{" "}
</a>{" "}
repository, but are updated faster and built with flexibility in mind.
</p>
</div>
</div>
<div className="flex flex-row gap-3">
<Link href="/scripts">
<Button
size="lg"
variant="mechanicus"
Icon={CustomArrowRightIcon}
iconPlacement="right"
>
<Button size="lg" variant="heretek" Icon={CustomArrowRightIcon} iconPlacement="right">
View Scripts
</Button>
</Link>
@@ -160,7 +141,7 @@ export default function Page() {
<Button
size="lg"
variant="outline"
className="border-rust-500/50 hover:border-rust-400 hover:bg-rust-500/10"
className="border-blood-500/50 hover:border-blood-400 hover:bg-blood-500/10"
>
Browse Categories
</Button>
@@ -183,7 +164,7 @@ export default function Page() {
<div className="max-w-4xl mx-auto px-4">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold tracking-tighter md:text-5xl mb-4 font-[family-name:var(--font-cinzel)]">
<span className="brass-text">Frequently Asked Questions</span>
<span className="steel-text">Frequently Asked Questions</span>
</h2>
<p className="text-muted-foreground text-lg">
Find answers to common questions about our Proxmox VE scripts
@@ -36,7 +36,7 @@ const features = [
export function FeatureCards() {
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{features.map(feature => (
{features.map((feature) => (
<Link
key={feature.title}
href={feature.href}
@@ -44,20 +44,37 @@ export function FeatureCards() {
rel={feature.external ? "noopener noreferrer" : undefined}
className="group"
>
<Card className="h-full transition-all duration-300 hover:border-rust-500/50 hover:shadow-lg hover:shadow-rust-500/10 rust-border">
<CardHeader>
<div className="mb-2 text-rust-400 group-hover:text-rust-300 transition-colors duration-300 group-hover:animate-heretic-glow">
<Card className="h-full transition-all duration-300 hover:border-blood-500/60 hover:shadow-lg hover:shadow-blood-500/20 heretek-card glitch relative overflow-hidden">
{/* Glitch overlay on hover */}
<div className="absolute inset-0 bg-gradient-to-r from-blood-500/5 via-transparent to-blood-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
{/* Scan line effect */}
<div className="absolute inset-0 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-blood-500/5 to-transparent animate-scan-line" />
</div>
<CardHeader className="relative z-10">
<div className="mb-2 text-blood-400 group-hover:text-blood-300 transition-colors duration-300 group-hover:animate-heretic-glow">
{feature.icon}
</div>
<CardTitle className="text-lg font-[family-name:var(--font-cinzel)] group-hover:text-brass-400 transition-colors duration-300">
{feature.title}
<CardTitle className="text-lg font-[family-name:var(--font-cinzel)] group-hover:text-blood-400 transition-colors duration-300 relative">
<span className="relative inline-block">
{feature.title}
{/* Underline glitch effect */}
<span className="absolute bottom-0 left-0 w-full h-px bg-gradient-to-r from-transparent via-blood-500 to-transparent transform scale-x-0 group-hover:scale-x-100 transition-transform duration-300" />
</span>
</CardTitle>
</CardHeader>
<CardContent>
<CardContent className="relative z-10">
<CardDescription className="text-muted-foreground group-hover:text-foreground/80 transition-colors duration-300">
{feature.description}
</CardDescription>
</CardContent>
{/* Corner accent */}
<div className="absolute top-0 right-0 w-8 h-8 overflow-hidden">
<div className="absolute top-0 right-0 w-16 h-16 bg-gradient-to-br from-blood-500/20 to-transparent transform rotate-45 translate-x-8 -translate-y-8 group-hover:translate-x-4 group-hover:-translate-y-4 transition-transform duration-300" />
</div>
</Card>
</Link>
))}
@@ -8,17 +8,16 @@ import { buttonVariants } from "./ui/button";
export default function Footer() {
return (
<div className="supports-backdrop-blur:bg-background/90 mt-auto border-t w-full flex justify-between border-border bg-background/40 py-4 backdrop-blur-lg rust-border metal-surface">
<div className="supports-backdrop-blur:bg-background/90 mt-auto border-t w-full flex justify-between border-border bg-background/40 py-4 backdrop-blur-lg heretek-panel">
<div className="mx-6 w-full flex justify-between text-xs sm:text-sm text-muted-foreground">
<div className="flex items-center">
<p>
Website built by the community. The source code is available on
{" "}
Website built by the community. The source code is available on{" "}
<Link
href={`https://github.com/Heretek-AI/${basePath}/tree/main/frontend`}
target="_blank"
rel="noreferrer"
className="font-semibold underline-offset-2 duration-300 hover:underline text-rust-400 hover:text-rust-300"
className="font-semibold underline-offset-2 duration-300 hover:underline text-blood-400 hover:text-blood-300"
data-umami-event="View Website Source Code on Github"
>
GitHub
@@ -29,35 +28,50 @@ export default function Footer() {
<div className="sm:flex hidden gap-2">
<Link
href="/scripts"
className={cn(buttonVariants({ variant: "link" }), "text-muted-foreground flex items-center gap-2 hover:text-rust-400 transition-colors duration-300")}
className={cn(
buttonVariants({ variant: "link" }),
"text-muted-foreground flex items-center gap-2 hover:text-blood-400 transition-colors duration-300",
)}
>
<FileJson className="h-4 w-4" />
Scripts
</Link>
<Link
href="/categories"
className={cn(buttonVariants({ variant: "link" }), "text-muted-foreground flex items-center gap-2 hover:text-rust-400 transition-colors duration-300")}
className={cn(
buttonVariants({ variant: "link" }),
"text-muted-foreground flex items-center gap-2 hover:text-blood-400 transition-colors duration-300",
)}
>
<FolderOpen className="h-4 w-4" />
Categories
</Link>
<Link
href="/community"
className={cn(buttonVariants({ variant: "link" }), "text-muted-foreground flex items-center gap-2 hover:text-rust-400 transition-colors duration-300")}
className={cn(
buttonVariants({ variant: "link" }),
"text-muted-foreground flex items-center gap-2 hover:text-blood-400 transition-colors duration-300",
)}
>
<MessageCircle className="h-4 w-4" />
Community
</Link>
<Link
href="/json-editor"
className={cn(buttonVariants({ variant: "link" }), "text-muted-foreground flex items-center gap-2 hover:text-rust-400 transition-colors duration-300")}
className={cn(
buttonVariants({ variant: "link" }),
"text-muted-foreground flex items-center gap-2 hover:text-blood-400 transition-colors duration-300",
)}
>
<FileJson className="h-4 w-4" />
JSON Editor
</Link>
<Link
href="/data"
className={cn(buttonVariants({ variant: "link" }), "text-muted-foreground flex items-center gap-2 hover:text-rust-400 transition-colors duration-300")}
className={cn(
buttonVariants({ variant: "link" }),
"text-muted-foreground flex items-center gap-2 hover:text-blood-400 transition-colors duration-300",
)}
>
<Server className="h-4 w-4" />
API Data
@@ -32,10 +32,9 @@ function Navbar() {
return (
<>
<div
className={`fixed left-0 top-0 z-50 flex w-screen justify-center px-4 xl:px-0 transition-all duration-300 ${isScrolled
? "glass rust-border border-b bg-background/50"
: ""
}`}
className={`fixed left-0 top-0 z-50 flex w-screen justify-center px-4 xl:px-0 transition-all duration-300 ${
isScrolled ? "glass blood-border border-b bg-background/50" : ""
}`}
>
<div className="flex h-20 w-full max-w-[1440px] items-center justify-between sm:flex-row">
<Link
@@ -45,7 +44,7 @@ function Navbar() {
<div className="relative machine-icon">
<Image height={18} unoptimized width={18} alt="logo" src="/ProxmoxVE/logo.png" className="" />
</div>
<span className="font-[family-name:var(--font-cinzel)] tracking-wide text-foreground group-hover:text-rust-400 transition-colors duration-300">
<span className="font-[family-name:var(--font-cinzel)] tracking-wide text-foreground group-hover:text-blood-400 transition-colors duration-300 glitch">
Heretek AI
</span>
</Link>
@@ -56,38 +55,42 @@ function Navbar() {
</Suspense>
</div>
<div className="hidden sm:flex items-center gap-2">
{navbarLinks.filter(link => !link.external).map(({ href, event, icon, text }) => (
<Button
key={event}
variant="ghost"
size="sm"
asChild
className="text-muted-foreground hover:text-rust-400 hover:bg-rust-500/10 transition-colors duration-300"
>
<Link href={href} data-umami-event={event}>
{icon}
<span className="ml-2 hidden lg:inline">{text}</span>
</Link>
</Button>
))}
{navbarLinks
.filter((link) => !link.external)
.map(({ href, event, icon, text }) => (
<Button
key={event}
variant="ghost"
size="sm"
asChild
className="text-muted-foreground hover:text-blood-400 hover:bg-blood-500/10 transition-colors duration-300"
>
<Link href={href} data-umami-event={event}>
{icon}
<span className="ml-2 hidden lg:inline">{text}</span>
</Link>
</Button>
))}
</div>
<div className="flex sm:gap-2">
<CommandMenu />
<GitHubStarsButton username="Heretek-AI" repo="ProxmoxVE" className="hidden md:flex" />
{navbarLinks.filter(link => link.external).map(({ href, event, icon, text, mobileHidden }) => (
<Button
key={event}
variant="ghost"
size="icon"
asChild
className={`text-muted-foreground hover:text-rust-400 hover:bg-rust-500/10 transition-colors duration-300 ${mobileHidden ? "hidden lg:flex" : ""}`}
>
<Link target="_blank" href={href} data-umami-event={event}>
{icon}
<span className="sr-only">{text}</span>
</Link>
</Button>
))}
{navbarLinks
.filter((link) => link.external)
.map(({ href, event, icon, text, mobileHidden }) => (
<Button
key={event}
variant="ghost"
size="icon"
asChild
className={`text-muted-foreground hover:text-blood-400 hover:bg-blood-500/10 transition-colors duration-300 ${mobileHidden ? "hidden lg:flex" : ""}`}
>
<Link target="_blank" href={href} data-umami-event={event}>
{icon}
<span className="sr-only">{text}</span>
</Link>
</Button>
))}
<ThemeToggle />
</div>
</div>
@@ -12,16 +12,12 @@ const buttonVariants = cva(
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
expandIcon:
"group relative text-primary-foreground bg-primary hover:bg-primary/90",
expandIcon: "group relative text-primary-foreground bg-primary hover:bg-primary/90",
ringHover:
"bg-primary text-primary-foreground transition-all duration-300 hover:bg-primary/90 hover:ring-2 hover:ring-primary/90 hover:ring-offset-2",
shine:
@@ -34,17 +30,23 @@ const buttonVariants = cva(
"relative after:absolute after:bg-primary after:bottom-2 after:h-[1px] after:w-2/3 after:origin-bottom-left after:scale-x-100 hover:after:origin-bottom-right hover:after:scale-x-0 after:transition-transform after:ease-in-out after:duration-300",
linkHover2:
"relative after:absolute after:bg-primary after:bottom-2 after:h-[1px] after:w-2/3 after:origin-bottom-right after:scale-x-0 hover:after:origin-bottom-left hover:after:scale-x-100 after:transition-transform after:ease-in-out after:duration-300",
// Mechanicus-themed variants
mechanicus:
"bg-gradient-to-r from-rust-600 to-rust-700 text-rust-100 border border-brass-500/30 hover:from-rust-500 hover:to-rust-600 hover:border-brass-400/50 hover:shadow-[0_0_15px_hsl(28_70%_45%_/_0.3)] transition-all duration-300",
brass:
"bg-gradient-to-r from-brass-500 to-brass-600 text-brass-100 border border-brass-400/30 hover:from-brass-400 hover:to-brass-500 hover:shadow-[0_0_15px_hsl(43_70%_50%_/_0.3)] transition-all duration-300",
// Heretek-themed variants - Blood & Steel
heretek:
"bg-gradient-to-r from-blood-600 to-blood-700 text-blood-100 border border-blood-500/30 hover:from-blood-500 hover:to-blood-600 hover:border-blood-400/50 hover:shadow-[0_0_15px_hsl(0_80%_50%_/_0.4)] transition-all duration-300",
blood:
"bg-gradient-to-r from-blood-500 to-blood-600 text-blood-100 border border-blood-400/30 hover:from-blood-400 hover:to-blood-500 hover:shadow-[0_0_20px_hsl(0_85%_55%_/_0.5)] transition-all duration-300",
void: "bg-gradient-to-r from-void-700 to-void-800 text-void-100 border border-void-500/30 hover:from-void-600 hover:to-void-700 hover:shadow-[0_0_15px_hsl(0_0%_20%_/_0.4)] transition-all duration-300",
steel:
"bg-gradient-to-r from-steel-600 to-steel-700 text-steel-100 border border-steel-500/30 hover:from-steel-500 hover:to-steel-600 hover:shadow-[0_0_10px_hsl(0_15%_35%_/_0.3)] transition-all duration-300",
corruption:
"bg-gradient-to-r from-corruption-600 to-corruption-700 text-corruption-100 border border-corruption-500/30 hover:from-corruption-500 hover:to-corruption-600 hover:shadow-[0_0_15px_hsl(145_60%_40%_/_0.3)] hover:animate-corrupted-pulse transition-all duration-300",
"bg-gradient-to-r from-corruption-600 to-corruption-700 text-corruption-100 border border-corruption-500/30 hover:from-corruption-500 hover:to-corruption-600 hover:shadow-[0_0_15px_hsl(0_70%_45%_/_0.4)] hover:animate-corrupted-pulse transition-all duration-300",
glitch:
"bg-gradient-to-r from-blood-600 to-void-700 text-void-100 border border-blood-500/30 hover:from-blood-500 hover:to-void-600 hover:border-blood-400/50 hover:shadow-[0_0_20px_hsl(0_80%_50%_/_0.5)] animate-glitch transition-all duration-300",
mechanicus:
"bg-gradient-to-r from-blood-600 to-blood-700 text-blood-100 border border-blood-500/30 hover:from-blood-500 hover:to-blood-600 hover:border-blood-400/50 hover:shadow-[0_0_15px_hsl(0_80%_50%_/_0.4)] transition-all duration-300",
forge:
"bg-gradient-to-r from-copper-600 to-copper-700 text-copper-100 border border-copper-500/30 hover:from-copper-500 hover:to-copper-600 hover:shadow-[0_0_20px_hsl(25_70%_45%_/_0.4)] transition-all duration-300",
iron:
"bg-gradient-to-r from-iron-700 to-iron-800 text-iron-100 border border-iron-500/30 hover:from-iron-600 hover:to-iron-700 hover:shadow-[0_0_10px_hsl(30_20%_30%_/_0.3)] transition-all duration-300",
"bg-gradient-to-r from-steel-600 to-blood-700 text-steel-100 border border-steel-500/30 hover:from-steel-500 hover:to-blood-600 hover:shadow-[0_0_20px_hsl(0_70%_45%_/_0.5)] transition-all duration-300",
iron: "bg-gradient-to-r from-iron-700 to-iron-800 text-iron-100 border border-iron-500/30 hover:from-iron-600 hover:to-iron-700 hover:shadow-[0_0_10px_hsl(0_10%_30%_/_0.3)] transition-all duration-300",
},
size: {
default: "h-10 px-4 py-2",
@@ -73,33 +75,16 @@ type IconRefProps = {
export type ButtonProps = {
asChild?: boolean;
} & React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof buttonVariants>;
} & React.ButtonHTMLAttributes<HTMLButtonElement> &
VariantProps<typeof buttonVariants>;
export type ButtonIconProps = IconProps | IconRefProps;
const Button = React.forwardRef<
HTMLButtonElement,
ButtonProps & ButtonIconProps
>(
(
{
className,
variant,
size,
asChild = false,
Icon,
iconPlacement,
...props
},
ref,
) => {
const Button = React.forwardRef<HTMLButtonElement, ButtonProps & ButtonIconProps>(
({ className, variant, size, asChild = false, Icon, iconPlacement, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
>
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props}>
{Icon && iconPlacement === "left" && (
<div className="group-hover:translate-x-100 w-0 translate-x-[0%] pr-0 opacity-0 transition-all duration-200 group-hover:w-5 group-hover:pr-2 group-hover:opacity-100">
<Icon />
@@ -4,33 +4,33 @@
@layer base {
:root {
/* Heretek Light Theme - Worn Metal */
--background: 30 15% 12%;
--foreground: 35 20% 85%;
--card: 30 12% 10%;
--card-foreground: 35 20% 85%;
--popover: 30 15% 8%;
--popover-foreground: 35 20% 85%;
--primary: 28 80% 45%;
--primary-foreground: 30 15% 8%;
--secondary: 35 30% 25%;
--secondary-foreground: 35 20% 85%;
--muted: 30 20% 20%;
--muted-foreground: 35 15% 55%;
--accent: 145 65% 35%;
--accent-foreground: 145 10% 95%;
--destructive: 0 75% 45%;
--destructive-foreground: 0 10% 95%;
--border: 30 25% 22%;
--input: 30 25% 18%;
--ring: 28 80% 45%;
/* Heretek Light Theme - Dark Metal with Red Accents */
--background: 0 0% 8%;
--foreground: 0 0% 88%;
--card: 0 0% 6%;
--card-foreground: 0 0% 88%;
--popover: 0 0% 4%;
--popover-foreground: 0 0% 88%;
--primary: 0 75% 50%;
--primary-foreground: 0 0% 4%;
--secondary: 0 10% 18%;
--secondary-foreground: 0 0% 85%;
--muted: 0 8% 14%;
--muted-foreground: 0 5% 55%;
--accent: 0 70% 45%;
--accent-foreground: 0 0% 95%;
--destructive: 0 80% 50%;
--destructive-foreground: 0 0% 95%;
--border: 0 12% 20%;
--input: 0 10% 12%;
--ring: 0 75% 50%;
--radius: 0.25rem;
/* Heretek Chart Colors - Rust & Corruption */
--chart-1: 28 75% 45%;
--chart-2: 35 60% 40%;
--chart-3: 145 55% 35%;
--chart-4: 180 40% 30%;
--chart-5: 0 60% 40%;
/* Heretek Chart Colors - Blood & Steel */
--chart-1: 0 75% 50%;
--chart-2: 0 60% 35%;
--chart-3: 0 50% 45%;
--chart-4: 0 40% 30%;
--chart-5: 0 65% 40%;
}
::selection {
@@ -39,32 +39,32 @@
}
.dark {
/* Heretek Dark Theme - Corrupted Forge */
--background: 30 20% 4%;
--foreground: 35 15% 80%;
--card: 30 18% 6%;
--card-foreground: 35 15% 80%;
--popover: 30 20% 3%;
--popover-foreground: 35 15% 80%;
--primary: 28 85% 50%;
--primary-foreground: 30 25% 5%;
--secondary: 35 25% 15%;
--secondary-foreground: 35 15% 80%;
--muted: 30 20% 12%;
--muted-foreground: 35 12% 50%;
--accent: 145 70% 40%;
--accent-foreground: 145 15% 95%;
--destructive: 0 70% 40%;
--destructive-foreground: 0 15% 95%;
--border: 30 30% 18%;
--input: 30 25% 12%;
--ring: 28 85% 50%;
/* Heretek Chart Colors - Corruption Spread */
--chart-1: 28 80% 50%;
--chart-2: 35 55% 35%;
--chart-3: 145 60% 38%;
--chart-4: 60 45% 30%;
--chart-5: 0 65% 42%;
/* Heretek Dark Theme - The Void Stares Back */
--background: 0 0% 3%;
--foreground: 0 0% 85%;
--card: 0 0% 5%;
--card-foreground: 0 0% 85%;
--popover: 0 0% 2%;
--popover-foreground: 0 0% 85%;
--primary: 0 80% 55%;
--primary-foreground: 0 0% 3%;
--secondary: 0 8% 12%;
--secondary-foreground: 0 0% 82%;
--muted: 0 6% 10%;
--muted-foreground: 0 5% 50%;
--accent: 0 75% 50%;
--accent-foreground: 0 0% 95%;
--destructive: 0 85% 55%;
--destructive-foreground: 0 0% 95%;
--border: 0 15% 15%;
--input: 0 10% 8%;
--ring: 0 80% 55%;
/* Heretek Chart Colors - Corruption Spreads */
--chart-1: 0 80% 55%;
--chart-2: 0 65% 40%;
--chart-3: 0 55% 45%;
--chart-4: 0 45% 35%;
--chart-5: 0 70% 45%;
}
}
@@ -75,7 +75,9 @@
body {
@apply bg-background text-foreground;
font-feature-settings: "liga" 1, "calt" 1;
font-feature-settings:
"liga" 1,
"calt" 1;
}
}
@@ -83,45 +85,36 @@
-ms-overflow-style: none;
}
/* Heretek Scrollbar - Corrupted Metal */
/* Heretek Scrollbar - Dark Steel */
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-track {
background: hsl(30 20% 8%);
border-left: 1px solid hsl(30 30% 18%);
background: hsl(0 0% 4%);
border-left: 1px solid hsl(0 15% 12%);
}
::-webkit-scrollbar-thumb {
background: linear-gradient(180deg,
hsl(28 70% 35%) 0%,
hsl(35 50% 25%) 50%,
hsl(28 70% 35%) 100%);
background: linear-gradient(180deg, hsl(0 60% 30%) 0%, hsl(0 40% 20%) 50%, hsl(0 60% 30%) 100%);
border-radius: 4px;
border: 1px solid hsl(30 30% 20%);
border: 1px solid hsl(0 20% 15%);
}
::-webkit-scrollbar-thumb:hover {
background: linear-gradient(180deg,
hsl(28 80% 42%) 0%,
hsl(35 60% 32%) 50%,
hsl(28 80% 42%) 100%);
background: linear-gradient(180deg, hsl(0 70% 40%) 0%, hsl(0 50% 28%) 50%, hsl(0 70% 40%) 100%);
}
/* Glass Effect - Corrupted Viewport */
.glass {
backdrop-filter: blur(12px) saturate(120%);
-webkit-backdrop-filter: blur(12px) saturate(120%);
background: linear-gradient(135deg,
hsl(30 20% 8% / 0.85) 0%,
hsl(30 25% 12% / 0.75) 100%);
border: 1px solid hsl(30 30% 18% / 0.6);
background: linear-gradient(135deg, hsl(0 0% 5% / 0.9) 0%, hsl(0 5% 8% / 0.8) 100%);
border: 1px solid hsl(0 20% 15% / 0.6);
}
/* Heretek Corruption Effects */
@layer utilities {
/* Noise Texture Overlay */
.noise-overlay {
position: relative;
@@ -132,82 +125,88 @@
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
opacity: 0.03;
opacity: 0.04;
pointer-events: none;
mix-blend-mode: overlay;
}
/* Rust Border Effect */
.rust-border {
/* Blood Border Effect */
.blood-border {
border: 1px solid transparent;
background:
linear-gradient(hsl(30 20% 6%), hsl(30 20% 6%)) padding-box,
linear-gradient(135deg,
hsl(28 70% 30%) 0%,
hsl(35 40% 20%) 25%,
hsl(28 60% 25%) 50%,
hsl(35 35% 18%) 75%,
hsl(28 70% 30%) 100%) border-box;
linear-gradient(hsl(0 0% 5%), hsl(0 0% 5%)) padding-box,
linear-gradient(
135deg,
hsl(0 70% 35%) 0%,
hsl(0 50% 20%) 25%,
hsl(0 60% 30%) 50%,
hsl(0 40% 18%) 75%,
hsl(0 70% 35%) 100%
)
border-box;
}
/* Corruption Glow */
/* Corruption Glow - Red */
.corruption-glow {
box-shadow:
0 0 20px hsl(145 60% 30% / 0.15),
0 0 40px hsl(145 50% 25% / 0.1),
inset 0 0 20px hsl(145 40% 20% / 0.05);
0 0 20px hsl(0 70% 40% / 0.2),
0 0 40px hsl(0 60% 30% / 0.15),
inset 0 0 20px hsl(0 50% 25% / 0.08);
}
/* Brass Text Effect */
.brass-text {
background: linear-gradient(180deg,
hsl(43 70% 55%) 0%,
hsl(35 60% 40%) 50%,
hsl(43 65% 45%) 100%);
/* Steel Text Effect */
.steel-text {
background: linear-gradient(180deg, hsl(0 0% 75%) 0%, hsl(0 5% 55%) 50%, hsl(0 0% 65%) 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
/* Copper Accent */
.copper-accent {
background: linear-gradient(135deg,
hsl(28 75% 45%) 0%,
hsl(25 65% 35%) 50%,
hsl(28 70% 40%) 100%);
/* Blood Accent */
.blood-accent {
background: linear-gradient(135deg, hsl(0 75% 50%) 0%, hsl(0 65% 40%) 50%, hsl(0 75% 45%) 100%);
}
/* Glitch Animation */
/* Enhanced Glitch Animation */
.glitch {
animation: glitch 4s infinite;
animation: glitch 3s infinite;
}
@keyframes glitch {
0%,
90%,
88%,
100% {
transform: translate(0);
filter: none;
}
89% {
transform: translate(-2px, 1px);
filter: hue-rotate(90deg) saturate(1.5);
}
90% {
transform: translate(2px, -1px);
filter: hue-rotate(-90deg) saturate(1.5);
}
91% {
transform: translate(-1px, 1px);
filter: hue-rotate(90deg);
transform: translate(-1px, -1px);
filter: hue-rotate(45deg) brightness(1.2);
}
92% {
transform: translate(1px, -1px);
filter: hue-rotate(-90deg);
transform: translate(1px, 1px);
filter: none;
}
93% {
transform: translate(-1px, -1px);
filter: hue-rotate(45deg);
transform: translate(-2px, 2px);
filter: hue-rotate(-45deg) saturate(1.3);
}
94% {
transform: translate(1px, 1px);
transform: translate(2px, -2px);
filter: none;
}
}
@@ -217,93 +216,95 @@
content: "";
position: absolute;
inset: 0;
background: repeating-linear-gradient(0deg,
transparent,
transparent 2px,
hsl(30 20% 2% / 0.3) 2px,
hsl(30 20% 2% / 0.3) 4px);
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
hsl(0 0% 2% / 0.4) 2px,
hsl(0 0% 2% / 0.4) 4px
);
pointer-events: none;
opacity: 0.5;
opacity: 0.6;
}
/* Flicker Animation */
.flicker {
animation: flicker 8s infinite;
animation: flicker 6s infinite;
}
@keyframes flicker {
0%,
100% {
opacity: 1;
}
90% {
opacity: 1;
}
91% {
opacity: 0.7;
}
92% {
opacity: 1;
}
93% {
opacity: 0.8;
opacity: 0.85;
}
94% {
opacity: 1;
}
96% {
95% {
opacity: 0.9;
}
97% {
96% {
opacity: 1;
}
}
/* Corrupted Pulse */
/* Corrupted Pulse - Red */
.corrupted-pulse {
animation: corrupted-pulse 3s ease-in-out infinite;
animation: corrupted-pulse 2.5s ease-in-out infinite;
}
@keyframes corrupted-pulse {
0%,
100% {
box-shadow:
0 0 10px hsl(145 50% 25% / 0.2),
0 0 20px hsl(145 40% 20% / 0.1);
0 0 10px hsl(0 65% 40% / 0.25),
0 0 20px hsl(0 55% 30% / 0.15);
}
50% {
box-shadow:
0 0 15px hsl(145 60% 30% / 0.3),
0 0 30px hsl(145 50% 25% / 0.15),
0 0 45px hsl(145 40% 20% / 0.1);
0 0 15px hsl(0 75% 45% / 0.35),
0 0 30px hsl(0 65% 35% / 0.2),
0 0 45px hsl(0 55% 25% / 0.1);
}
}
/* Metal Surface Gradient */
/* Dark Metal Surface Gradient */
.metal-surface {
background:
linear-gradient(145deg,
hsl(30 15% 15%) 0%,
hsl(30 20% 10%) 50%,
hsl(30 15% 12%) 100%);
background: linear-gradient(145deg, hsl(0 0% 10%) 0%, hsl(0 5% 6%) 50%, hsl(0 0% 8%) 100%);
box-shadow:
inset 1px 1px 0 hsl(30 20% 20%),
inset -1px -1px 0 hsl(30 25% 5%);
inset 1px 1px 0 hsl(0 10% 15%),
inset -1px -1px 0 hsl(0 15% 3%);
}
/* Tech Heresy Warning */
/* Heresy Warning - Enhanced */
.heresy-warning {
border-left: 3px solid hsl(0 70% 40%);
background: linear-gradient(90deg,
hsl(0 50% 10% / 0.3) 0%,
transparent 100%);
border-left: 3px solid hsl(0 80% 50%);
background: linear-gradient(90deg, hsl(0 60% 15% / 0.4) 0%, transparent 100%);
}
/* ===== NEW MECHANICUS EFFECTS ===== */
/* ===== ENHANCED HERETEK EFFECTS ===== */
/* Pulsing Circuit Lines */
/* Pulsing Circuit Lines - Red */
.circuit-pulse {
position: relative;
overflow: hidden;
@@ -313,11 +314,8 @@
content: "";
position: absolute;
inset: 0;
background: linear-gradient(90deg,
transparent 0%,
hsl(145 60% 35% / 0.15) 50%,
transparent 100%);
animation: circuit-flow 3s linear infinite;
background: linear-gradient(90deg, transparent 0%, hsl(0 70% 45% / 0.2) 50%, transparent 100%);
animation: circuit-flow 2.5s linear infinite;
}
@keyframes circuit-flow {
@@ -330,19 +328,15 @@
}
}
/* Rusted Metal Texture */
.rusted-metal {
background:
linear-gradient(145deg,
hsl(30 15% 12%) 0%,
hsl(28 20% 8%) 50%,
hsl(30 12% 10%) 100%);
/* Dark Steel Texture */
.dark-steel {
background: linear-gradient(145deg, hsl(0 0% 8%) 0%, hsl(0 5% 5%) 50%, hsl(0 0% 6%) 100%);
box-shadow:
inset 2px 2px 4px hsl(30 20% 18%),
inset -2px -2px 4px hsl(30 25% 5%);
inset 2px 2px 4px hsl(0 10% 12%),
inset -2px -2px 4px hsl(0 15% 2%);
}
/* Tech-Priest Data Stream Effect */
/* Data Stream Effect - Red */
.data-stream {
position: relative;
overflow: hidden;
@@ -352,12 +346,14 @@
content: "";
position: absolute;
inset: 0;
background: repeating-linear-gradient(0deg,
transparent,
transparent 2px,
hsl(145 50% 30% / 0.05) 2px,
hsl(145 50% 30% / 0.05) 4px);
animation: stream-scroll 20s linear infinite;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
hsl(0 60% 40% / 0.06) 2px,
hsl(0 60% 40% / 0.06) 4px
);
animation: stream-scroll 15s linear infinite;
pointer-events: none;
}
@@ -371,7 +367,7 @@
}
}
/* Omnissiah's Blessing - Machine Spirit Glow */
/* Machine Spirit Glow - Red */
.machine-spirit {
position: relative;
}
@@ -380,39 +376,34 @@
content: "";
position: absolute;
inset: -2px;
background: linear-gradient(45deg,
hsl(28 80% 50% / 0.1),
hsl(43 70% 50% / 0.1),
hsl(28 80% 50% / 0.1));
background: linear-gradient(45deg, hsl(0 80% 50% / 0.15), hsl(0 70% 40% / 0.1), hsl(0 80% 50% / 0.15));
border-radius: inherit;
animation: spirit-pulse 4s ease-in-out infinite;
animation: spirit-pulse 3s ease-in-out infinite;
z-index: -1;
}
@keyframes spirit-pulse {
0%,
100% {
opacity: 0.3;
opacity: 0.4;
transform: scale(1);
}
50% {
opacity: 0.6;
opacity: 0.7;
transform: scale(1.02);
}
}
/* Binary Choir Text Effect */
/* Binary Choir Text Effect - Red */
.binary-choir {
text-shadow:
0 0 10px hsl(145 70% 40% / 0.5),
0 0 20px hsl(145 60% 30% / 0.3);
animation: binary-flicker 5s infinite;
0 0 10px hsl(0 75% 50% / 0.6),
0 0 20px hsl(0 65% 40% / 0.4);
animation: binary-flicker 4s infinite;
}
@keyframes binary-flicker {
0%,
100% {
opacity: 1;
@@ -431,51 +422,51 @@
}
}
/* Noosphere Connection Effect */
/* Noosphere Border - Red/Black */
.noosphere-border {
border: 1px solid transparent;
background:
linear-gradient(hsl(30 20% 6%), hsl(30 20% 6%)) padding-box,
linear-gradient(135deg,
hsl(145 60% 35%) 0%,
hsl(28 70% 40%) 25%,
hsl(43 60% 35%) 50%,
hsl(28 70% 40%) 75%,
hsl(145 60% 35%) 100%) border-box;
linear-gradient(hsl(0 0% 5%), hsl(0 0% 5%)) padding-box,
linear-gradient(
135deg,
hsl(0 70% 45%) 0%,
hsl(0 50% 30%) 25%,
hsl(0 60% 35%) 50%,
hsl(0 50% 25%) 75%,
hsl(0 70% 45%) 100%
)
border-box;
}
/* Mechanicus Panel */
.mechanicus-panel {
background: linear-gradient(145deg,
hsl(30 18% 8%) 0%,
hsl(30 22% 6%) 50%,
hsl(30 18% 10%) 100%);
border: 1px solid hsl(28 50% 20% / 0.5);
/* Heretek Panel */
.heretek-panel {
background: linear-gradient(145deg, hsl(0 5% 6%) 0%, hsl(0 8% 4%) 50%, hsl(0 5% 7%) 100%);
border: 1px solid hsl(0 40% 18% / 0.6);
box-shadow:
inset 0 1px 0 hsl(30 25% 15%),
inset 0 -1px 0 hsl(30 20% 5%),
0 4px 20px hsl(30 20% 5% / 0.3);
inset 0 1px 0 hsl(0 15% 12%),
inset 0 -1px 0 hsl(0 10% 3%),
0 4px 20px hsl(0 0% 3% / 0.4);
}
/* Sacred Machine Icon */
/* Sacred Machine Icon - Red */
.machine-icon {
filter: drop-shadow(0 0 8px hsl(28 70% 45% / 0.4));
filter: drop-shadow(0 0 8px hsl(0 70% 50% / 0.5));
transition: filter 0.3s ease;
}
.machine-icon:hover {
filter: drop-shadow(0 0 12px hsl(28 80% 50% / 0.6));
filter: drop-shadow(0 0 12px hsl(0 80% 55% / 0.7));
}
/* Heretek Input Focus */
/* Heretek Focus - Red */
.heretek-focus:focus {
outline: none;
box-shadow:
0 0 0 2px hsl(28 70% 40%),
0 0 15px hsl(28 60% 35% / 0.3);
0 0 0 2px hsl(0 70% 45%),
0 0 15px hsl(0 60% 40% / 0.4);
}
/* Corrupted Text Glitch */
/* Corrupted Text Glitch - Enhanced */
.text-glitch {
position: relative;
}
@@ -491,113 +482,106 @@
}
.text-glitch::before {
color: hsl(145 70% 40%);
animation: glitch-1 2s infinite linear alternate-reverse;
color: hsl(0 75% 50%);
animation: glitch-1 1.5s infinite linear alternate-reverse;
clip-path: polygon(0 0, 100% 0, 100% 35%, 0 35%);
}
.text-glitch::after {
color: hsl(0 70% 45%);
animation: glitch-2 2s infinite linear alternate-reverse;
color: hsl(0 85% 55%);
animation: glitch-2 1.5s infinite linear alternate-reverse;
clip-path: polygon(0 65%, 100% 65%, 100% 100%, 0 100%);
}
@keyframes glitch-1 {
0%,
100% {
transform: translate(0);
}
20% {
transform: translate(-2px, 2px);
transform: translate(-3px, 2px);
}
40% {
transform: translate(2px, -2px);
transform: translate(3px, -2px);
}
60% {
transform: translate(-1px, 1px);
transform: translate(-2px, 1px);
}
80% {
transform: translate(1px, -1px);
transform: translate(2px, -1px);
}
}
@keyframes glitch-2 {
0%,
100% {
transform: translate(0);
}
20% {
transform: translate(2px, -2px);
transform: translate(3px, -2px);
}
40% {
transform: translate(-2px, 2px);
transform: translate(-3px, 2px);
}
60% {
transform: translate(1px, -1px);
transform: translate(2px, -1px);
}
80% {
transform: translate(-1px, 1px);
transform: translate(-2px, 1px);
}
}
/* Forge Glow - for active elements */
/* Forge Glow - Red */
.forge-glow {
box-shadow:
0 0 10px hsl(28 70% 45% / 0.3),
0 0 20px hsl(28 60% 35% / 0.2),
inset 0 0 10px hsl(28 50% 25% / 0.1);
0 0 10px hsl(0 70% 50% / 0.35),
0 0 20px hsl(0 60% 40% / 0.25),
inset 0 0 10px hsl(0 50% 30% / 0.15);
}
/* Sacred Scroll Effect */
.sacred-scroll {
background:
linear-gradient(180deg,
hsl(30 20% 6%) 0%,
hsl(30 18% 8%) 10%,
hsl(30 20% 8%) 90%,
hsl(30 20% 6%) 100%);
border-top: 1px solid hsl(43 40% 25% / 0.3);
border-bottom: 1px solid hsl(43 40% 25% / 0.3);
background: linear-gradient(180deg, hsl(0 0% 4%) 0%, hsl(0 5% 6%) 10%, hsl(0 0% 5%) 90%, hsl(0 0% 4%) 100%);
border-top: 1px solid hsl(0 30% 20% / 0.4);
border-bottom: 1px solid hsl(0 30% 20% / 0.4);
}
/* Circuit Board Pattern */
/* Circuit Board Pattern - Red */
.circuit-board {
background-image:
linear-gradient(90deg, hsl(145 40% 20% / 0.1) 1px, transparent 1px),
linear-gradient(180deg, hsl(145 40% 20% / 0.1) 1px, transparent 1px);
linear-gradient(90deg, hsl(0 50% 25% / 0.12) 1px, transparent 1px),
linear-gradient(180deg, hsl(0 50% 25% / 0.12) 1px, transparent 1px);
background-size: 20px 20px;
}
/* Rust Particle Animation */
.rust-particles {
/* Blood Particle Animation */
.blood-particles {
position: relative;
overflow: hidden;
}
.rust-particles::after {
.blood-particles::after {
content: "";
position: absolute;
inset: 0;
background-image:
radial-gradient(circle at 20% 30%, hsl(28 60% 40% / 0.3) 1px, transparent 1px),
radial-gradient(circle at 80% 70%, hsl(35 50% 35% / 0.3) 1px, transparent 1px),
radial-gradient(circle at 50% 50%, hsl(28 70% 45% / 0.2) 1px, transparent 1px);
radial-gradient(circle at 20% 30%, hsl(0 70% 45% / 0.35) 1px, transparent 1px),
radial-gradient(circle at 80% 70%, hsl(0 60% 40% / 0.35) 1px, transparent 1px),
radial-gradient(circle at 50% 50%, hsl(0 75% 50% / 0.25) 1px, transparent 1px);
background-size: 100px 100px;
animation: rust-drift 30s linear infinite;
animation: blood-drift 25s linear infinite;
pointer-events: none;
}
@keyframes rust-drift {
@keyframes blood-drift {
0% {
transform: translateY(0) rotate(0deg);
}
@@ -606,4 +590,232 @@
transform: translateY(-100px) rotate(360deg);
}
}
/* Heretek Glitch Text - Advanced */
.heretek-glitch {
position: relative;
animation: heretek-glitch 4s infinite;
}
.heretek-glitch::before,
.heretek-glitch::after {
content: attr(data-text);
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0.8;
}
.heretek-glitch::before {
color: hsl(0 80% 50%);
animation: heretek-glitch-1 0.3s infinite;
clip-path: polygon(0 0, 100% 0, 100% 45%, 0 45%);
transform: translate(-2px, -2px);
}
.heretek-glitch::after {
color: hsl(0 90% 60%);
animation: heretek-glitch-2 0.3s infinite;
clip-path: polygon(0 55%, 100% 55%, 100% 100%, 0 100%);
transform: translate(2px, 2px);
}
@keyframes heretek-glitch {
0%,
85%,
100% {
transform: translate(0);
}
86% {
transform: translate(-2px, 1px);
}
87% {
transform: translate(2px, -1px);
}
88% {
transform: translate(-1px, -1px);
}
89% {
transform: translate(1px, 1px);
}
}
@keyframes heretek-glitch-1 {
0%,
100% {
transform: translate(-2px, -2px);
}
50% {
transform: translate(2px, 2px);
}
}
@keyframes heretek-glitch-2 {
0%,
100% {
transform: translate(2px, 2px);
}
50% {
transform: translate(-2px, -2px);
}
}
/* Void Background */
.void-bg {
background: radial-gradient(ellipse at center, hsl(0 0% 5%) 0%, hsl(0 0% 3%) 50%, hsl(0 0% 1%) 100%);
}
/* Blood Drip Effect */
.blood-drip {
position: relative;
}
.blood-drip::after {
content: "";
position: absolute;
bottom: 0;
left: 50%;
width: 2px;
height: 0;
background: linear-gradient(180deg, hsl(0 80% 50%), hsl(0 70% 40%), transparent);
animation: blood-drip 3s ease-in-out infinite;
transform: translateX(-50%);
}
@keyframes blood-drip {
0%,
100% {
height: 0;
opacity: 0;
}
50% {
height: 10px;
opacity: 1;
}
80% {
height: 15px;
opacity: 0.5;
}
}
/* Static Noise Effect */
.static-noise {
position: relative;
overflow: hidden;
}
.static-noise::before {
content: "";
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
opacity: 0;
animation: static-flash 8s infinite;
pointer-events: none;
}
@keyframes static-flash {
0%,
95%,
100% {
opacity: 0;
}
96% {
opacity: 0.08;
}
97% {
opacity: 0;
}
98% {
opacity: 0.05;
}
99% {
opacity: 0;
}
}
/* Chromatic Aberration */
.chromatic-aberration {
position: relative;
}
.chromatic-aberration::before,
.chromatic-aberration::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
}
.chromatic-aberration::before {
background: inherit;
transform: translate(-2px, 0);
opacity: 0.5;
mix-blend-mode: multiply;
filter: hue-rotate(90deg);
}
.chromatic-aberration::after {
background: inherit;
transform: translate(2px, 0);
opacity: 0.5;
mix-blend-mode: multiply;
filter: hue-rotate(-90deg);
}
/* Terminal Text Effect */
.terminal-text {
font-family: "Courier New", monospace;
text-shadow:
0 0 5px hsl(0 70% 50% / 0.8),
0 0 10px hsl(0 60% 40% / 0.5);
animation: terminal-flicker 0.1s infinite;
}
@keyframes terminal-flicker {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.98;
}
}
/* Heretek Card */
.heretek-card {
background: linear-gradient(145deg, hsl(0 5% 7%) 0%, hsl(0 8% 5%) 50%, hsl(0 5% 8%) 100%);
border: 1px solid hsl(0 30% 18%);
transition: all 0.3s ease;
}
.heretek-card:hover {
border-color: hsl(0 60% 35%);
box-shadow:
0 0 15px hsl(0 70% 40% / 0.2),
0 0 30px hsl(0 60% 30% / 0.1),
inset 0 0 10px hsl(0 50% 20% / 0.05);
}
/* Corruption Spread */
.corruption-spread {
position: relative;
overflow: hidden;
}
.corruption-spread::before {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(circle at var(--x, 50%) var(--y, 50%), hsl(0 70% 40% / 0.15) 0%, transparent 50%);
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
.corruption-spread:hover::before {
opacity: 1;
}
}
@@ -2,19 +2,12 @@
//
import type { Config } from "tailwindcss";
const {
default: flattenColorPalette,
} = require("tailwindcss/lib/util/flattenColorPalette");
const { default: flattenColorPalette } = require("tailwindcss/lib/util/flattenColorPalette");
const svgToDataUri = require("mini-svg-data-uri");
const config = {
darkMode: ["class"],
content: [
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
],
content: ["./pages/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}"],
prefix: "",
theme: {
container: {
@@ -59,71 +52,97 @@ const config = {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
// Heretek Custom Colors
// Heretek Custom Colors - Blood & Steel
blood: {
50: "hsl(0 60% 95%)",
100: "hsl(0 65% 90%)",
200: "hsl(0 70% 80%)",
300: "hsl(0 75% 65%)",
400: "hsl(0 80% 55%)",
500: "hsl(0 85% 45%)",
600: "hsl(0 80% 38%)",
700: "hsl(0 75% 30%)",
800: "hsl(0 70% 22%)",
900: "hsl(0 65% 15%)",
950: "hsl(0 60% 8%)",
},
steel: {
50: "hsl(0 5% 95%)",
100: "hsl(0 8% 88%)",
200: "hsl(0 10% 78%)",
300: "hsl(0 12% 65%)",
400: "hsl(0 15% 50%)",
500: "hsl(0 18% 42%)",
600: "hsl(0 20% 35%)",
700: "hsl(0 18% 28%)",
800: "hsl(0 15% 20%)",
900: "hsl(0 12% 12%)",
950: "hsl(0 10% 6%)",
},
void: {
50: "hsl(0 0% 95%)",
100: "hsl(0 0% 88%)",
200: "hsl(0 0% 78%)",
300: "hsl(0 0% 65%)",
400: "hsl(0 0% 50%)",
500: "hsl(0 0% 42%)",
600: "hsl(0 0% 35%)",
700: "hsl(0 0% 28%)",
800: "hsl(0 0% 20%)",
900: "hsl(0 0% 12%)",
950: "hsl(0 0% 6%)",
},
rust: {
50: "hsl(28 60% 95%)",
100: "hsl(28 60% 90%)",
200: "hsl(28 60% 80%)",
300: "hsl(28 65% 70%)",
400: "hsl(28 70% 55%)",
500: "hsl(28 75% 45%)",
600: "hsl(28 80% 38%)",
700: "hsl(28 75% 30%)",
800: "hsl(28 70% 22%)",
900: "hsl(28 65% 15%)",
950: "hsl(28 60% 8%)",
50: "hsl(0 60% 95%)",
100: "hsl(0 65% 90%)",
200: "hsl(0 70% 80%)",
300: "hsl(0 75% 70%)",
400: "hsl(0 80% 55%)",
500: "hsl(0 85% 45%)",
600: "hsl(0 80% 38%)",
700: "hsl(0 75% 30%)",
800: "hsl(0 70% 22%)",
900: "hsl(0 65% 15%)",
950: "hsl(0 60% 8%)",
},
brass: {
50: "hsl(43 50% 92%)",
100: "hsl(43 55% 85%)",
200: "hsl(43 60% 75%)",
300: "hsl(43 65% 62%)",
400: "hsl(43 70% 50%)",
500: "hsl(43 65% 45%)",
600: "hsl(43 60% 38%)",
700: "hsl(43 55% 30%)",
800: "hsl(43 50% 22%)",
900: "hsl(43 45% 15%)",
950: "hsl(43 40% 8%)",
},
copper: {
50: "hsl(25 50% 92%)",
100: "hsl(25 55% 85%)",
200: "hsl(25 60% 75%)",
300: "hsl(25 65% 62%)",
400: "hsl(25 70% 50%)",
500: "hsl(25 75% 42%)",
600: "hsl(25 80% 35%)",
700: "hsl(25 75% 28%)",
800: "hsl(25 70% 20%)",
900: "hsl(25 65% 14%)",
950: "hsl(25 60% 7%)",
50: "hsl(45 50% 92%)",
100: "hsl(45 55% 85%)",
200: "hsl(45 60% 75%)",
300: "hsl(45 65% 62%)",
400: "hsl(45 70% 50%)",
500: "hsl(45 65% 45%)",
600: "hsl(45 60% 38%)",
700: "hsl(45 55% 30%)",
800: "hsl(45 50% 22%)",
900: "hsl(45 45% 15%)",
950: "hsl(45 40% 8%)",
},
corruption: {
50: "hsl(145 40% 95%)",
100: "hsl(145 45% 88%)",
200: "hsl(145 50% 78%)",
300: "hsl(145 55% 65%)",
400: "hsl(145 60% 50%)",
500: "hsl(145 65% 40%)",
600: "hsl(145 70% 32%)",
700: "hsl(145 65% 25%)",
800: "hsl(145 60% 18%)",
900: "hsl(145 55% 12%)",
950: "hsl(145 50% 6%)",
50: "hsl(0 40% 95%)",
100: "hsl(0 45% 88%)",
200: "hsl(0 50% 78%)",
300: "hsl(0 55% 65%)",
400: "hsl(0 60% 50%)",
500: "hsl(0 65% 40%)",
600: "hsl(0 70% 32%)",
700: "hsl(0 65% 25%)",
800: "hsl(0 60% 18%)",
900: "hsl(0 55% 12%)",
950: "hsl(0 50% 6%)",
},
iron: {
50: "hsl(30 10% 95%)",
100: "hsl(30 12% 88%)",
200: "hsl(30 15% 78%)",
300: "hsl(30 18% 65%)",
400: "hsl(30 20% 50%)",
500: "hsl(30 22% 42%)",
600: "hsl(30 25% 35%)",
700: "hsl(30 22% 28%)",
800: "hsl(30 20% 20%)",
900: "hsl(30 18% 12%)",
950: "hsl(30 15% 6%)",
50: "hsl(0 5% 95%)",
100: "hsl(0 8% 88%)",
200: "hsl(0 10% 78%)",
300: "hsl(0 12% 65%)",
400: "hsl(0 15% 50%)",
500: "hsl(0 18% 42%)",
600: "hsl(0 20% 35%)",
700: "hsl(0 18% 28%)",
800: "hsl(0 15% 20%)",
900: "hsl(0 12% 12%)",
950: "hsl(0 10% 6%)",
},
},
borderRadius: {
@@ -140,11 +159,11 @@ const config = {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
"shine": {
shine: {
from: { backgroundPosition: "200% 0" },
to: { backgroundPosition: "-200% 0" },
},
"gradient": {
gradient: {
to: {
backgroundPosition: "var(--bg-size) 0",
},
@@ -156,11 +175,11 @@ const config = {
"50%": {
"background-position": "100% 100%",
},
"to": {
to: {
"background-position": "0% 0%",
},
},
"moveHorizontal": {
moveHorizontal: {
"0%": {
transform: "translateX(-50%) translateY(-10%)",
},
@@ -171,7 +190,7 @@ const config = {
transform: "translateX(-50%) translateY(-10%)",
},
},
"moveInCircle": {
moveInCircle: {
"0%": {
transform: "rotate(0deg)",
},
@@ -182,7 +201,7 @@ const config = {
transform: "rotate(360deg)",
},
},
"moveVertical": {
moveVertical: {
"0%": {
transform: "translateY(-50%)",
},
@@ -193,26 +212,34 @@ const config = {
transform: "translateY(-50%)",
},
},
// Heretek Glitch Animations
"glitch": {
"0%, 90%, 100%": {
// Heretek Glitch Animations - Enhanced
glitch: {
"0%, 88%, 100%": {
transform: "translate(0)",
filter: "none",
},
"91%": {
"89%": {
transform: "translate(-2px, 1px)",
filter: "hue-rotate(90deg) saturate(1.5)",
},
"92%": {
"90%": {
transform: "translate(2px, -1px)",
filter: "hue-rotate(-90deg) saturate(1.5)",
},
"93%": {
"91%": {
transform: "translate(-1px, -1px)",
filter: "hue-rotate(45deg)",
filter: "hue-rotate(45deg) brightness(1.2)",
},
"92%": {
transform: "translate(1px, 1px)",
filter: "none",
},
"93%": {
transform: "translate(-2px, 2px)",
filter: "hue-rotate(-45deg) saturate(1.3)",
},
"94%": {
transform: "translate(1px, 1px)",
transform: "translate(2px, -2px)",
filter: "none",
},
},
@@ -221,29 +248,32 @@ const config = {
"text-shadow": "none",
},
"1%": {
"text-shadow": "-2px 0 hsl(145 70% 40%), 2px 0 hsl(0 70% 45%)",
"text-shadow": "-2px 0 hsl(0 80% 50%), 2px 0 hsl(0 90% 60%)",
},
"2%": {
"text-shadow": "2px 0 hsl(145 70% 40%), -2px 0 hsl(0 70% 45%)",
"text-shadow": "2px 0 hsl(0 80% 50%), -2px 0 hsl(0 90% 60%)",
},
"3%": {
"text-shadow": "none",
},
},
"flicker": {
flicker: {
"0%, 100%": { opacity: "1" },
"90%": { opacity: "1" },
"91%": { opacity: "0.7" },
"92%": { opacity: "1" },
"93%": { opacity: "0.8" },
"93%": { opacity: "0.85" },
"94%": { opacity: "1" },
"96%": { opacity: "0.9" },
"97%": { opacity: "1" },
"95%": { opacity: "0.9" },
"96%": { opacity: "1" },
},
"corrupted-pulse": {
"0%, 100%": {
"box-shadow": "0 0 10px hsl(145 50% 25% / 0.2), 0 0 20px hsl(145 40% 20% / 0.1)",
"box-shadow": "0 0 10px hsl(0 65% 40% / 0.25), 0 0 20px hsl(0 55% 30% / 0.15)",
},
"50%": {
"box-shadow": "0 0 15px hsl(145 60% 30% / 0.3), 0 0 30px hsl(145 50% 25% / 0.15), 0 0 45px hsl(145 40% 20% / 0.1)",
"box-shadow":
"0 0 15px hsl(0 75% 45% / 0.35), 0 0 30px hsl(0 65% 35% / 0.2), 0 0 45px hsl(0 55% 25% / 0.1)",
},
},
"scan-line": {
@@ -254,20 +284,18 @@ const config = {
transform: "translateY(100vh)",
},
},
"rust-fall": {
"0%": {
transform: "translateY(-10%) rotate(0deg)",
"blood-drip": {
"0%, 100%": {
height: "0",
opacity: "0",
},
"10%": {
"50%": {
height: "10px",
opacity: "1",
},
"90%": {
opacity: "1",
},
"100%": {
transform: "translateY(100vh) rotate(720deg)",
opacity: "0",
"80%": {
height: "15px",
opacity: "0.5",
},
},
"metal-shine": {
@@ -280,13 +308,13 @@ const config = {
},
"heretic-glow": {
"0%, 100%": {
"filter": "brightness(1) saturate(1)",
filter: "brightness(1) saturate(1)",
},
"50%": {
"filter": "brightness(1.1) saturate(1.2)",
filter: "brightness(1.15) saturate(1.3)",
},
},
// New Mechanicus Animations
// Heretek Enhanced Animations
"binary-flicker": {
"0%, 100%": { opacity: "1" },
"50%": { opacity: "0.95" },
@@ -298,8 +326,8 @@ const config = {
"100%": { transform: "translateX(100%)" },
},
"spirit-pulse": {
"0%, 100%": { opacity: "0.3", transform: "scale(1)" },
"50%": { opacity: "0.6", transform: "scale(1.02)" },
"0%, 100%": { opacity: "0.4", transform: "scale(1)" },
"50%": { opacity: "0.7", transform: "scale(1.02)" },
},
"stream-scroll": {
"0%": { transform: "translateY(0)" },
@@ -307,50 +335,73 @@ const config = {
},
"forge-pulse": {
"0%, 100%": {
"box-shadow": "0 0 10px hsl(28 70% 45% / 0.3), 0 0 20px hsl(28 60% 35% / 0.2)",
"box-shadow": "0 0 10px hsl(0 70% 50% / 0.35), 0 0 20px hsl(0 60% 40% / 0.25)",
},
"50%": {
"box-shadow": "0 0 15px hsl(28 80% 50% / 0.4), 0 0 30px hsl(28 70% 40% / 0.3), 0 0 45px hsl(28 60% 35% / 0.2)",
"box-shadow":
"0 0 15px hsl(0 80% 55% / 0.45), 0 0 30px hsl(0 70% 45% / 0.35), 0 0 45px hsl(0 60% 35% / 0.2)",
},
},
"rust-drift": {
"blood-drift": {
"0%": { transform: "translateY(0) rotate(0deg)" },
"100%": { transform: "translateY(-100px) rotate(360deg)" },
},
"mechanicus-shimmer": {
"heretek-shimmer": {
"0%": { backgroundPosition: "-200% 0" },
"100%": { backgroundPosition: "200% 0" },
},
"static-flash": {
"0%, 95%, 100%": { opacity: "0" },
"96%": { opacity: "0.08" },
"97%": { opacity: "0" },
"98%": { opacity: "0.05" },
"99%": { opacity: "0" },
},
"terminal-flicker": {
"0%, 100%": { opacity: "1" },
"50%": { opacity: "0.98" },
},
"void-pulse": {
"0%, 100%": {
"box-shadow": "0 0 20px hsl(0 0% 0% / 0.5), inset 0 0 20px hsl(0 0% 0% / 0.3)",
},
"50%": {
"box-shadow": "0 0 40px hsl(0 0% 5% / 0.6), inset 0 0 30px hsl(0 0% 3% / 0.4)",
},
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"shine": "shine 8s ease-in-out infinite",
"gradient": "gradient 8s linear infinite",
shine: "shine 8s ease-in-out infinite",
gradient: "gradient 8s linear infinite",
// Heretek Animations
"glitch": "glitch 4s infinite",
"glitch-text": "glitch-text 8s infinite",
"flicker": "flicker 8s infinite",
"corrupted-pulse": "corrupted-pulse 3s ease-in-out infinite",
"scan-line": "scan-line 8s linear infinite",
"rust-fall": "rust-fall 10s linear infinite",
glitch: "glitch 3s infinite",
"glitch-text": "glitch-text 6s infinite",
flicker: "flicker 6s infinite",
"corrupted-pulse": "corrupted-pulse 2.5s ease-in-out infinite",
"scan-line": "scan-line 6s linear infinite",
"blood-drip": "blood-drip 3s ease-in-out infinite",
"metal-shine": "metal-shine 3s ease-in-out infinite",
"heretic-glow": "heretic-glow 4s ease-in-out infinite",
// New Mechanicus Animations
"binary-flicker": "binary-flicker 5s infinite",
"circuit-flow": "circuit-flow 3s linear infinite",
"spirit-pulse": "spirit-pulse 4s ease-in-out infinite",
"stream-scroll": "stream-scroll 20s linear infinite",
"forge-pulse": "forge-pulse 3s ease-in-out infinite",
"rust-drift": "rust-drift 30s linear infinite",
"mechanicus-shimmer": "mechanicus-shimmer 3s ease-in-out infinite",
// Heretek Enhanced Animations
"binary-flicker": "binary-flicker 4s infinite",
"circuit-flow": "circuit-flow 2.5s linear infinite",
"spirit-pulse": "spirit-pulse 3s ease-in-out infinite",
"stream-scroll": "stream-scroll 15s linear infinite",
"forge-pulse": "forge-pulse 2.5s ease-in-out infinite",
"blood-drift": "blood-drift 25s linear infinite",
"heretek-shimmer": "heretek-shimmer 3s ease-in-out infinite",
"static-flash": "static-flash 8s infinite",
"terminal-flicker": "terminal-flicker 0.1s infinite",
"void-pulse": "void-pulse 4s ease-in-out infinite",
},
backgroundImage: {
// Heretek Background Patterns
"rust-gradient": "linear-gradient(135deg, hsl(28 70% 35%) 0%, hsl(35 50% 25%) 50%, hsl(28 60% 30%) 100%)",
"corruption-gradient": "linear-gradient(180deg, hsl(145 50% 20%) 0%, hsl(145 60% 30%) 50%, hsl(145 50% 25%) 100%)",
"metal-surface": "linear-gradient(145deg, hsl(30 15% 15%) 0%, hsl(30 20% 10%) 50%, hsl(30 15% 12%) 100%)",
"brass-shine": "linear-gradient(90deg, hsl(43 60% 35%) 0%, hsl(43 75% 50%) 50%, hsl(43 60% 35%) 100%)",
"blood-gradient": "linear-gradient(135deg, hsl(0 70% 35%) 0%, hsl(0 50% 25%) 50%, hsl(0 60% 30%) 100%)",
"void-gradient": "linear-gradient(180deg, hsl(0 0% 5%) 0%, hsl(0 0% 3%) 50%, hsl(0 0% 8%) 100%)",
"steel-surface": "linear-gradient(145deg, hsl(0 5% 12%) 0%, hsl(0 8% 8%) 50%, hsl(0 5% 10%) 100%)",
"corruption-spread": "linear-gradient(90deg, hsl(0 0% 5%) 0%, hsl(0 60% 30%) 50%, hsl(0 0% 5%) 100%)",
},
},
},
@@ -382,9 +433,9 @@ const config = {
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" fill="none" stroke="${value}" stroke-width="0.5"><path d="M0 32h20m4 0h16m4 0h20M32 0v20m0 4v16m0 4v20"/><circle cx="32" cy="32" r="4" fill="${value}" fill-opacity="0.3"/><circle cx="24" cy="32" r="2" fill="${value}"/><circle cx="40" cy="32" r="2" fill="${value}"/><circle cx="32" cy="24" r="2" fill="${value}"/><circle cx="32" cy="40" r="2" fill="${value}"/></svg>`,
)}")`,
}),
"bg-rust-texture": (value: any) => ({
"bg-void-texture": (value: any) => ({
backgroundImage: `url("${svgToDataUri(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100"><filter id="rust"><feTurbulence type="fractalNoise" baseFrequency="0.8" numOctaves="4" result="noise"/><feColorMatrix type="saturate" values="0.3" in="noise" result="rust"/></filter><rect width="100" height="100" fill="${value}" style="filter:url(#rust)"/></svg>`,
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100"><filter id="void"><feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="4" result="noise"/><feColorMatrix type="saturate" values="0" in="noise" result="void"/></filter><rect width="100" height="100" fill="${value}" style="filter:url(#void)"/></svg>`,
)}")`,
}),
},
@@ -399,9 +450,7 @@ const config = {
function addVariablesForColors({ addBase, theme }: any) {
const allColors = flattenColorPalette(theme("colors"));
const newVars = Object.fromEntries(
Object.entries(allColors).map(([key, val]) => [`--${key}`, val]),
);
const newVars = Object.fromEntries(Object.entries(allColors).map(([key, val]) => [`--${key}`, val]));
addBase({
":root": newVars,
});
@@ -1,50 +0,0 @@
{
"name": "Unsloth Studio (In Beta)",
"slug": "unsolth-studio",
"categories": [20],
"date_created": "2026-03-18",
"type": "ct",
"updateable": true,
"privileged": false,
"interface_port": 8888,
"documentation": "https://unsloth.ai/docs/new/studio/start",
"website": "https://unsloth.ai/",
"logo": "https://unsloth.ai/favicon.ico",
"config_path": "/opt/unsolth-studio/.env",
"description": "Local, browser-based GUI for fine-tuning LLMs without writing code. Supports QLoRA, LoRA, and full fine-tuning with live training monitoring, GPU stats, and model export to GGUF/Safetensors.",
"install_methods": [
{
"type": "default",
"script": "ct/unsolth-studio.sh",
"resources": {
"cpu": 4,
"ram": 16384,
"hdd": 50,
"os": "Debian",
"version": "13"
}
}
],
"default_credentials": {
"username": null,
"password": null
},
"notes": [
{
"text": "Requires GPU passthrough for training. NVIDIA, AMD (ROCm), and Intel GPUs are supported.",
"type": "info"
},
{
"text": "First launch takes 5-10 minutes to compile llama.cpp binaries.",
"type": "warning"
},
{
"text": "Minimum 16GB RAM recommended for training models up to 7B parameters.",
"type": "info"
},
{
"text": "For AMD GPUs, ensure ROCm is properly configured on the host.",
"type": "info"
}
]
}
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
# Copyright (c) 2021-2026 community-scripts ORG
# Author: cobalt (cobaltgit)
# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
# Source: https://ntfy.sh/
APP="Alpine-ntfy"
var_tags="${var_tags:-notification}"
var_cpu="${var_cpu:-1}"
var_ram="${var_ram:-256}"
var_disk="${var_disk:-2}"
var_os="${var_os:-alpine}"
var_version="${var_version:-3.23}"
var_unprivileged="${var_unprivileged:-1}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /etc/ntfy ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
msg_info "Updating ntfy LXC"
$STD apk -U upgrade
setcap 'cap_net_bind_service=+ep' /usr/bin/ntfy
msg_ok "Updated ntfy LXC"
msg_info "Restarting ntfy"
rc-service ntfy restart
msg_ok "Restarted ntfy"
msg_ok "Updated successfully!"
exit
}
start
build_container
description
msg_ok "Completed successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}${CL}"
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://anytype.io
APP="Anytype-Server"
var_tags="${var_tags:-notes;productivity;sync}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-4096}"
var_disk="${var_disk:-16}"
var_os="${var_os:-ubuntu}"
var_version="${var_version:-24.04}"
var_unprivileged="${var_unprivileged:-1}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -f /opt/anytype/any-sync-bundle ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "anytype" "grishy/any-sync-bundle"; then
msg_info "Stopping Service"
systemctl stop anytype
msg_ok "Stopped Service"
msg_info "Backing up Data"
cp -r /opt/anytype/data /opt/anytype_data_backup
msg_ok "Backed up Data"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "anytype" "grishy/any-sync-bundle" "prebuild" "latest" "/opt/anytype" "any-sync-bundle_*_linux_amd64.tar.gz"
chmod +x /opt/anytype/any-sync-bundle
msg_info "Restoring Data"
cp -r /opt/anytype_data_backup/. /opt/anytype/data
rm -rf /opt/anytype_data_backup
msg_ok "Restored Data"
msg_info "Starting Service"
systemctl start anytype
msg_ok "Started Service"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:33010${CL}"
echo -e "${INFO}${YW} Client config file:${CL}"
echo -e "${TAB}${GATEWAY}${BGN}/opt/anytype/data/client-config.yml${CL}"
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/qdm12/gluetun
APP="Gluetun"
var_tags="${var_tags:-vpn;wireguard;openvpn}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-2048}"
var_disk="${var_disk:-8}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_unprivileged="${var_unprivileged:-1}"
var_tun="${var_tun:-yes}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -f /usr/local/bin/gluetun ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "gluetun" "qdm12/gluetun"; then
msg_info "Stopping Service"
systemctl stop gluetun
msg_ok "Stopped Service"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "gluetun" "qdm12/gluetun" "tarball"
msg_info "Building Gluetun"
cd /opt/gluetun
$STD go mod download
CGO_ENABLED=0 $STD go build -trimpath -ldflags="-s -w" -o /usr/local/bin/gluetun ./cmd/gluetun/
msg_ok "Built Gluetun"
msg_info "Starting Service"
systemctl start gluetun
msg_ok "Started Service"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:8000${CL}"
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
# Copyright (c) 2021-2026 community-scripts ORG
# Author: johanngrobe
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/oss-apps/split-pro
APP="Split-Pro"
var_tags="${var_tags:-finance;expense-sharing}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-4096}"
var_disk="${var_disk:-6}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_unprivileged="${var_unprivileged:-1}"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /opt/split-pro ]]; then
msg_error "No Split Pro Installation Found!"
exit
fi
if check_for_gh_release "split-pro" "oss-apps/split-pro"; then
msg_info "Stopping Service"
systemctl stop split-pro
msg_ok "Stopped Service"
msg_info "Backing up Data"
cp /opt/split-pro/.env /opt/split-pro.env
msg_ok "Backed up Data"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "split-pro" "oss-apps/split-pro" "tarball"
msg_info "Building Application"
cd /opt/split-pro
$STD pnpm install --frozen-lockfile
$STD pnpm build
cp /opt/split-pro.env /opt/split-pro/.env
rm -f /opt/split-pro.env
ln -sf /opt/split-pro_data/uploads /opt/split-pro/uploads
$STD pnpm exec prisma migrate deploy
msg_ok "Built Application"
msg_info "Starting Service"
systemctl start split-pro
msg_ok "Started Service"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:3000${CL}"
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/FuzzyGrim/Yamtrack
APP="Yamtrack"
var_tags="${var_tags:-media;tracker;movies;anime}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-2048}"
var_disk="${var_disk:-8}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_unprivileged="${var_unprivileged:-1}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /opt/yamtrack ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "yamtrack" "FuzzyGrim/Yamtrack"; then
msg_info "Stopping Services"
systemctl stop yamtrack yamtrack-celery
msg_ok "Stopped Services"
msg_info "Backing up Data"
cp /opt/yamtrack/src/.env /opt/yamtrack_env.bak
msg_ok "Backed up Data"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "yamtrack" "FuzzyGrim/Yamtrack" "tarball"
msg_info "Installing Python Dependencies"
cd /opt/yamtrack
$STD uv venv .venv
$STD uv pip install --no-cache-dir -r requirements.txt
msg_ok "Installed Python Dependencies"
msg_info "Restoring Data"
cp /opt/yamtrack_env.bak /opt/yamtrack/src/.env
rm -f /opt/yamtrack_env.bak
msg_ok "Restored Data"
msg_info "Updating Yamtrack"
cd /opt/yamtrack/src
$STD /opt/yamtrack/.venv/bin/python manage.py migrate
$STD /opt/yamtrack/.venv/bin/python manage.py collectstatic --noinput
msg_ok "Updated Yamtrack"
msg_info "Updating Nginx Configuration"
cp /opt/yamtrack/nginx.conf /etc/nginx/nginx.conf
sed -i 's|user abc;|user www-data;|' /etc/nginx/nginx.conf
sed -i 's|/yamtrack/staticfiles/|/opt/yamtrack/src/staticfiles/|' /etc/nginx/nginx.conf
$STD systemctl reload nginx
msg_ok "Updated Nginx Configuration"
msg_info "Starting Services"
systemctl start yamtrack yamtrack-celery
msg_ok "Started Services"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:8000${CL}"
@@ -1,50 +0,0 @@
{
"name": "Unsloth Studio (In Beta)",
"slug": "unsolth-studio",
"categories": [20],
"date_created": "2026-03-18",
"type": "ct",
"updateable": true,
"privileged": false,
"interface_port": 8888,
"documentation": "https://unsloth.ai/docs/new/studio/start",
"website": "https://unsloth.ai/",
"logo": "https://unsloth.ai/favicon.ico",
"config_path": "/opt/unsolth-studio/.env",
"description": "Local, browser-based GUI for fine-tuning LLMs without writing code. Supports QLoRA, LoRA, and full fine-tuning with live training monitoring, GPU stats, and model export to GGUF/Safetensors.",
"install_methods": [
{
"type": "default",
"script": "ct/unsolth-studio.sh",
"resources": {
"cpu": 4,
"ram": 16384,
"hdd": 50,
"os": "Debian",
"version": "13"
}
}
],
"default_credentials": {
"username": null,
"password": null
},
"notes": [
{
"text": "Requires GPU passthrough for training. NVIDIA, AMD (ROCm), and Intel GPUs are supported.",
"type": "info"
},
{
"text": "First launch takes 5-10 minutes to compile llama.cpp binaries.",
"type": "warning"
},
{
"text": "Minimum 16GB RAM recommended for training models up to 7B parameters.",
"type": "info"
},
{
"text": "For AMD GPUs, ensure ROCm is properly configured on the host.",
"type": "info"
}
]
}
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: cobalt (cobaltgit)
# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
# Source: https://ntfy.sh/
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
msg_info "Installing ntfy"
$STD apk add --no-cache ntfy ntfy-openrc libcap
sed -i '/^listen-http/s/^\(.*\)$/#\1\n/' /etc/ntfy/server.yml
setcap 'cap_net_bind_service=+ep' /usr/bin/ntfy
$STD rc-update add ntfy default
$STD service ntfy start
msg_ok "Installed ntfy"
motd_ssh
customize
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://anytype.io
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
setup_mongodb
msg_info "Configuring MongoDB Replica Set"
cat <<EOF >>/etc/mongod.conf
replication:
replSetName: "rs0"
EOF
systemctl restart mongod
sleep 3
$STD mongosh --eval 'rs.initiate({_id: "rs0", members: [{_id: 0, host: "127.0.0.1:27017"}]})'
msg_ok "Configured MongoDB Replica Set"
msg_info "Installing Redis Stack"
setup_deb822_repo \
"redis-stack" \
"https://packages.redis.io/gpg" \
"https://packages.redis.io/deb" \
"jammy" \
"main"
$STD apt install -y redis-stack-server
systemctl enable -q --now redis-stack-server
msg_ok "Installed Redis Stack"
fetch_and_deploy_gh_release "anytype" "grishy/any-sync-bundle" "prebuild" "latest" "/opt/anytype" "any-sync-bundle_*_linux_amd64.tar.gz"
chmod +x /opt/anytype/any-sync-bundle
msg_info "Configuring Anytype"
mkdir -p /opt/anytype/data/storage
cat <<EOF >/opt/anytype/.env
ANY_SYNC_BUNDLE_CONFIG=/opt/anytype/data/bundle-config.yml
ANY_SYNC_BUNDLE_CLIENT_CONFIG=/opt/anytype/data/client-config.yml
ANY_SYNC_BUNDLE_INIT_STORAGE=/opt/anytype/data/storage/
ANY_SYNC_BUNDLE_INIT_EXTERNAL_ADDRS=${LOCAL_IP}
ANY_SYNC_BUNDLE_INIT_MONGO_URI=mongodb://127.0.0.1:27017/
ANY_SYNC_BUNDLE_INIT_REDIS_URI=redis://127.0.0.1:6379/
ANY_SYNC_BUNDLE_LOG_LEVEL=info
EOF
msg_ok "Configured Anytype"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/anytype.service
[Unit]
Description=Anytype Sync Server (any-sync-bundle)
After=network-online.target mongod.service redis-stack-server.service
Wants=network-online.target
Requires=mongod.service redis-stack-server.service
[Service]
Type=simple
User=root
WorkingDirectory=/opt/anytype
EnvironmentFile=/opt/anytype/.env
ExecStart=/opt/anytype/any-sync-bundle start-bundle
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now anytype
msg_ok "Created Service"
motd_ssh
customize
cleanup_lxc
@@ -0,0 +1,96 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/qdm12/gluetun
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
msg_info "Installing Dependencies"
$STD apt install -y \
openvpn \
wireguard-tools \
iptables
msg_ok "Installed Dependencies"
msg_info "Configuring iptables"
$STD update-alternatives --set iptables /usr/sbin/iptables-legacy
$STD update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy
ln -sf /usr/sbin/openvpn /usr/sbin/openvpn2.6
msg_ok "Configured iptables"
setup_go
fetch_and_deploy_gh_release "gluetun" "qdm12/gluetun" "tarball"
msg_info "Building Gluetun"
cd /opt/gluetun
$STD go mod download
CGO_ENABLED=0 $STD go build -trimpath -ldflags="-s -w" -o /usr/local/bin/gluetun ./cmd/gluetun/
msg_ok "Built Gluetun"
msg_info "Configuring Gluetun"
mkdir -p /opt/gluetun-data
touch /etc/alpine-release
ln -sf /opt/gluetun-data /gluetun
cat <<EOF >/opt/gluetun-data/.env
VPN_SERVICE_PROVIDER=custom
VPN_TYPE=openvpn
OPENVPN_CUSTOM_CONFIG=/opt/gluetun-data/custom.ovpn
OPENVPN_USER=
OPENVPN_PASSWORD=
OPENVPN_PROCESS_USER=root
PUID=0
PGID=0
HTTP_CONTROL_SERVER_ADDRESS=:8000
HTTPPROXY=off
SHADOWSOCKS=off
PPROF_ENABLED=no
PPROF_BLOCK_PROFILE_RATE=0
PPROF_MUTEX_PROFILE_RATE=0
PPROF_HTTP_SERVER_ADDRESS=:6060
FIREWALL_ENABLED_DISABLING_IT_SHOOTS_YOU_IN_YOUR_FOOT=on
HEALTH_SERVER_ADDRESS=127.0.0.1:9999
DNS_UPSTREAM_RESOLVERS=cloudflare
LOG_LEVEL=info
STORAGE_FILEPATH=/gluetun/servers.json
PUBLICIP_FILE=/gluetun/ip
VPN_PORT_FORWARDING_STATUS_FILE=/gluetun/forwarded_port
TZ=UTC
EOF
msg_ok "Configured Gluetun"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/gluetun.service
[Unit]
Description=Gluetun VPN Client
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/gluetun-data
EnvironmentFile=/opt/gluetun-data/.env
UnsetEnvironment=USER
ExecStartPre=/bin/sh -c 'rm -f /etc/openvpn/target.ovpn'
ExecStart=/usr/local/bin/gluetun
Restart=on-failure
RestartSec=5
AmbientCapabilities=CAP_NET_ADMIN
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now gluetun
msg_ok "Created Service"
motd_ssh
customize
cleanup_lxc
@@ -0,0 +1,74 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: johanngrobe
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/oss-apps/split-pro
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
NODE_VERSION="22" NODE_MODULE="pnpm" setup_nodejs
PG_VERSION="17" PG_MODULES="cron" setup_postgresql
msg_info "Installing Dependencies"
$STD apt install -y openssl
msg_ok "Installed Dependencies"
PG_DB_NAME="splitpro" PG_DB_USER="splitpro" PG_DB_EXTENSIONS="pg_cron" setup_postgresql_db
fetch_and_deploy_gh_release "split-pro" "oss-apps/split-pro" "tarball"
msg_info "Installing Dependencies"
cd /opt/split-pro
$STD pnpm install --frozen-lockfile
msg_ok "Installed Dependencies"
msg_info "Building Split Pro"
cd /opt/split-pro
mkdir -p /opt/split-pro_data/uploads
ln -sf /opt/split-pro_data/uploads /opt/split-pro/uploads
NEXTAUTH_SECRET=$(openssl rand -base64 32)
cp .env.example .env
sed -i "s|^DATABASE_URL=.*|DATABASE_URL=\"postgresql://${PG_DB_USER}:${PG_DB_PASS}@localhost:5432/${PG_DB_NAME}\"|" .env
sed -i "s|^NEXTAUTH_SECRET=.*|NEXTAUTH_SECRET=\"${NEXTAUTH_SECRET}\"|" .env
sed -i "s|^NEXTAUTH_URL=.*|NEXTAUTH_URL=\"http://${LOCAL_IP}:3000\"|" .env
sed -i "s|^NEXTAUTH_URL_INTERNAL=.*|NEXTAUTH_URL_INTERNAL=\"http://localhost:3000\"|" .env
sed -i "/^POSTGRES_CONTAINER_NAME=/d" .env
sed -i "/^POSTGRES_USER=/d" .env
sed -i "/^POSTGRES_PASSWORD=/d" .env
sed -i "/^POSTGRES_DB=/d" .env
sed -i "/^POSTGRES_PORT=/d" .env
$STD pnpm build
$STD pnpm exec prisma migrate deploy
msg_ok "Built Split Pro"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/split-pro.service
[Unit]
Description=Split Pro
After=network.target postgresql.service
Requires=postgresql.service
[Service]
Type=simple
User=root
WorkingDirectory=/opt/split-pro
EnvironmentFile=/opt/split-pro/.env
ExecStart=/usr/bin/pnpm start
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now split-pro
msg_ok "Created Service"
motd_ssh
customize
cleanup_lxc
@@ -0,0 +1,105 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/FuzzyGrim/Yamtrack
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
msg_info "Installing Dependencies"
$STD apt install -y \
nginx \
redis-server
msg_ok "Installed Dependencies"
PG_VERSION="16" setup_postgresql
PG_DB_NAME="yamtrack" PG_DB_USER="yamtrack" setup_postgresql_db
PYTHON_VERSION="3.12" setup_uv
fetch_and_deploy_gh_release "yamtrack" "FuzzyGrim/Yamtrack" "tarball"
msg_info "Installing Python Dependencies"
cd /opt/yamtrack
$STD uv venv .venv
$STD uv pip install --no-cache-dir -r requirements.txt
msg_ok "Installed Python Dependencies"
msg_info "Configuring Yamtrack"
SECRET=$(openssl rand -hex 32)
cat <<EOF >/opt/yamtrack/src/.env
SECRET=${SECRET}
DB_HOST=localhost
DB_NAME=${PG_DB_NAME}
DB_USER=${PG_DB_USER}
DB_PASSWORD=${PG_DB_PASS}
DB_PORT=5432
REDIS_URL=redis://localhost:6379
URLS=http://${LOCAL_IP}:8000
EOF
cd /opt/yamtrack/src
$STD /opt/yamtrack/.venv/bin/python manage.py migrate
$STD /opt/yamtrack/.venv/bin/python manage.py collectstatic --noinput
msg_ok "Configured Yamtrack"
msg_info "Configuring Nginx"
rm -f /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default
cp /opt/yamtrack/nginx.conf /etc/nginx/nginx.conf
sed -i 's|user abc;|user www-data;|' /etc/nginx/nginx.conf
sed -i 's|pid /tmp/nginx.pid;|pid /run/nginx.pid;|' /etc/nginx/nginx.conf
sed -i 's|/yamtrack/staticfiles/|/opt/yamtrack/src/staticfiles/|' /etc/nginx/nginx.conf
sed -i 's|error_log /dev/stderr|error_log /var/log/nginx/error.log|' /etc/nginx/nginx.conf
sed -i 's|access_log /dev/stdout|access_log /var/log/nginx/access.log|' /etc/nginx/nginx.conf
$STD nginx -t
systemctl enable -q nginx
$STD systemctl restart nginx
msg_ok "Configured Nginx"
msg_info "Creating Services"
cat <<EOF >/etc/systemd/system/yamtrack.service
[Unit]
Description=Yamtrack Gunicorn
After=network.target postgresql.service redis-server.service
Requires=postgresql.service redis-server.service
[Service]
Type=simple
WorkingDirectory=/opt/yamtrack/src
ExecStart=/opt/yamtrack/.venv/bin/gunicorn config.wsgi:application -b 127.0.0.1:8001 -w 2 --timeout 120
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
cat <<EOF >/etc/systemd/system/yamtrack-celery.service
[Unit]
Description=Yamtrack Celery Worker
After=network.target postgresql.service redis-server.service yamtrack.service
Requires=postgresql.service redis-server.service
[Service]
Type=simple
WorkingDirectory=/opt/yamtrack/src
ExecStart=/opt/yamtrack/.venv/bin/celery -A config worker --beat --scheduler django --loglevel INFO
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now redis-server yamtrack yamtrack-celery
msg_ok "Created Services"
motd_ssh
customize
cleanup_lxc
@@ -1,50 +0,0 @@
{
"name": "Unsloth Studio (In Beta)",
"slug": "unsolth-studio",
"categories": [20],
"date_created": "2026-03-18",
"type": "ct",
"updateable": true,
"privileged": false,
"interface_port": 8888,
"documentation": "https://unsloth.ai/docs/new/studio/start",
"website": "https://unsloth.ai/",
"logo": "https://unsloth.ai/favicon.ico",
"config_path": "/opt/unsolth-studio/.env",
"description": "Local, browser-based GUI for fine-tuning LLMs without writing code. Supports QLoRA, LoRA, and full fine-tuning with live training monitoring, GPU stats, and model export to GGUF/Safetensors.",
"install_methods": [
{
"type": "default",
"script": "ct/unsolth-studio.sh",
"resources": {
"cpu": 4,
"ram": 16384,
"hdd": 50,
"os": "Debian",
"version": "13"
}
}
],
"default_credentials": {
"username": null,
"password": null
},
"notes": [
{
"text": "Requires GPU passthrough for training. NVIDIA, AMD (ROCm), and Intel GPUs are supported.",
"type": "info"
},
{
"text": "First launch takes 5-10 minutes to compile llama.cpp binaries.",
"type": "warning"
},
{
"text": "Minimum 16GB RAM recommended for training models up to 7B parameters.",
"type": "info"
},
{
"text": "For AMD GPUs, ensure ROCm is properly configured on the host.",
"type": "info"
}
]
}
@@ -91,17 +91,18 @@ msg_info "Running Unsloth Studio Setup"
# This requires GPU access - set up environment for ROCm if installed
# Set up ROCm environment if available
# Use ${VAR:-} to handle unset variables (set -u causes errors otherwise)
if [ -d "/opt/rocm" ]; then
export PATH="/opt/rocm/bin:$PATH"
export LD_LIBRARY_PATH="/opt/rocm/lib:$LD_LIBRARY_PATH"
export LD_LIBRARY_PATH="/opt/rocm/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
export ROCM_PATH="/opt/rocm"
elif [ -d "/opt/rocm-7.2" ]; then
export PATH="/opt/rocm-7.2/bin:$PATH"
export LD_LIBRARY_PATH="/opt/rocm-7.2/lib:$LD_LIBRARY_PATH"
export LD_LIBRARY_PATH="/opt/rocm-7.2/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
export ROCM_PATH="/opt/rocm-7.2"
elif [ -d "/opt/rocm-6.2" ]; then
export PATH="/opt/rocm-6.2/bin:$PATH"
export LD_LIBRARY_PATH="/opt/rocm-6.2/lib:$LD_LIBRARY_PATH"
export LD_LIBRARY_PATH="/opt/rocm-6.2/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
export ROCM_PATH="/opt/rocm-6.2"
fi
@@ -1,50 +0,0 @@
{
"name": "Unsloth Studio (In Beta)",
"slug": "unsolth-studio",
"categories": [20],
"date_created": "2026-03-18",
"type": "ct",
"updateable": true,
"privileged": false,
"interface_port": 8888,
"documentation": "https://unsloth.ai/docs/new/studio/start",
"website": "https://unsloth.ai/",
"logo": "https://unsloth.ai/favicon.ico",
"config_path": "/opt/unsolth-studio/.env",
"description": "Local, browser-based GUI for fine-tuning LLMs without writing code. Supports QLoRA, LoRA, and full fine-tuning with live training monitoring, GPU stats, and model export to GGUF/Safetensors.",
"install_methods": [
{
"type": "default",
"script": "ct/unsolth-studio.sh",
"resources": {
"cpu": 4,
"ram": 16384,
"hdd": 50,
"os": "Debian",
"version": "13"
}
}
],
"default_credentials": {
"username": null,
"password": null
},
"notes": [
{
"text": "Requires GPU passthrough for training. NVIDIA, AMD (ROCm), and Intel GPUs are supported.",
"type": "info"
},
{
"text": "First launch takes 5-10 minutes to compile llama.cpp binaries.",
"type": "warning"
},
{
"text": "Minimum 16GB RAM recommended for training models up to 7B parameters.",
"type": "info"
},
{
"text": "For AMD GPUs, ensure ROCm is properly configured on the host.",
"type": "info"
}
]
}
@@ -1,50 +0,0 @@
{
"name": "Unsloth Studio (In Beta)",
"slug": "unsolth-studio",
"categories": [20],
"date_created": "2026-03-18",
"type": "ct",
"updateable": true,
"privileged": false,
"interface_port": 8888,
"documentation": "https://unsloth.ai/docs/new/studio/start",
"website": "https://unsloth.ai/",
"logo": "https://unsloth.ai/favicon.ico",
"config_path": "/opt/unsolth-studio/.env",
"description": "Local, browser-based GUI for fine-tuning LLMs without writing code. Supports QLoRA, LoRA, and full fine-tuning with live training monitoring, GPU stats, and model export to GGUF/Safetensors.",
"install_methods": [
{
"type": "default",
"script": "ct/unsolth-studio.sh",
"resources": {
"cpu": 4,
"ram": 16384,
"hdd": 50,
"os": "Debian",
"version": "13"
}
}
],
"default_credentials": {
"username": null,
"password": null
},
"notes": [
{
"text": "Requires GPU passthrough for training. NVIDIA, AMD (ROCm), and Intel GPUs are supported.",
"type": "info"
},
{
"text": "First launch takes 5-10 minutes to compile llama.cpp binaries.",
"type": "warning"
},
{
"text": "Minimum 16GB RAM recommended for training models up to 7B parameters.",
"type": "info"
},
{
"text": "For AMD GPUs, ensure ROCm is properly configured on the host.",
"type": "info"
}
]
}
@@ -30,20 +30,55 @@ msg_ok "Installed Dependencies"
setup_hwaccel
# Setup Python virtual environment with uv (fast Python package manager)
PYTHON_VERSION="3.12" setup_uv
PYTHON_VERSION="3.13" setup_uv
msg_info "Creating Virtual Environment"
mkdir -p /opt/unsolth-studio
cd /opt/unsolth-studio || exit
$STD uv venv --python 3.12
$STD uv venv --python 3.13
source .venv/bin/activate
msg_ok "Created Virtual Environment"
msg_info "Installing PyTorch"
# Install PyTorch first (required by unsloth)
# Use CPU version for broader compatibility; GPU version can be installed manually if needed
$STD uv pip install torch --index-url https://download.pytorch.org/whl/cpu
msg_ok "Installed PyTorch"
msg_info "Detecting GPU Type for PyTorch Installation"
# Detect GPU type based on what setup_hwaccel installed
# setup_hwaccel runs before this and installs NVIDIA drivers or ROCm
GPU_TYPE="cpu"
# Check for NVIDIA GPU (nvidia-smi installed by setup_hwaccel)
if command -v nvidia-smi &>/dev/null && nvidia-smi &>/dev/null; then
GPU_TYPE="nvidia"
msg_info "NVIDIA GPU detected - installing PyTorch with CUDA support"
# Check for AMD GPU (ROCm installed by setup_hwaccel at /opt/rocm)
elif [ -d "/opt/rocm" ] || [ -d "/opt/rocm-7.2" ] || [ -d "/opt/rocm-6.2" ]; then
GPU_TYPE="amd"
msg_info "AMD GPU detected (ROCm installed) - installing PyTorch with ROCm support"
# Check for AMD render devices (GPU passthrough configured)
elif ls /dev/dri/renderD* &>/dev/null 2>&1; then
# Check if any render device is AMD
for render_dev in /dev/dri/renderD*; do
if [ -e "$render_dev" ]; then
GPU_TYPE="amd"
msg_info "AMD GPU detected (render device) - installing PyTorch with ROCm support"
break
fi
done
fi
if [ "$GPU_TYPE" = "nvidia" ]; then
# NVIDIA GPU - install PyTorch with CUDA 12.4 support
$STD uv pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124
msg_ok "Installed PyTorch with CUDA Support"
elif [ "$GPU_TYPE" = "amd" ]; then
# AMD GPU - install PyTorch with ROCm 7.2 support
$STD uv pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/test/rocm7.2
msg_ok "Installed PyTorch with ROCm Support"
else
# No GPU detected - install CPU version
msg_info "No GPU detected - installing PyTorch CPU version"
$STD uv pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
msg_ok "Installed PyTorch (CPU version)"
fi
msg_info "Installing Unsloth"
# Install unsloth and its dependencies
@@ -53,9 +88,36 @@ msg_ok "Installed Unsloth"
msg_info "Running Unsloth Studio Setup"
# Run the unsloth studio setup command to compile llama.cpp
# Use Python module invocation since uv pip install doesn't create entry points
$STD /opt/unsolth-studio/.venv/bin/python -m unsloth studio setup
msg_ok "Completed Unsloth Studio Setup"
# This requires GPU access - set up environment for ROCm if installed
# Set up ROCm environment if available
if [ -d "/opt/rocm" ]; then
export PATH="/opt/rocm/bin:$PATH"
export LD_LIBRARY_PATH="/opt/rocm/lib:$LD_LIBRARY_PATH"
export ROCM_PATH="/opt/rocm"
elif [ -d "/opt/rocm-7.2" ]; then
export PATH="/opt/rocm-7.2/bin:$PATH"
export LD_LIBRARY_PATH="/opt/rocm-7.2/lib:$LD_LIBRARY_PATH"
export ROCM_PATH="/opt/rocm-7.2"
elif [ -d "/opt/rocm-6.2" ]; then
export PATH="/opt/rocm-6.2/bin:$PATH"
export LD_LIBRARY_PATH="/opt/rocm-6.2/lib:$LD_LIBRARY_PATH"
export ROCM_PATH="/opt/rocm-6.2"
fi
# Check if GPU is available (works for both CUDA and ROCm)
if /opt/unsolth-studio/.venv/bin/python -c "import torch; exit(0 if torch.cuda.is_available() else 1)" 2>/dev/null; then
$STD /opt/unsolth-studio/.venv/bin/python -m unsloth studio setup
msg_ok "Completed Unsloth Studio Setup"
else
msg_info "GPU not detected via torch.cuda - skipping Unsloth Studio setup"
msg_info "This may be normal if ROCm libraries need system restart to take effect"
echo ""
echo -e "${GN}Note: If you have GPU passthrough configured, try:${CL}"
echo -e "${GN} 1. Restart the container: pct stop <CTID> && pct start <CTID>${CL}"
echo -e "${GN} 2. Then run: source /opt/unsolth-studio/.venv/bin/activate && unsloth studio setup${CL}"
echo ""
fi
msg_info "Creating Directories"
mkdir -p /opt/unsolth-studio/models
@@ -65,6 +127,34 @@ chmod 755 /var/log/unsolth-studio
msg_ok "Created Directories"
msg_info "Creating Service"
# Create environment file for ROCm/CUDA paths
cat <<EOF >/opt/unsolth-studio/environment.sh
#!/bin/bash
# Set up GPU environment for Unsloth Studio
# ROCm environment (AMD GPUs)
if [ -d "/opt/rocm" ]; then
export PATH="/opt/rocm/bin:\$PATH"
export LD_LIBRARY_PATH="/opt/rocm/lib:\$LD_LIBRARY_PATH"
export ROCM_PATH="/opt/rocm"
elif [ -d "/opt/rocm-7.2" ]; then
export PATH="/opt/rocm-7.2/bin:\$PATH"
export LD_LIBRARY_PATH="/opt/rocm-7.2/lib:\$LD_LIBRARY_PATH"
export ROCM_PATH="/opt/rocm-7.2"
elif [ -d "/opt/rocm-6.2" ]; then
export PATH="/opt/rocm-6.2/bin:\$PATH"
export LD_LIBRARY_PATH="/opt/rocm-6.2/lib:\$LD_LIBRARY_PATH"
export ROCM_PATH="/opt/rocm-6.2"
fi
# NVIDIA CUDA environment
if [ -d "/usr/local/cuda" ]; then
export PATH="/usr/local/cuda/bin:\$PATH"
export LD_LIBRARY_PATH="/usr/local/cuda/lib64:\$LD_LIBRARY_PATH"
fi
EOF
chmod +x /opt/unsolth-studio/environment.sh
cat <<EOF >/etc/systemd/system/unsolth-studio.service
[Unit]
Description=Unsloth Studio - Local LLM Fine-tuning Web UI
@@ -75,6 +165,7 @@ Wants=network-online.target
Type=simple
WorkingDirectory=/opt/unsolth-studio
Environment="PATH=/opt/unsolth-studio/.venv/bin:/usr/local/bin:/usr/bin:/bin"
EnvironmentFile=/opt/unsolth-studio/environment.sh
ExecStart=/opt/unsolth-studio/.venv/bin/python -m unsloth studio -H 0.0.0.0 -p 8888
Restart=on-failure
RestartSec=10
@@ -90,8 +181,15 @@ TimeoutStopSec=60
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now unsolth-studio
# Don't auto-start the service since GPU passthrough may not be configured yet
# User needs to configure GPU passthrough first, then start the service manually
systemctl enable -q unsolth-studio
msg_ok "Created Service"
echo ""
echo -e "${GN}Note: The unsolth-studio service is enabled but not started.${CL}"
echo -e "${GN}Configure GPU passthrough first, then start with:${CL}"
echo -e "${GN} systemctl start unsolth-studio${CL}"
echo ""
# Create GPU passthrough info file
cat <<EOF >/opt/unsolth-studio/GPU_PASSTHROUGH.md
standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/ct/headers/unsolth-studio → standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/ct/headers/unsolth-studio
View File
standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/ct/unsolth-studio.sh → standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/ct/unsolth-studio.sh
View File
standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/frontend/public/json/unsolth-studio.json → standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/standalone/frontend/public/json/unsolth-studio.json
View File
@@ -0,0 +1,167 @@
#!/usr/bin/env bash
# Author: Heretek-AI
# License: MIT | https://github.com/Heretek-AI/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/unslothai/unsloth
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
msg_info "Installing Dependencies"
$STD apt-get install -y \
curl \
wget \
git \
cmake \
build-essential \
python3 \
python3-pip \
python3-venv \
pciutils
msg_ok "Installed Dependencies"
# Setup GPU hardware acceleration FIRST (detects GPU, installs drivers, configures permissions)
# This must run before installing unsloth/torch so PyTorch can detect the GPU
setup_hwaccel
# Setup Python virtual environment with uv (fast Python package manager)
PYTHON_VERSION="3.12" setup_uv
msg_info "Creating Virtual Environment"
mkdir -p /opt/unsolth-studio
cd /opt/unsolth-studio || exit
$STD uv venv --python 3.12
source .venv/bin/activate
msg_ok "Created Virtual Environment"
msg_info "Installing PyTorch"
# Install PyTorch first (required by unsloth)
# Use CPU version for broader compatibility; GPU version can be installed manually if needed
$STD uv pip install torch --index-url https://download.pytorch.org/whl/cpu
msg_ok "Installed PyTorch"
msg_info "Installing Unsloth"
# Install unsloth and its dependencies
# packaging module is required but not declared as dependency
$STD uv pip install unsloth packaging
msg_ok "Installed Unsloth"
msg_info "Running Unsloth Studio Setup"
# Run the unsloth studio setup command to compile llama.cpp
# Use Python module invocation since uv pip install doesn't create entry points
$STD /opt/unsolth-studio/.venv/bin/python -m unsloth studio setup
msg_ok "Completed Unsloth Studio Setup"
msg_info "Creating Directories"
mkdir -p /opt/unsolth-studio/models
mkdir -p /opt/unsolth-studio/datasets
mkdir -p /var/log/unsolth-studio
chmod 755 /var/log/unsolth-studio
msg_ok "Created Directories"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/unsolth-studio.service
[Unit]
Description=Unsloth Studio - Local LLM Fine-tuning Web UI
After=network.target network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/unsolth-studio
Environment="PATH=/opt/unsolth-studio/.venv/bin:/usr/local/bin:/usr/bin:/bin"
ExecStart=/opt/unsolth-studio/.venv/bin/python -m unsloth studio -H 0.0.0.0 -p 8888
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=unsolth-studio
# Resource limits
LimitNOFILE=65535
TimeoutStartSec=600
TimeoutStopSec=60
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now unsolth-studio
msg_ok "Created Service"
# Create GPU passthrough info file
cat <<EOF >/opt/unsolth-studio/GPU_PASSTHROUGH.md
# GPU Passthrough Configuration for Unsloth Studio
This container has been configured for GPU acceleration for LLM fine-tuning.
## Required Proxmox Configuration
Add the following lines to your container config file:
/etc/pve/lxc/<CTID>.conf
### For NVIDIA GPUs:
\`\`\`
# Requires nvidia-container-toolkit on host
lxc.cgroup2.devices.allow: c 195:* rwm
lxc.cgroup2.devices.allow: c 509:* rwm
dev0: /dev/nvidia0,gid=104
dev1: /dev/nvidiactl,gid=104
dev2: /dev/nvidia-uvm,gid=104
dev3: /dev/nvidia-uvm-tools,gid=104
\`\`\`
### For AMD GPUs (ROCm):
\`\`\`
dev0: /dev/kfd,gid=104
dev1: /dev/dri/renderD128,gid=104
lxc.cgroup2.devices.allow: c 226:0 rwm
lxc.cgroup2.devices.allow: c 226:128 rwm
\`\`\`
### For Intel GPUs:
\`\`\`
dev0: /dev/dri/renderD128,gid=104
lxc.cgroup2.devices.allow: c 226:128 rwm
\`\`\`
## Verify GPU Access
Run these commands inside the container:
- nvidia-smi (NVIDIA GPUs)
- rocminfo (AMD GPUs)
- python -c "import torch; print(torch.cuda.is_available())"
## Usage
Access the web UI at: http://<IP>:8888
On first launch:
1. Create a password to secure your account
2. Follow the onboarding wizard to select a model and dataset
3. Configure training parameters
4. Start fine-tuning!
## Supported Models
Unsloth Studio supports fine-tuning many LLM models including:
- Llama 3.x
- Qwen 2.x / 3.x
- Mistral
- Gemma
- Phi-3
- And many more...
## Documentation
- Official Docs: https://unsloth.ai/docs/new/studio/start
- GitHub: https://github.com/unslothai/unsloth
EOF
motd_ssh
customize
cleanup_lxc
@@ -1,5 +1,5 @@
{
"generated": "2026-03-15T12:22:39Z",
"generated": "2026-03-16T18:42:54Z",
"versions": [
{
"slug": "agregarr",
@@ -18,9 +18,9 @@
{
"slug": "llamacpp",
"repo": "ggml-org/llama.cpp",
"version": "b8354",
"version": "b8373",
"pinned": false,
"date": "2026-03-15T10:06:38Z"
"date": "2026-03-16T10:55:12Z"
},
{
"slug": "localai",
@@ -46,9 +46,9 @@
{
"slug": "pegaprox",
"repo": "PegaProx/project-pegaprox",
"version": "v0.9.1.3",
"version": "v0.9.2",
"pinned": false,
"date": "2026-03-11T20:23:52Z"
"date": "2026-03-16T08:00:29Z"
},
{
"slug": "ragflow",
@@ -48,7 +48,8 @@ jobs:
const https = require('https');
const http = require('http');
const url = require('url');
function request(fullUrl, opts) {
function request(fullUrl, opts, redirectCount) {
redirectCount = redirectCount || 0;
return new Promise(function(resolve, reject) {
const u = url.parse(fullUrl);
const isHttps = u.protocol === 'https:';
@@ -63,6 +64,13 @@ jobs:
if (body) options.headers['Content-Length'] = Buffer.byteLength(body);
const lib = isHttps ? https : http;
const req = lib.request(options, function(res) {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
if (redirectCount >= 5) return reject(new Error('Too many redirects from ' + fullUrl));
const redirectUrl = url.resolve(fullUrl, res.headers.location);
res.resume();
resolve(request(redirectUrl, opts, redirectCount + 1));
return;
}
let data = '';
res.on('data', function(chunk) { data += chunk; });
res.on('end', function() {
@@ -125,15 +133,15 @@ jobs:
var osVersionToId = {};
try {
const res = await request(apiBase + '/collections/z_ref_note_types/records?perPage=500', { headers: { 'Authorization': token } });
if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.type != null) noteTypeToId[item.type] = item.id; });
if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.type != null) { noteTypeToId[item.type] = item.id; noteTypeToId[item.type.toLowerCase()] = item.id; } });
} catch (e) { console.warn('z_ref_note_types:', e.message); }
try {
const res = await request(apiBase + '/collections/z_ref_install_method_types/records?perPage=500', { headers: { 'Authorization': token } });
if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.type != null) installMethodTypeToId[item.type] = item.id; });
if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.type != null) { installMethodTypeToId[item.type] = item.id; installMethodTypeToId[item.type.toLowerCase()] = item.id; } });
} catch (e) { console.warn('z_ref_install_method_types:', e.message); }
try {
const res = await request(apiBase + '/collections/z_ref_os/records?perPage=500', { headers: { 'Authorization': token } });
if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.os != null) osToId[item.os] = item.id; });
if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.os != null) { osToId[item.os] = item.id; osToId[item.os.toLowerCase()] = item.id; } });
} catch (e) { console.warn('z_ref_os:', e.message); }
try {
const res = await request(apiBase + '/collections/z_ref_os_version/records?perPage=500&expand=os', { headers: { 'Authorization': token } });
@@ -154,7 +162,7 @@ jobs:
name: data.name,
slug: data.slug,
script_created: data.date_created || data.script_created,
script_updated: data.date_created || data.script_updated,
script_updated: new Date().toISOString().split('T')[0],
updateable: data.updateable,
privileged: data.privileged,
port: data.interface_port != null ? data.interface_port : data.port,
@@ -163,8 +171,8 @@ jobs:
logo: data.logo,
description: data.description,
config_path: data.config_path,
default_user: (data.default_credentials && data.default_credentials.username) || data.default_user,
default_passwd: (data.default_credentials && data.default_credentials.password) || data.default_passwd,
default_user: (data.default_credentials && data.default_credentials.username) || data.default_user || null,
default_passwd: (data.default_credentials && data.default_credentials.password) || data.default_passwd || null,
is_dev: false
};
var resolvedType = typeValueToId[data.type];
@@ -190,7 +198,7 @@ jobs:
var postRes = await request(notesCollUrl, {
method: 'POST',
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
body: JSON.stringify({ text: note.text || '', type: typeId })
body: JSON.stringify({ text: note.text || '', type: typeId, script: scriptId })
});
if (postRes.ok) noteIds.push(JSON.parse(postRes.body).id);
}
@@ -3,12 +3,12 @@ name: Sync Upstream (Git Merge Strategy)
on:
schedule:
# Runs automatically every day at 2:00 AM UTC
- cron: '0 2 * * *'
- cron: "0 2 * * *"
workflow_dispatch:
# Allows manual trigger from the Actions tab
inputs:
force_sync:
description: 'Force sync even if no new commits detected'
description: "Force sync even if no new commits detected"
required: false
default: false
type: boolean
@@ -48,7 +48,7 @@ jobs:
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Configure the 'ours' merge driver for fork-specific files
# This tells git to use our version for files marked with merge=ours in .gitattributes
git config merge.ours.name "ours merge driver"
@@ -64,17 +64,17 @@ jobs:
run: |
# Get the merge base between fork and upstream
MERGE_BASE=$(git merge-base HEAD upstream/${{ env.UPSTREAM_BRANCH }} 2>/dev/null || echo "")
if [ -z "$MERGE_BASE" ]; then
echo "No common ancestor found - this may be a fresh sync"
echo "has_changes=true" >> $GITHUB_OUTPUT
echo "commit_count=unknown" >> $GITHUB_OUTPUT
exit 0
fi
# Count commits in upstream that are not in fork
UPSTREAM_COMMITS=$(git rev-list $MERGE_BASE..upstream/${{ env.UPSTREAM_BRANCH }} --count 2>/dev/null || echo "0")
if [ "$UPSTREAM_COMMITS" -eq 0 ] && [ "${{ github.event.inputs.force_sync }}" != "true" ]; then
echo "No new commits from upstream. Nothing to sync."
echo "has_changes=false" >> $GITHUB_OUTPUT
@@ -113,7 +113,7 @@ jobs:
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Configure the 'ours' merge driver
# Files with merge=ours in .gitattributes will automatically keep fork version
git config merge.ours.name "ours merge driver"
@@ -135,14 +135,14 @@ jobs:
id: merge
run: |
echo "Starting merge from upstream/${{ env.UPSTREAM_BRANCH }}..."
# Attempt merge with the 'ours' strategy for conflicts
# The .gitattributes file configures which files use merge=ours
git merge upstream/${{ env.UPSTREAM_BRANCH }} \
--no-edit \
-m "Merge upstream ${{ env.UPSTREAM_REPO }} into ${{ env.FORK_BRANCH }}" \
2>&1 || MERGE_STATUS=$?
if [ "${MERGE_STATUS:-0}" -eq 0 ]; then
echo "Merge completed successfully with no conflicts."
echo "merge_status=success" >> $GITHUB_OUTPUT
@@ -152,29 +152,70 @@ jobs:
echo "merge_status=conflicts" >> $GITHUB_OUTPUT
echo "has_conflicts=true" >> $GITHUB_OUTPUT
# List conflicted files
# List all conflicted files
CONFLICTS=$(git diff --name-only --diff-filter=U 2>/dev/null || echo "")
echo "conflicted_files<<EOF" >> $GITHUB_OUTPUT
echo "$CONFLICTS" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# Check if conflicts are only in fork-specific files
# These should have been handled by merge=ours, but check anyway
FORK_FILES=$(git diff --name-only --diff-filter=U \
-- 'install/' 'ct/' 'frontend/' 'misc/images/' '.github/workflows/' '.github/runner/' 'tools/addon/' 'README.md' 'CHANGELOG.md' 2>/dev/null || echo "")
echo "Resolving conflicts..."
if [ -n "$FORK_FILES" ]; then
echo "Warning: Fork-specific files have unexpected conflicts:"
echo "$FORK_FILES"
# For fork files, always take our version
for file in $FORK_FILES; do
# 1. Handle content conflicts in fork-specific files - always keep our version
# These are files that exist in both but have different content
FORK_CONTENT_FILES=$(git diff --name-only --diff-filter=U \
-- 'CHANGELOG.md' 'misc/tools.func' 'README.md' 2>/dev/null || echo "")
if [ -n "$FORK_CONTENT_FILES" ]; then
echo "Resolving content conflicts in fork-specific files:"
echo "$FORK_CONTENT_FILES"
for file in $FORK_CONTENT_FILES; do
echo " Keeping fork version: $file"
git checkout --ours "$file" 2>/dev/null || true
git add "$file" 2>/dev/null || true
done
fi
# For other conflicts, we leave them for manual resolution in the PR
# This allows reviewers to see what needs attention
# 2. Handle modify/delete conflicts - files deleted in fork but modified upstream
# We want to keep our deletion (remove the file)
MODIFY_DELETE_CONFLICTS=$(git status --porcelain | grep "^DU" | awk '{print $2}' 2>/dev/null || echo "")
if [ -n "$MODIFY_DELETE_CONFLICTS" ]; then
echo "Resolving modify/delete conflicts (keeping fork deletions):"
echo "$MODIFY_DELETE_CONFLICTS"
for file in $MODIFY_DELETE_CONFLICTS; do
echo " Removing file (fork deleted it): $file"
git rm --ignore-unmatch "$file" 2>/dev/null || true
done
fi
# 3. Handle any remaining content conflicts in fork directories
REMAINING_CONFLICTS=$(git diff --name-only --diff-filter=U 2>/dev/null || echo "")
if [ -n "$REMAINING_CONFLICTS" ]; then
echo "Resolving remaining conflicts in fork directories:"
echo "$REMAINING_CONFLICTS"
# For files in fork-specific directories, keep our version
FORK_DIR_CONFLICTS=$(echo "$REMAINING_CONFLICTS" | grep -E '^(install/|ct/|frontend/|misc/images/|tools/addon/)' || true)
if [ -n "$FORK_DIR_CONFLICTS" ]; then
for file in $FORK_DIR_CONFLICTS; do
echo " Keeping fork version: $file"
git checkout --ours "$file" 2>/dev/null || true
git add "$file" 2>/dev/null || true
done
fi
fi
# Check if there are still unresolved conflicts
REMAINING=$(git diff --name-only --diff-filter=U 2>/dev/null || echo "")
if [ -n "$REMAINING" ]; then
echo "Warning: Some conflicts could not be auto-resolved:"
echo "$REMAINING"
echo "These will need manual resolution in the PR."
else
echo "All conflicts resolved successfully."
fi
else
echo "Merge failed with status: ${MERGE_STATUS}"
echo "merge_status=failed" >> $GITHUB_OUTPUT
@@ -188,22 +229,28 @@ jobs:
echo "upstream_commits<<EOF" >> $GITHUB_OUTPUT
git log HEAD..upstream/${{ env.UPSTREAM_BRANCH }} --oneline --no-merges | head -30 >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# Get changed files summary
echo "changed_files<<EOF" >> $GITHUB_OUTPUT
git diff --name-only HEAD upstream/${{ env.UPSTREAM_BRANCH }} | head -50 >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# Count files by category
TOTAL_CHANGED=$(git diff --name-only HEAD upstream/${{ env.UPSTREAM_BRANCH }} | wc -l)
echo "total_changed=$TOTAL_CHANGED" >> $GITHUB_OUTPUT
- name: Commit merge result
if: steps.merge.outputs.merge_status == 'conflicts'
run: |
# Commit the merge with resolved fork-specific conflicts
# Remaining conflicts will be visible in the PR for review
git commit -m "Merge upstream with partial conflict resolution" -m "Fork-specific files preserved, other conflicts need review" || true
# Check if there are staged changes to commit
if git diff --cached --quiet 2>/dev/null && git diff --quiet 2>/dev/null; then
echo "No changes to commit."
else
# Commit the merge with resolved conflicts
git commit -m "Merge upstream ${{ env.UPSTREAM_REPO }} with conflict resolution" \
-m "- Fork-specific files preserved (CHANGELOG.md, misc/tools.func)" \
-m "- Deleted files kept as deleted (fork deletions preserved)" \
-m "- New upstream scripts added" || echo "Commit may have already been made"
fi
- name: Push sync branch
run: |
@@ -216,56 +263,60 @@ jobs:
BRANCH="${{ steps.branch.outputs.branch_name }}"
HAS_CONFLICTS="${{ steps.merge.outputs.has_conflicts }}"
CONFLICTED_FILES="${{ steps.merge.outputs.conflicted_files }}"
# Build PR body
PR_BODY="## 🔄 Upstream Sync Summary
This PR syncs the latest changes from upstream [\`${{ env.UPSTREAM_REPO }}\`](https://github.com/${{ env.UPSTREAM_REPO }}) while preserving fork-specific customizations.
### 📊 Sync Statistics
- **Upstream commits:** ${{ needs.check-and-sync.outputs.commit_count }}
- **Files changed:** ${{ steps.summary.outputs.total_changed }}
- **Merge conflicts:** ${HAS_CONFLICTS:-none}
### 📋 Upstream Commits (last 30)
\`\`\`
${{ steps.summary.outputs.upstream_commits }}
\`\`\`
### 📁 Changed Files
<details>
<summary>Click to view changed files</summary>
\`\`\`
${{ steps.summary.outputs.changed_files }}
\`\`\`
</details>
"
# Add conflict warning if needed
# Add conflict resolution section
if [ "$HAS_CONFLICTS" = "true" ]; then
PR_BODY="$PR_BODY
### ⚠️ Merge Conflicts Detected
The following files have conflicts that need manual review:
### ⚠️ Conflicts Auto-Resolved
The following conflict resolution strategy was applied:
1. **Fork-specific content files** (CHANGELOG.md, misc/tools.func, README.md) - Kept fork version
2. **Modify/delete conflicts** - Kept fork's deletions (files removed in fork stay removed)
3. **Fork directory conflicts** (install/, ct/, frontend/) - Kept fork version
Original conflicted files:
\`\`\`
$CONFLICTED_FILES
\`\`\`
**Note:** Fork-specific files have been automatically preserved using the \`merge=ours\` strategy configured in \`.gitattributes\`.
"
else
PR_BODY="$PR_BODY
### ✅ Clean Merge
No conflicts detected. Fork-specific files were automatically preserved using the \`merge=ours\` strategy.
No conflicts detected. All changes merged cleanly.
"
fi
# Add preserved files section
PR_BODY="$PR_BODY
### 🔒 Fork-Specific Files Preserved
The following files/directories are configured in \`.gitattributes\` to always keep the fork version:
- \`install/\` - Custom install scripts
- \`ct/\` - Custom container scripts
@@ -276,25 +327,24 @@ jobs:
- \`tools/addon/\` - Custom addon tools
- \`README.md\` - Fork-specific documentation
- \`CHANGELOG.md\` - Fork-specific changelog
### ✅ Pre-Merge Checklist
- [ ] Review all changed files for unexpected modifications
- [ ] Verify fork-specific files are intact
- [ ] Resolve any remaining merge conflicts
- [ ] Test any new scripts or features from upstream
---
*This PR was automatically created by the [upstream-sync workflow](https://github.com/${{ github.repository }}/blob/main/.github/workflows/upstream-sync.yml).*
"
# Create PR title with conflict indicator
if [ "$HAS_CONFLICTS" = "true" ]; then
TITLE="🔄 Upstream Sync - $(date +%Y-%m-%d) ⚠️ CONFLICTS"
TITLE="🔄 Upstream Sync - $(date +%Y-%m-%d) ✅ Conflicts Resolved"
else
TITLE="🔄 Upstream Sync - $(date +%Y-%m-%d)"
fi
# Create the PR (labels are optional - will fail silently if labels don't exist)
# Create the PR
gh pr create \
--title "$TITLE" \
--body "$PR_BODY" \

Some files were not shown because too many files have changed in this diff Show More