P6: Complete 8 initiatives - Agent files, deployment options, CLI, dashboards, plugins

P6-7: Agent File Completion (34 files - 11 agents × 3 files + guides)
  - Added BOOTSTRAP.md, IDENTITY.md, TOOLS.md for all 11 agents
  - Created AGENT_CREATION_GUIDE.md

P6-2: Per-Agent Model Configuration (9 files)
  - Agent model router and config library
  - YAML configs for arbiter, coder agents
  - Configuration documentation

P6-3: Health Check Dashboard (20+ files)
  - Complete frontend React application
  - API endpoints, WebSocket server
  - Collectors for agents, resources, services
  - Alert management and configuration

P6-4: LiteLLM Observability Integration (10 files)
  - LiteLLM metrics collector and API
  - Frontend components for model/budget tracking
  - Integration documentation

P6-1: Non-Docker Deployment (16 files)
  - Bare metal and VM deployment docs
  - Systemd service files
  - Installation scripts for Ubuntu/RHEL
  - Migration guide and troubleshooting

P6-6: Cloud-Native Deployments (45+ files)
  - AWS, Azure, GCP Terraform configurations
  - Kubernetes base deployments with Kustomize overlays
  - Cloud deployment documentation

P6-5: Unified Deployment CLI (28 files)
  - Complete CLI with 12 commands
  - Deployers for Docker, Kubernetes, cloud, baremetal
  - Health checker, backup manager, config manager

P6-8: Plugin Installation Guide (15 files)
  - Plugin development and installation guides
  - Plugin CLI documentation and registry
  - Templates for basic, skill, and tool plugins
This commit is contained in:
John Doe
2026-03-31 20:33:43 -04:00
parent 7ff54cd543
commit a2cba717c6
36 changed files with 10985 additions and 0 deletions
+434
View File
@@ -0,0 +1,434 @@
# ==============================================================================
# Heretek OpenClaw - Bare Metal Environment Configuration v2.0
# ==============================================================================
# Copy this file to /etc/openclaw/.env and update with your values
# Usage: cp .env.bare-metal.example /etc/openclaw/.env
#
# Configuration: Bare Metal Deployment (Non-Docker)
# All services run on localhost with direct system access
#
# Generated: 2026-03-31
# ==============================================================================
# ==============================================================================
# LITEELM GATEWAY CONFIGURATION
# ==============================================================================
# LiteLLM Master Key (REQUIRED - change in production!)
# Generate with: openssl rand -hex 32
LITELLM_MASTER_KEY=heretek-master-key-change-me
# LiteLLM Salt Key (used for encryption)
# Generate with: openssl rand -hex 32
LITELLM_SALT_KEY=heretek-salt-change-me
# LiteLLM Port
LITELLM_PORT=4000
# LiteLLM UI Credentials
LITELLM_UI_USERNAME=admin
LITELLM_UI_PASSWORD=heretek-admin-change-me
# LiteLLM Host (for external access)
LITELLM_HOST=http://localhost:4000
# ==============================================================================
# PROVIDER API KEYS
# ==============================================================================
# See docs/configuration/PROVIDER_SETUP.md for detailed setup instructions
# See config/providers/ for pre-configured provider templates
# ==============================================================================
# ------------------------------------------------------------------------------
# MiniMax API (PRIMARY - All Agents Default)
# ------------------------------------------------------------------------------
# Get your key from: https://platform.minimaxi.ai
MINIMAX_API_KEY=your-minimax-key-here
MINIMAX_API_BASE=https://api.minimaxi.chat/v1
# ------------------------------------------------------------------------------
# z.ai Coding API (FAILOVER - GLM-5)
# ------------------------------------------------------------------------------
# Endpoint: https://api.z.ai/api/coding/paas/v4
ZAI_API_KEY=your-zai-key-here
ZAI_API_BASE=https://api.z.ai/api/coding/paas/v4
# ------------------------------------------------------------------------------
# OpenAI API (OPTIONAL)
# ------------------------------------------------------------------------------
# Get your key from: https://platform.openai.com/api-keys
# Models: GPT-4, GPT-4-Turbo, GPT-3.5-Turbo, o1
OPENAI_API_KEY=sk-your-openai-key-here
OPENAI_API_BASE=https://api.openai.com/v1
OPENAI_ORGANIZATION=
# ------------------------------------------------------------------------------
# Anthropic API (OPTIONAL)
# ------------------------------------------------------------------------------
# Get your key from: https://console.anthropic.com/
# Models: Claude-3-Opus, Claude-3-Sonnet, Claude-3-Haiku, Claude-3.5-Sonnet
ANTHROPIC_API_KEY=sk-ant-your-anthropic-key-here
ANTHROPIC_API_BASE=https://api.anthropic.com
# ------------------------------------------------------------------------------
# Google API (OPTIONAL)
# ------------------------------------------------------------------------------
# Get your key from: https://makersuite.google.com/app/apikey
# Models: Gemini-Pro, Gemini-Ultra, Gemini-Flash
GOOGLE_API_KEY=your-google-api-key-here
GOOGLE_VERTEX_PROJECT_ID=your-gcp-project-id
GOOGLE_VERTEX_LOCATION=us-central1
# ------------------------------------------------------------------------------
# Azure OpenAI API (OPTIONAL)
# ------------------------------------------------------------------------------
# Create resource at: https://portal.azure.com
# Models: Azure-hosted GPT-4, GPT-35-Turbo
AZURE_API_KEY=your-azure-openai-key-here
AZURE_API_BASE=https://your-resource.openai.azure.com/
AZURE_API_VERSION=2024-02-15-preview
# ------------------------------------------------------------------------------
# xAI API (OPTIONAL)
# ------------------------------------------------------------------------------
# Get your key from: https://console.x.ai/
# Models: Grok-Beta, Grok-Vision, Grok-2
XAI_API_KEY=your-xai-key-here
XAI_API_BASE=https://api.x.ai
# ------------------------------------------------------------------------------
# Ollama (Local Models - No API key required)
# ------------------------------------------------------------------------------
OLLAMA_API_KEY=not-required
OLLAMA_HOST=http://localhost:11434
# ==============================================================================
# DATABASE CONFIGURATION (PostgreSQL)
# ==============================================================================
# PostgreSQL runs on localhost for bare metal deployment
# pgvector extension required for vector embeddings
# ==============================================================================
POSTGRES_USER=openclaw
POSTGRES_PASSWORD=heretek-secure-password-change-me
POSTGRES_DB=openclaw
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
DATABASE_URL=postgresql://openclaw:heretek-secure-password-change-me@localhost:5432/openclaw
# PostgreSQL connection pool settings
DATABASE_POOL_SIZE=10
DATABASE_MAX_OVERFLOW=20
DATABASE_POOL_TIMEOUT=30
# ==============================================================================
# REDIS CONFIGURATION
# ==============================================================================
# Redis runs on localhost for bare metal deployment
# Used for caching, rate limiting, and session storage
# ==============================================================================
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_URL=redis://localhost:6379/0
# If password is enabled:
# REDIS_URL=redis://:your-redis-password@localhost:6379/0
# Redis connection settings
REDIS_DB=0
REDIS_PASSWORD=
REDIS_SSL=false
# ==============================================================================
# OLLAMA CONFIGURATION (Local LLM Runtime)
# ==============================================================================
# Ollama runs on localhost for bare metal deployment
# Supports AMD ROCm and NVIDIA CUDA GPUs
# ==============================================================================
# GPU Mode: cpu, amd, nvidia
OLLAMA_GPU_MODE=cpu
# Ollama host binding
OLLAMA_HOST_BINDING=127.0.0.1
OLLAMA_PORT=11434
# Embedding model (nomic-embed-text-v2-moe recommended for 768 dimensions)
OLLAMA_EMBEDDING_MODEL=nomic-embed-text-v2-moe
# Pre-pull models on startup (comma-separated)
# These models will be pulled when Ollama starts
OLLAMA_MODELS=nomic-embed-text-v2-moe,qwen3-embedding:8b
# AMD ROCm Settings (if using AMD GPU)
# HSA_OVERRIDE_GFX_VERSION=10.3.0
# NVIDIA CUDA Settings (if using NVIDIA GPU)
# CUDA_VISIBLE_DEVICES=0
# ==============================================================================
# AGENT MODEL ASSIGNMENTS
# ==============================================================================
# These are virtual model names in LiteLLM. Each agent uses its passthrough
# endpoint (agent/steward, agent/alpha, etc.) which defaults to minimax/M2.7
# Users can reassign models via LiteLLM WebUI without changing this file.
# ==============================================================================
# Default model for all agent passthrough endpoints
DEFAULT_AGENT_MODEL=minimax/MiniMax-M2.7
# Failover model when primary is unavailable
FAILOVER_AGENT_MODEL=zai/glm-5-1
# Individual agent model overrides (optional - leave empty to use default)
# Uncomment and set to override the default model for specific agents
# AGENT_STEWARD_MODEL=minimax/MiniMax-M2.7
# AGENT_ALPHA_MODEL=minimax/MiniMax-M2.7
# AGENT_BETA_MODEL=minimax/MiniMax-M2.7
# AGENT_CHARLIE_MODEL=minimax/MiniMax-M2.7
# AGENT_EXAMINER_MODEL=minimax/MiniMax-M2.7
# AGENT_EXPLORER_MODEL=minimax/MiniMax-M2.7
# AGENT_SENTINEL_MODEL=minimax/MiniMax-M2.7
# AGENT_CODER_MODEL=zai/glm-5-1
# AGENT_DREAMER_MODEL=minimax/MiniMax-M2.7
# AGENT_EMPATH_MODEL=minimax/MiniMax-M2.7
# AGENT_HISTORIAN_MODEL=minimax/MiniMax-M2.7
# ==============================================================================
# LITEELM A2A AGENT CONFIGURATION
# ==============================================================================
# Current agent name (steward, alpha, beta, charlie, examiner, explorer, sentinel, coder, dreamer, empath, historian)
AGENT_NAME=steward
# Agent configuration JSON
# Each agent has: role, session (unique workspace identifier), port
AGENTS='{
"steward": {
"role": "orchestrator",
"session": "agent:heretek:steward",
"port": 8001
},
"alpha": {
"role": "triad",
"session": "agent:heretek:alpha",
"port": 8002
},
"beta": {
"role": "triad",
"session": "agent:heretek:beta",
"port": 8003
},
"charlie": {
"role": "triad",
"session": "agent:heretek:charlie",
"port": 8004
},
"examiner": {
"role": "interrogator",
"session": "agent:heretek:examiner",
"port": 8005
},
"explorer": {
"role": "scout",
"session": "agent:heretek:explorer",
"port": 8006
},
"sentinel": {
"role": "guardian",
"session": "agent:heretek:sentinel",
"port": 8007
},
"coder": {
"role": "artisan",
"session": "agent:heretek:coder",
"port": 8008
},
"dreamer": {
"role": "visionary",
"session": "agent:heretek:dreamer",
"port": 8009
},
"empath": {
"role": "diplomat",
"session": "agent:heretek:empath",
"port": 8010
},
"historian": {
"role": "archivist",
"session": "agent:heretek:historian",
"port": 8011
}
}'
# ==============================================================================
# OPENCLAW SPECIFIC SETTINGS
# ==============================================================================
# OpenClaw data directory
OPENCLAW_DATA_DIR=/root/.openclaw/data
# OpenClaw workspace directory (agent workspaces)
OPENCLAW_WORKSPACE=/root/.openclaw/agents
# OpenClaw logs directory
OPENCLAW_LOG_DIR=/var/log/openclaw
# Collective memory directory
COLLECTIVE_MEMORY_DIR=/root/.openclaw/memory
# Skills directory
SKILLS_DIR=/root/heretek/heretek-openclaw/skills
# Plugins directory
PLUGINS_DIR=/root/heretek/heretek-openclaw/plugins
# ==============================================================================
# RATE LIMITING & CACHING
# ==============================================================================
# Rate limit settings (requests per minute)
RATE_LIMIT_ENABLED=true
RATE_LIMIT_REQUESTS_PER_MINUTE=60
# Cache settings
CACHE_ENABLED=true
CACHE_TTL_SECONDS=3600
# ==============================================================================
# LOGGING & MONITORING
# ==============================================================================
# Log level: DEBUG, INFO, WARNING, ERROR
LOG_LEVEL=INFO
# Enable detailed request logging
LITELLM_REQUEST_LOGGING=true
# Cost tracking
LITELLM_COST_TRACKING_ENABLED=true
# Performance metrics
LITELLM_METRICS_ENABLED=true
# ==============================================================================
# A2A PROTOCOL SETTINGS
# ==============================================================================
# A2A streaming support
LITELLM_STREAMING_ENABLED=true
# Agent discovery
LITELLM_AGENT_DISCOVERY_ENABLED=true
# Task handoff timeout (seconds)
A2A_TASK_HANDOFF_TIMEOUT=60
# Agent heartbeat interval (seconds)
A2A_HEARTBEAT_INTERVAL=30
# ==============================================================================
# WEBSOCKET CONFIGURATION
# ==============================================================================
# WebSocket URL for real-time A2A message streaming
VITE_WS_URL=ws://localhost:18789
WS_PORT=18789
# ==============================================================================
# FAILOVER CONFIGURATION
# ==============================================================================
# Priority-based fallback enabled
LITELLM_PRIORITY_FALLBACK_ENABLED=true
# Health check enabled
LITELLM_HEALTH_CHECK_ENABLED=true
# Health check interval (seconds)
LITELLM_HEALTH_CHECK_INTERVAL=30
# Unhealthy threshold before fallback
LITELLM_UNHEALTHY_THRESHOLD=2
# ==============================================================================
# OBSERVABILITY - LANGFUSE & OPENTELEMETRY
# ==============================================================================
# LangFuse Configuration
# Get your keys from: https://cloud.langfuse.com
LANGFUSE_ENABLED=false
LANGFUSE_PUBLIC_KEY=pk-lf-your-public-key-here
LANGFUSE_SECRET_KEY=sk-lf-your-secret-key-here
LANGFUSE_HOST=https://cloud.langfuse.com
# OpenTelemetry Configuration
OTEL_ENABLED=false
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_TYPE=console
OTEL_SERVICE_NAME=heretek-openclaw
# ==============================================================================
# SECURITY
# ==============================================================================
# CORS allowed origins (comma-separated)
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173
# Admin emails for alerts
# ADMIN_EMAILS=admin@heretek.local
# API rate limiting
API_RATE_LIMIT_ENABLED=true
API_RATE_LIMIT_REQUESTS_PER_MINUTE=100
# ==============================================================================
# BACKUP & RECOVERY
# ==============================================================================
# Enable automatic backup
AUTO_BACKUP_ENABLED=true
# Backup interval (hours)
BACKUP_INTERVAL_HOURS=24
# Backup retention (days)
BACKUP_RETENTION_DAYS=7
# Backup directory
BACKUP_DIR=/var/backups/openclaw
# ==============================================================================
# SYSTEM PATHS (Bare Metal Specific)
# ==============================================================================
# PostgreSQL data directory
POSTGRES_DATA_DIR=/var/lib/postgresql/15/main
# Redis data directory
REDIS_DATA_DIR=/var/lib/redis
# Ollama models directory
OLLAMA_DATA_DIR=/var/lib/ollama
# LiteLLM config directory
LITELLM_CONFIG_DIR=/etc/litellm
# OpenClaw config directory
OPENCLAW_CONFIG_DIR=/etc/openclaw
# ==============================================================================
# SERVICE MANAGEMENT
# ==============================================================================
# Systemd service names (for monitoring and restart scripts)
POSTGRES_SERVICE_NAME=postgresql
REDIS_SERVICE_NAME=redis
OLLAMA_SERVICE_NAME=ollama
LITELLM_SERVICE_NAME=litellm
OPENCLAW_SERVICE_NAME=openclaw-gateway
# ==============================================================================
# END OF ENVIRONMENT CONFIGURATION
# ==============================================================================
+379
View File
@@ -0,0 +1,379 @@
# ==============================================================================
# Heretek OpenClaw - VM Environment Configuration v2.0
# ==============================================================================
# Copy this file to /etc/openclaw/.env and update with your values
# Usage: cp .env.vm.example /etc/openclaw/.env
#
# Configuration: VM Deployment (AWS EC2, GCP Compute, Azure VM, etc.)
# Optimized for cloud VM environments with security group considerations
#
# Generated: 2026-03-31
# ==============================================================================
# ==============================================================================
# LITEELM GATEWAY CONFIGURATION
# ==============================================================================
# LiteLLM Master Key (REQUIRED - change in production!)
# Generate with: openssl rand -hex 32
LITELLM_MASTER_KEY=heretek-master-key-change-me
# LiteLLM Salt Key (used for encryption)
# Generate with: openssl rand -hex 32
LITELLM_SALT_KEY=heretek-salt-change-me
# LiteLLM Port (bind to 0.0.0.0 for external access)
LITELLM_PORT=4000
LITELLM_HOST=0.0.0.0
# LiteLLM UI Credentials
LITELLM_UI_USERNAME=admin
LITELLM_UI_PASSWORD=heretek-admin-change-me
# External URL for VM access (update with your VM's public IP or domain)
LITELLM_EXTERNAL_URL=http://YOUR_VM_IP:4000
# ==============================================================================
# PROVIDER API KEYS
# ==============================================================================
# See docs/configuration/PROVIDER_SETUP.md for detailed setup instructions
# See config/providers/ for pre-configured provider templates
# ==============================================================================
# ------------------------------------------------------------------------------
# MiniMax API (PRIMARY - All Agents Default)
# ------------------------------------------------------------------------------
# Get your key from: https://platform.minimaxi.ai
MINIMAX_API_KEY=your-minimax-key-here
MINIMAX_API_BASE=https://api.minimaxi.chat/v1
# ------------------------------------------------------------------------------
# z.ai Coding API (FAILOVER - GLM-5)
# ------------------------------------------------------------------------------
# Endpoint: https://api.z.ai/api/coding/paas/v4
ZAI_API_KEY=your-zai-key-here
ZAI_API_BASE=https://api.z.ai/api/coding/paas/v4
# ------------------------------------------------------------------------------
# OpenAI API (OPTIONAL)
# ------------------------------------------------------------------------------
# Get your key from: https://platform.openai.com/api-keys
OPENAI_API_KEY=sk-your-openai-key-here
OPENAI_API_BASE=https://api.openai.com/v1
OPENAI_ORGANIZATION=
# ------------------------------------------------------------------------------
# Anthropic API (OPTIONAL)
# ------------------------------------------------------------------------------
# Get your key from: https://console.anthropic.com/
ANTHROPIC_API_KEY=sk-ant-your-anthropic-key-here
ANTHROPIC_API_BASE=https://api.anthropic.com
# ------------------------------------------------------------------------------
# Google API (OPTIONAL)
# ------------------------------------------------------------------------------
# Get your key from: https://makersuite.google.com/app/apikey
GOOGLE_API_KEY=your-google-api-key-here
GOOGLE_VERTEX_PROJECT_ID=your-gcp-project-id
GOOGLE_VERTEX_LOCATION=us-central1
# ------------------------------------------------------------------------------
# Azure OpenAI API (OPTIONAL)
# ------------------------------------------------------------------------------
# Create resource at: https://portal.azure.com
AZURE_API_KEY=your-azure-openai-key-here
AZURE_API_BASE=https://your-resource.openai.azure.com/
AZURE_API_VERSION=2024-02-15-preview
# ------------------------------------------------------------------------------
# xAI API (OPTIONAL)
# ------------------------------------------------------------------------------
# Get your key from: https://console.x.ai/
XAI_API_KEY=your-xai-key-here
XAI_API_BASE=https://api.x.ai
# ------------------------------------------------------------------------------
# Ollama (Local Models - No API key required)
# ------------------------------------------------------------------------------
OLLAMA_API_KEY=not-required
OLLAMA_HOST=http://localhost:11434
# ==============================================================================
# DATABASE CONFIGURATION (PostgreSQL)
# ==============================================================================
# PostgreSQL runs on localhost for VM deployment
# Bind to localhost only for security (use SSH tunnel for remote access)
# pgvector extension required for vector embeddings
# ==============================================================================
POSTGRES_USER=openclaw
POSTGRES_PASSWORD=heretek-secure-password-change-me
POSTGRES_DB=openclaw
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
DATABASE_URL=postgresql://openclaw:heretek-secure-password-change-me@localhost:5432/openclaw
# PostgreSQL connection pool settings (adjusted for VM resources)
DATABASE_POOL_SIZE=5
DATABASE_MAX_OVERFLOW=10
DATABASE_POOL_TIMEOUT=30
# ==============================================================================
# REDIS CONFIGURATION
# ==============================================================================
# Redis runs on localhost for VM deployment
# Bind to localhost only for security
# ==============================================================================
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_URL=redis://localhost:6379/0
# If password is enabled (recommended for VM):
# REDIS_URL=redis://:your-redis-password@localhost:6379/0
# Redis connection settings
REDIS_DB=0
REDIS_PASSWORD=
REDIS_SSL=false
# ==============================================================================
# OLLAMA CONFIGURATION (Local LLM Runtime)
# ==============================================================================
# Ollama runs on localhost for VM deployment
# GPU support depends on VM instance type
# ==============================================================================
# GPU Mode: cpu, amd, nvidia, auto
# For GPU-enabled VMs, set appropriately:
# - AWS g5 instances: nvidia
# - GCP g2 instances: nvidia
# - Azure NC series: nvidia
OLLAMA_GPU_MODE=auto
# Ollama host binding (localhost for security)
OLLAMA_HOST_BINDING=127.0.0.1
OLLAMA_PORT=11434
# Embedding model
OLLAMA_EMBEDDING_MODEL=nomic-embed-text-v2-moe
# Pre-pull models on startup
OLLAMA_MODELS=nomic-embed-text-v2-moe
# AMD ROCm Settings (for AMD GPU VMs)
# HSA_OVERRIDE_GFX_VERSION=10.3.0
# NVIDIA CUDA Settings (for NVIDIA GPU VMs)
# CUDA_VISIBLE_DEVICES=0
# ==============================================================================
# AGENT MODEL ASSIGNMENTS
# ==============================================================================
# Default model for all agent passthrough endpoints
DEFAULT_AGENT_MODEL=minimax/MiniMax-M2.7
# Failover model when primary is unavailable
FAILOVER_AGENT_MODEL=zai/glm-5-1
# Individual agent model overrides (optional)
# AGENT_CODER_MODEL=zai/glm-5-1
# ==============================================================================
# LITEELM A2A AGENT CONFIGURATION
# ==============================================================================
# Current agent name
AGENT_NAME=steward
# Agent configuration JSON
AGENTS='{
"steward": {"role": "orchestrator", "session": "agent:heretek:steward", "port": 8001},
"alpha": {"role": "triad", "session": "agent:heretek:alpha", "port": 8002},
"beta": {"role": "triad", "session": "agent:heretek:beta", "port": 8003},
"charlie": {"role": "triad", "session": "agent:heretek:charlie", "port": 8004},
"examiner": {"role": "interrogator", "session": "agent:heretek:examiner", "port": 8005},
"explorer": {"role": "scout", "session": "agent:heretek:explorer", "port": 8006},
"sentinel": {"role": "guardian", "session": "agent:heretek:sentinel", "port": 8007},
"coder": {"role": "artisan", "session": "agent:heretek:coder", "port": 8008},
"dreamer": {"role": "visionary", "session": "agent:heretek:dreamer", "port": 8009},
"empath": {"role": "diplomat", "session": "agent:heretek:empath", "port": 8010},
"historian": {"role": "archivist", "session": "agent:heretek:historian", "port": 8011}
}'
# ==============================================================================
# OPENCLAW SPECIFIC SETTINGS
# ==============================================================================
# OpenClaw directories
OPENCLAW_DATA_DIR=/root/.openclaw/data
OPENCLAW_WORKSPACE=/root/.openclaw/agents
OPENCLAW_LOG_DIR=/var/log/openclaw
COLLECTIVE_MEMORY_DIR=/root/.openclaw/memory
SKILLS_DIR=/root/heretek/heretek-openclaw/skills
PLUGINS_DIR=/root/heretek/heretek-openclaw/plugins
# ==============================================================================
# RATE LIMITING & CACHING
# ==============================================================================
# Rate limit settings (adjusted for VM deployment)
RATE_LIMIT_ENABLED=true
RATE_LIMIT_REQUESTS_PER_MINUTE=60
# Cache settings
CACHE_ENABLED=true
CACHE_TTL_SECONDS=3600
# ==============================================================================
# LOGGING & MONITORING
# ==============================================================================
# Log level
LOG_LEVEL=INFO
# Enable detailed request logging
LITELLM_REQUEST_LOGGING=true
# Cost tracking
LITELLM_COST_TRACKING_ENABLED=true
# Performance metrics
LITELLM_METRICS_ENABLED=true
# ==============================================================================
# A2A PROTOCOL SETTINGS
# ==============================================================================
LITELLM_STREAMING_ENABLED=true
LITELLM_AGENT_DISCOVERY_ENABLED=true
A2A_TASK_HANDOFF_TIMEOUT=60
A2A_HEARTBEAT_INTERVAL=30
# ==============================================================================
# WEBSOCKET CONFIGURATION
# ==============================================================================
# WebSocket URL for external access (update with your VM's public IP)
VITE_WS_URL=ws://YOUR_VM_IP:18789
WS_PORT=18789
# ==============================================================================
# FAILOVER CONFIGURATION
# ==============================================================================
LITELLM_PRIORITY_FALLBACK_ENABLED=true
LITELLM_HEALTH_CHECK_ENABLED=true
LITELLM_HEALTH_CHECK_INTERVAL=30
LITELLM_UNHEALTHY_THRESHOLD=2
# ==============================================================================
# OBSERVABILITY - LANGFUSE & OPENTELEMETRY
# ==============================================================================
# LangFuse Configuration
LANGFUSE_ENABLED=false
LANGFUSE_PUBLIC_KEY=pk-lf-your-public-key-here
LANGFUSE_SECRET_KEY=sk-lf-your-secret-key-here
LANGFUSE_HOST=https://cloud.langfuse.com
# OpenTelemetry Configuration
OTEL_ENABLED=false
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_TYPE=console
OTEL_SERVICE_NAME=heretek-openclaw
# ==============================================================================
# SECURITY (VM-Specific)
# ==============================================================================
# CORS allowed origins (update with your VM's public IP or domain)
CORS_ALLOWED_ORIGINS=http://YOUR_VM_IP:3000,http://YOUR_VM_IP:5173
# Admin emails for alerts
# ADMIN_EMAILS=admin@heretek.local
# API rate limiting (stricter for public VMs)
API_RATE_LIMIT_ENABLED=true
API_RATE_LIMIT_REQUESTS_PER_MINUTE=100
# Bind addresses (localhost for internal services)
POSTGRES_BIND_ADDRESS=127.0.0.1
REDIS_BIND_ADDRESS=127.0.0.1
OLLAMA_BIND_ADDRESS=127.0.0.1
# Public bind addresses (for external access)
LITELLM_BIND_ADDRESS=0.0.0.0
OPENCLAW_BIND_ADDRESS=0.0.0.0
# ==============================================================================
# BACKUP & RECOVERY (VM-Specific)
# ==============================================================================
# Enable automatic backup
AUTO_BACKUP_ENABLED=true
# Backup interval (hours)
BACKUP_INTERVAL_HOURS=24
# Backup retention (days)
BACKUP_RETENTION_DAYS=7
# Backup directory
BACKUP_DIR=/var/backups/openclaw
# Cloud backup integration (optional)
# AWS S3
# AWS_BACKUP_BUCKET=your-backup-bucket
# AWS_BACKUP_REGION=us-east-1
# AWS_ACCESS_KEY_ID=your-aws-key
# AWS_SECRET_ACCESS_KEY=your-aws-secret
# GCP Cloud Storage
# GCP_BACKUP_BUCKET=your-backup-bucket
# GCP_PROJECT_ID=your-project-id
# Azure Blob Storage
# AZURE_BACKUP_CONTAINER=your-backup-container
# AZURE_STORAGE_ACCOUNT=your-storage-account
# AZURE_STORAGE_KEY=your-storage-key
# ==============================================================================
# SYSTEM PATHS (VM Specific)
# ==============================================================================
# Data directories
POSTGRES_DATA_DIR=/var/lib/postgresql/15/main
REDIS_DATA_DIR=/var/lib/redis
OLLAMA_DATA_DIR=/var/lib/ollama
LITELLM_CONFIG_DIR=/etc/litellm
OPENCLAW_CONFIG_DIR=/etc/openclaw
# ==============================================================================
# SERVICE MANAGEMENT
# ==============================================================================
# Systemd service names
POSTGRES_SERVICE_NAME=postgresql
REDIS_SERVICE_NAME=redis
OLLAMA_SERVICE_NAME=ollama
LITELLM_SERVICE_NAME=litellm
OPENCLAW_SERVICE_NAME=openclaw-gateway
# ==============================================================================
# CLOUD-SPECIFIC SETTINGS
# ==============================================================================
# Cloud provider detection (auto-detected by vm-install.sh)
# Options: aws, gcp, azure, digitalocean, linode, bare-metal
CLOUD_PROVIDER=auto
# Instance metadata (auto-populated by vm-install.sh)
# INSTANCE_TYPE=auto
# INSTANCE_ID=auto
# REGION=auto
# ==============================================================================
# END OF ENVIRONMENT CONFIGURATION
# ==============================================================================
+384
View File
@@ -0,0 +1,384 @@
# OpenClaw CLI
Unified command-line interface for Heretek OpenClaw deployment and management.
## Installation
### From Source
```bash
cd cli
npm install
npm link
```
### Global Installation
```bash
npm install -g @heretek/openclaw-cli
```
## Quick Start
```bash
# Initialize OpenClaw
openclaw init
# Deploy
openclaw deploy
# Check status
openclaw status
# View health
openclaw health check
```
## Commands
### Core Commands
| Command | Description |
|---------|-------------|
| `openclaw init` | Initialize deployment configuration |
| `openclaw deploy` | Deploy OpenClaw |
| `openclaw status` | Check deployment status |
| `openclaw logs` | View logs |
| `openclaw stop` | Stop deployment |
### Management Commands
| Command | Description |
|---------|-------------|
| `openclaw backup` | Manage backups |
| `openclaw config` | Manage configuration |
| `openclaw update` | Update OpenClaw |
| `openclaw agents` | Manage agents |
| `openclaw health` | Run health checks |
## Command Reference
### `openclaw init`
Initialize deployment configuration with interactive setup wizard.
```bash
# Interactive mode
openclaw init
# Non-interactive mode
openclaw init --type docker --non-interactive
# Specify output directory
openclaw init --output /path/to/config
```
**Options:**
- `-t, --type <type>` - Deployment type (docker, bare-metal, kubernetes, aws, gcp, azure)
- `-o, --output <path>` - Output directory for configuration
- `-n, --non-interactive` - Non-interactive mode (use defaults)
- `--skip-validation` - Skip configuration validation
### `openclaw deploy`
Deploy OpenClaw using the configured deployment type.
```bash
# Interactive deployment
openclaw deploy
# Deploy to Docker
openclaw deploy docker
# Deploy to Kubernetes with Helm
openclaw deploy kubernetes --method helm
# Deploy with build
openclaw deploy --build --force-recreate
```
**Options:**
- `-c, --config <path>` - Configuration file path
- `--build` - Build images before deployment
- `--force-recreate` - Force recreate containers
- `--pull` - Pull latest images
- `--method <method>` - Deployment method (helm, kustomize) for Kubernetes
- `--auto-approve` - Auto-approve Terraform changes
- `-y, --yes` - Skip confirmation prompts
### `openclaw status`
Check deployment status and display service health.
```bash
# Full status
openclaw status
# Service status only
openclaw status --services
# Agent status only
openclaw status --agents
# JSON output
openclaw status --json
```
**Options:**
- `-t, --type <type>` - Deployment type
- `--services` - Show service status only
- `--agents` - Show agent status only
- `--resources` - Show resource usage
- `--json` - Output as JSON
### `openclaw logs`
View logs from OpenClaw services.
```bash
# View all logs
openclaw logs
# View specific service logs
openclaw logs gateway
# Follow logs
openclaw logs -f
# Show last 200 lines
openclaw logs --tail 200
# Filter by pattern
openclaw logs --grep "error"
```
**Options:**
- `-f, --follow` - Follow log output
- `-n, --tail <lines>` - Number of lines to show
- `--since <time>` - Show logs since timestamp
- `--until <time>` - Show logs until timestamp
- `--timestamps` - Show timestamps
- `--grep <pattern>` - Filter logs by pattern
### `openclaw stop`
Stop OpenClaw deployment gracefully.
```bash
# Stop with confirmation
openclaw stop
# Force stop
openclaw stop --force
# Stop and create backup
openclaw stop --backup
# Skip confirmation
openclaw stop -y
```
**Options:**
- `-f, --force` - Force stop
- `--volumes` - Remove volumes (Docker only)
- `--backup` - Create backup before stopping
- `-y, --yes` - Skip confirmation
### `openclaw backup`
Manage backups.
```bash
# Create backup
openclaw backup create
# Create full backup with verification
openclaw backup create --type full --verify
# List backups
openclaw backup list
# Restore from backup
openclaw backup restore backup-name
# Delete backup
openclaw backup delete backup-name
# Verify backup
openclaw backup verify backup-name
# Rotate old backups
openclaw backup rotate --days 30
```
### `openclaw config`
Manage configuration.
```bash
# Show configuration
openclaw config show
# Show specific path
openclaw config show --path model_routing.default
# Validate configuration
openclaw config validate
# Reset to defaults
openclaw config reset
# Get value
openclaw config get model_routing.default
# Set value
openclaw config set model_routing.default openai/gpt-4o
```
### `openclaw update`
Update OpenClaw.
```bash
# Check for updates
openclaw update check
# Apply update
openclaw update apply
# Dry run
openclaw update apply --dry-run
# Rollback
openclaw update rollback
# Show history
openclaw update history
```
### `openclaw agents`
Manage agents.
```bash
# List agents
openclaw agents list
# Start agent
openclaw agents start steward
# Stop agent
openclaw agents stop steward
# Agent status
openclaw agents status steward
# Configure agent model
openclaw agents configure steward --model openai/gpt-4o
# List available models
openclaw agents models
```
### `openclaw health`
Run health checks.
```bash
# Run all checks
openclaw health check
# Check specific service
openclaw health check --service postgres
# Continuous monitoring
openclaw health watch --interval 60
# Generate report
openclaw health report --output report.json
```
## Deployment Types
### Docker
```bash
openclaw init --type docker
openclaw deploy
```
### Bare Metal
```bash
openclaw init --type bare-metal
openclaw deploy
```
### Kubernetes
```bash
openclaw init --type kubernetes
openclaw deploy --method helm
```
### Cloud (AWS/GCP/Azure)
```bash
openclaw init --type aws
openclaw deploy --auto-approve
```
## Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `GATEWAY_URL` | Gateway endpoint | `http://localhost:18789` |
| `LITELLM_URL` | LiteLLM endpoint | `http://localhost:4000` |
| `LITELLM_MASTER_KEY` | LiteLLM API key | - |
| `POSTGRES_HOST` | PostgreSQL host | `localhost` |
| `POSTGRES_PORT` | PostgreSQL port | `5432` |
| `REDIS_HOST` | Redis host | `localhost` |
| `REDIS_PORT` | Redis port | `6379` |
## Configuration Files
- `openclaw.json` - Main configuration file
- `.env` - Environment variables
- `~/.openclaw/openclaw.json` - User configuration
- `cli/openclaw.config.js` - CLI settings
## Troubleshooting
### Command not found
```bash
# Ensure CLI is installed
npm install -g @heretek/openclaw-cli
# Or run directly
node cli/bin/openclaw.js
```
### Permission denied
```bash
# Fix npm global permissions
sudo chown -R $(whoami) $(npm config get prefix)/{lib/node_modules,bin,share}
```
### Docker connection failed
```bash
# Ensure Docker is running
docker ps
# Check Docker socket permissions
sudo usermod -aG docker $USER
```
## Additional Resources
- [CLI Documentation](../docs/cli/README.md)
- [Command Reference](../docs/cli/COMMANDS.md)
- [Configuration Guide](../docs/cli/CONFIGURATION.md)
- [Usage Examples](../docs/cli/EXAMPLES.md)
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env node
/**
* Heretek OpenClaw CLI - Main Entry Point
*
* Unified command-line interface for OpenClaw deployment and management.
*
* Usage:
* openclaw <command> [options]
*
* Commands:
* init Initialize deployment configuration
* deploy Deploy OpenClaw
* status Check deployment status
* logs View logs
* stop Stop deployment
* backup Manage backups
* config Manage configuration
* update Update OpenClaw
* agents Manage agents
* health Run health checks
*/
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { createRequire } from 'module';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const require = createRequire(import.meta.url);
// Get CLI version from package.json
const pkg = require('../package.json');
// Import Commander
import { program } from 'commander';
// Configure program
program
.name('openclaw')
.description('Heretek OpenClaw - Unified Deployment CLI')
.version(pkg.version, '-v, --version', 'Display CLI version')
.helpOption('-h, --help', 'Display help for command')
.addHelpText('before', `
╔══════════════════════════════════════════════════════════╗
║ ║
║ Heretek OpenClaw CLI v${pkg.version}
║ Unified deployment and management tool ║
║ ║
╚══════════════════════════════════════════════════════════╝
`);
// Register commands
const commands = [
{ name: 'init', description: 'Initialize deployment configuration', file: '../src/commands/init.js' },
{ name: 'deploy', description: 'Deploy OpenClaw', file: '../src/commands/deploy.js' },
{ name: 'status', description: 'Check deployment status', file: '../src/commands/status.js' },
{ name: 'logs', description: 'View logs', file: '../src/commands/logs.js' },
{ name: 'stop', description: 'Stop deployment', file: '../src/commands/stop.js' },
{ name: 'backup', description: 'Manage backups', file: '../src/commands/backup.js' },
{ name: 'config', description: 'Manage configuration', file: '../src/commands/config.js' },
{ name: 'update', description: 'Update OpenClaw', file: '../src/commands/update.js' },
{ name: 'agents', description: 'Manage agents', file: '../src/commands/agents.js' },
{ name: 'health', description: 'Run health checks', file: '../src/commands/health.js' },
];
// Add commands to program
commands.forEach(({ name, description, file }) => {
try {
const commandModule = require(file);
if (commandModule.default) {
program.addCommand(commandModule.default);
}
} catch (error) {
console.error(`Failed to load command '${name}': ${error.message}`);
}
});
// Handle unknown commands
program.on('command:*', () => {
console.error(`Error: Unknown command '${program.args.join(' ')}'`);
console.error('Run "openclaw --help" to see available commands.');
process.exit(1);
});
// Parse arguments
program.parse(process.argv);
// Show help if no command provided
if (!process.argv.slice(2).length) {
program.outputHelp();
}
+149
View File
@@ -0,0 +1,149 @@
/**
* OpenClaw CLI Configuration
*
* This file contains configuration options for the OpenClaw CLI tool.
*/
export default {
/**
* CLI settings
*/
cli: {
name: 'openclaw',
version: '1.0.0',
description: 'Heretek OpenClaw - Unified Deployment CLI',
},
/**
* Default paths
*/
paths: {
// OpenClaw installation directory
openclawDir: '~/.openclaw',
// Workspace directory for agents
workspaceDir: '~/.openclaw/workspace',
// Agents directory
agentsDir: '~/.openclaw/agents',
// Backups directory
backupsDir: '~/.openclaw/backups',
// Logs directory
logsDir: '~/.openclaw/logs',
// Cache directory
cacheDir: '~/.openclaw/cache',
},
/**
* Default deployment settings
*/
deployment: {
// Default deployment type
defaultType: 'docker',
// Docker settings
docker: {
composeFile: 'docker-compose.yml',
projectName: 'openclaw',
},
// Kubernetes settings
kubernetes: {
namespace: 'openclaw',
releaseName: 'openclaw',
chartDir: './charts/openclaw',
},
// Cloud settings
cloud: {
terraformDir: './terraform',
autoApprove: false,
},
},
/**
* Health check settings
*/
health: {
// Default timeout for health checks (ms)
timeout: 5000,
// Watch interval (seconds)
watchInterval: 30,
// Service endpoints
endpoints: {
gateway: 'http://localhost:18789',
litellm: 'http://localhost:4000',
postgres: 'localhost:5432',
redis: 'localhost:6379',
ollama: 'http://localhost:11434',
langfuse: 'http://localhost:3000',
},
},
/**
* Backup settings
*/
backup: {
// Default backup directory
directory: '~/.openclaw/backups',
// Retention period (days)
retentionDays: 30,
// Compression enabled
compress: true,
// Backup schedule
schedule: {
full: '0 2 * * 0', // Sunday at 2 AM
incremental: '0 2 * * 1-6', // Mon-Sat at 2 AM
},
},
/**
* Logging settings
*/
logging: {
// Default log level
level: 'info',
// Show timestamps
timestamps: true,
// Color output
colors: true,
},
/**
* Update settings
*/
update: {
// Check for updates on startup
checkOnStartup: false,
// Auto-update (not recommended for production)
autoUpdate: false,
// Update channel
channel: 'stable',
},
/**
* Feature flags
*/
features: {
// Enable interactive prompts
interactive: true,
// Enable telemetry (future)
telemetry: false,
// Enable experimental features
experimental: false,
},
};
+53
View File
@@ -0,0 +1,53 @@
{
"name": "@heretek/openclaw-cli",
"version": "1.0.0",
"description": "Unified CLI tool for Heretek OpenClaw deployment and management",
"type": "module",
"bin": {
"openclaw": "./bin/openclaw.js"
},
"main": "src/index.js",
"scripts": {
"start": "node bin/openclaw.js",
"dev": "node --watch bin/openclaw.js",
"test": "vitest run",
"lint": "eslint src/",
"lint:fix": "eslint src/ --fix"
},
"keywords": [
"openclaw",
"heretek",
"cli",
"deployment",
"a2a",
"agents"
],
"author": "Heretek",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/heretek/heretek-openclaw.git",
"directory": "cli"
},
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"commander": "^12.0.0",
"chalk": "^5.3.0",
"ora": "^8.0.1",
"inquirer": "^9.2.15",
"execa": "^8.0.1",
"fs-extra": "^11.2.0",
"yaml": "^2.4.0",
"axios": "^1.6.7",
"dotenv": "^16.4.5",
"boxen": "^7.1.1",
"gradient-string": "^2.0.2"
},
"devDependencies": {
"@types/node": "^20.11.0",
"eslint": "^9.0.0",
"vitest": "^1.3.0"
}
}
+293
View File
@@ -0,0 +1,293 @@
/**
* Agents Command
*
* Manage OpenClaw agents including list, start, stop, and configure.
*/
import { Command } from 'commander';
import axios from 'axios';
import log from '../lib/logger.js';
import { promptSelect, promptConfirm } from '../lib/prompts.js';
const command = new Command('agents');
const GATEWAY_URL = process.env.GATEWAY_URL || 'http://localhost:18789';
command
.description('Manage agents')
.addCommand(new Command('list')
.description('List all agents')
.option('--json', 'Output as JSON')
.action((options) => handleAgentsList(options))
)
.addCommand(new Command('start')
.description('Start an agent')
.argument('<agent>', 'Agent ID to start')
.option('--model <model>', 'Model to assign')
.action((agent, options) => handleAgentsStart(agent, options))
)
.addCommand(new Command('stop')
.description('Stop an agent')
.argument('<agent>', 'Agent ID to stop')
.action((agent) => handleAgentsStop(agent))
)
.addCommand(new Command('status')
.description('Show agent status')
.argument('[agent]', 'Specific agent ID (optional)')
.option('--json', 'Output as JSON')
.action((agent, options) => handleAgentsStatus(agent, options))
)
.addCommand(new Command('configure')
.description('Configure agent model')
.argument('<agent>', 'Agent ID to configure')
.option('--model <model>', 'Model to assign')
.option('--primary <model>', 'Primary model')
.option('--failover <model>', 'Failover model')
.action((agent, options) => handleAgentsConfigure(agent, options))
)
.addCommand(new Command('models')
.description('List available models')
.option('--json', 'Output as JSON')
.action((options) => handleAgentsModels(options))
);
/**
* Handle agents list
*/
async function handleAgentsList(options) {
log.section('Registered Agents');
try {
const response = await axios.get(`${GATEWAY_URL}/v1/agents`, {
timeout: 5000,
});
const agents = response.data?.agents || [];
if (options.json) {
console.log(JSON.stringify(agents, null, 2));
return;
}
if (agents.length === 0) {
console.log(' No agents registered');
return;
}
console.log(' ┌─────────────────┬─────────────────┬─────────────────┐');
console.log(' │ Agent │ Role │ Status │');
console.log(' ├─────────────────┼─────────────────┼─────────────────┤');
for (const agent of agents) {
const name = (agent.agent_name || agent.name || 'unknown').substring(0, 15).padEnd(15);
const role = (agent.role || 'unknown').substring(0, 15).padEnd(15);
const status = (agent.status || 'active').substring(0, 15).padEnd(15);
console.log(`${name}${role}${status}`);
}
console.log(' └─────────────────┴─────────────────┴─────────────────┘');
console.log(`\n Total: ${agents.length} agent(s)`);
} catch (error) {
log.error(`Failed to list agents: ${error.message}`);
console.log('\n Make sure the Gateway is running and accessible.');
}
}
/**
* Handle agents start
*/
async function handleAgentsStart(agent, options) {
log.section(`Starting Agent: ${agent}`);
try {
const payload = {
agent_id: agent,
model: options.model,
};
await axios.post(`${GATEWAY_URL}/api/v1/agents/${agent}/start`, payload, {
timeout: 10000,
});
log.success(`Agent ${agent} started`);
if (options.model) {
log.info(`Model assigned: ${options.model}`);
}
} catch (error) {
log.error(`Failed to start agent: ${error.message}`);
console.log(`
To start an agent manually:
cd agents/${agent}
npm start
`);
}
}
/**
* Handle agents stop
*/
async function handleAgentsStop(agent) {
log.section(`Stopping Agent: ${agent}`);
try {
await axios.post(`${GATEWAY_URL}/api/v1/agents/${agent}/stop`, {}, {
timeout: 5000,
});
log.success(`Agent ${agent} stopped`);
} catch (error) {
log.error(`Failed to stop agent: ${error.message}`);
log.info('You can stop the agent manually by killing its process');
}
}
/**
* Handle agents status
*/
async function handleAgentsStatus(agent, options) {
if (agent) {
await handleAgentStatusSingle(agent, options);
} else {
await handleAgentsList(options);
}
}
/**
* Handle single agent status
*/
async function handleAgentStatusSingle(agent, options) {
log.section(`Agent Status: ${agent}`);
try {
const response = await axios.get(`${GATEWAY_URL}/api/v1/agents/${agent}`, {
timeout: 5000,
});
const agentData = response.data;
if (options.json) {
console.log(JSON.stringify(agentData, null, 2));
return;
}
console.log(`
ID: ${agentData.id || agent}
Name: ${agentData.name || 'unknown'}
Role: ${agentData.role || 'unknown'}
Status: ${agentData.status || 'unknown'}
Model: ${agentData.model || 'not assigned'}
Port: ${agentData.port || 'N/A'}
Last Active: ${agentData.lastActive || 'never'}
Memory: ${agentData.memory || 'N/A'}
`);
} catch (error) {
log.error(`Failed to get agent status: ${error.message}`);
}
}
/**
* Handle agents configure
*/
async function handleAgentsConfigure(agent, options) {
log.section(`Configuring Agent: ${agent}`);
if (!options.model && !options.primary && !options.failover) {
// Interactive mode
const models = await getAvailableModels();
const selectedModel = await promptSelect(
'Select model for this agent:',
models.map(m => ({ name: m, value: m }))
);
options.model = selectedModel;
}
try {
const payload = {
model: options.model,
primary: options.primary,
failover: options.failover,
};
await axios.put(`${GATEWAY_URL}/api/v1/agents/${agent}/model`, payload, {
timeout: 5000,
});
log.success(`Agent ${agent} configured`);
if (options.model) log.info(`Model: ${options.model}`);
if (options.primary) log.info(`Primary: ${options.primary}`);
if (options.failover) log.info(`Failover: ${options.failover}`);
} catch (error) {
log.error(`Failed to configure agent: ${error.message}`);
}
}
/**
* Handle agents models
*/
async function handleAgentsModels(options) {
log.section('Available Models');
try {
const models = await getAvailableModels();
if (options.json) {
console.log(JSON.stringify(models, null, 2));
return;
}
if (models.length === 0) {
console.log(' No models available');
return;
}
// Group by provider
const grouped = {};
for (const model of models) {
const [provider] = model.split('/');
if (!grouped[provider]) {
grouped[provider] = [];
}
grouped[provider].push(model);
}
for (const [provider, providerModels] of Object.entries(grouped)) {
console.log(`\n ${provider.toUpperCase()}:`);
providerModels.forEach(m => console.log(` - ${m}`));
}
} catch (error) {
log.error(`Failed to list models: ${error.message}`);
}
}
/**
* Get available models from LiteLLM
*/
async function getAvailableModels() {
try {
const response = await axios.get(`${process.env.LITELLM_URL || 'http://localhost:4000'}/v1/models`, {
timeout: 5000,
headers: {
'Authorization': `Bearer ${process.env.LITELLM_MASTER_KEY || 'heretek-master-key-change-me'}`,
},
});
return response.data?.data?.map(m => m.id) || [];
} catch {
// Return default models
return [
'ollama/llama2',
'openai/gpt-4o',
'anthropic/claude-sonnet-4-20250514',
'google/gemini-2.5-pro',
'minimax/minimax-abab6.5s',
'zai/glm-5-1',
];
}
}
export default command;
+261
View File
@@ -0,0 +1,261 @@
/**
* Backup Command
*
* Manage OpenClaw backups including create, list, restore, and delete.
*/
import { Command } from 'commander';
import log from '../lib/logger.js';
import BackupManager from '../lib/backup-manager.js';
import { promptConfirm } from '../lib/prompts.js';
const command = new Command('backup');
command
.description('Manage backups')
.addCommand(new Command('create')
.description('Create a new backup')
.option('-t, --type <type>', 'Backup type: full, incremental', 'incremental')
.option('--verify', 'Verify backup after creation')
.option('--compress', 'Compress backup files', true)
.action((options) => handleBackupCreate(options))
)
.addCommand(new Command('list')
.description('List available backups')
.option('--json', 'Output as JSON')
.action((options) => handleBackupList(options))
)
.addCommand(new Command('restore')
.description('Restore from a backup')
.argument('<name>', 'Backup name to restore')
.option('--components <list>', 'Components to restore (all, postgresql, redis, workspace, config)', 'all')
.option('--confirm', 'Skip confirmation prompt')
.action((name, options) => handleBackupRestore(name, options))
)
.addCommand(new Command('delete')
.description('Delete a backup')
.argument('<name>', 'Backup name to delete')
.option('--confirm', 'Skip confirmation prompt')
.action((name, options) => handleBackupDelete(name, options))
)
.addCommand(new Command('verify')
.description('Verify a backup')
.argument('<name>', 'Backup name to verify')
.action((name) => handleBackupVerify(name))
)
.addCommand(new Command('rotate')
.description('Rotate old backups based on retention policy')
.option('--days <days>', 'Retention period in days', '30')
.action((options) => handleBackupRotate(options))
);
/**
* Handle backup create
*/
async function handleBackupCreate(options) {
log.section('Creating Backup');
const backupManager = new BackupManager();
try {
const result = await backupManager.create({
type: options.type,
verify: options.verify,
compress: options.compress,
});
if (result.success) {
log.success(`Backup created: ${result.name}`);
} else {
log.warn(`Backup completed with errors:`);
result.errors.forEach(e => log.warn(` - ${e.component}: ${e.error}`));
}
} catch (error) {
log.error(`Backup failed: ${error.message}`);
process.exit(1);
}
}
/**
* Handle backup list
*/
async function handleBackupList(options) {
const backupManager = new BackupManager();
try {
const backups = await backupManager.list();
if (options.json) {
console.log(JSON.stringify(backups, null, 2));
return;
}
log.section('Available Backups');
if (backups.length === 0) {
console.log(' No backups found');
return;
}
console.log(' ┌─────────────────────────────────────────────────────────────┐');
console.log(' │ Name │ Type │ Size │');
console.log(' ├─────────────────────────────────────────────────────────────┤');
for (const backup of backups) {
const name = backup.name.substring(0, 29).padEnd(29);
const type = backup.type.padEnd(12);
const size = formatSize(backup.size).padEnd(12);
console.log(`${name}${type}${size}`);
}
console.log(' └─────────────────────────────────────────────────────────────┘');
console.log(`\n Total: ${backups.length} backup(s)`);
} catch (error) {
log.error(`Failed to list backups: ${error.message}`);
process.exit(1);
}
}
/**
* Handle backup restore
*/
async function handleBackupRestore(name, options) {
log.section('Restoring Backup');
if (!options.confirm) {
const confirmed = await promptConfirm(
`Restore from backup "${name}"? This will overwrite existing data.`,
{ default: false }
);
if (!confirmed) {
log.info('Restore cancelled');
return;
}
}
const backupManager = new BackupManager();
try {
const components = options.components === 'all'
? ['all']
: options.components.split(',');
const result = await backupManager.restore(name, {
components,
confirm: true,
});
if (result.success) {
log.success(`Backup restored: ${name}`);
} else {
log.warn(`Restore completed with errors:`);
result.errors.forEach(e => log.warn(` - ${e.component}: ${e.error}`));
}
} catch (error) {
log.error(`Restore failed: ${error.message}`);
process.exit(1);
}
}
/**
* Handle backup delete
*/
async function handleBackupDelete(name, options) {
log.section('Deleting Backup');
if (!options.confirm) {
const confirmed = await promptConfirm(
`Delete backup "${name}"? This action cannot be undone.`,
{ default: false }
);
if (!confirmed) {
log.info('Delete cancelled');
return;
}
}
const backupManager = new BackupManager();
try {
const success = await backupManager.delete(name);
if (success) {
log.success(`Backup deleted: ${name}`);
} else {
log.error('Failed to delete backup');
process.exit(1);
}
} catch (error) {
log.error(`Delete failed: ${error.message}`);
process.exit(1);
}
}
/**
* Handle backup verify
*/
async function handleBackupVerify(name) {
log.section('Verifying Backup');
const backupManager = new BackupManager();
try {
const result = await backupManager.verify(name);
if (result.valid) {
log.success(`Backup verification passed: ${name}`);
result.checks.forEach(check => {
if (check.valid) {
log.success(`${check.name}`);
}
});
} else {
log.error(`Backup verification failed: ${name}`);
result.checks.forEach(check => {
if (!check.valid) {
log.error(`${check.name}: ${check.error}`);
}
});
}
} catch (error) {
log.error(`Verification failed: ${error.message}`);
process.exit(1);
}
}
/**
* Handle backup rotate
*/
async function handleBackupRotate(options) {
log.section('Rotating Backups');
const backupManager = new BackupManager({
retentionDays: parseInt(options.days, 10),
});
try {
const deleted = await backupManager.rotate();
log.success(`Rotation complete: ${deleted} backup(s) deleted`);
} catch (error) {
log.error(`Rotation failed: ${error.message}`);
process.exit(1);
}
}
/**
* Format file size
*/
function formatSize(bytes) {
if (!bytes || bytes === 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
}
export default command;
+249
View File
@@ -0,0 +1,249 @@
/**
* Config Command
*
* Manage OpenClaw configuration including view, edit, validate, and reset.
*/
import { Command } from 'commander';
import fs from 'fs-extra';
import path from 'path';
import log from '../lib/logger.js';
import ConfigManager from '../lib/config-manager.js';
import { promptConfirm, promptEditor } from '../lib/prompts.js';
const command = new Command('config');
command
.description('Manage configuration')
.addCommand(new Command('show')
.description('Show current configuration')
.option('--json', 'Output as JSON')
.option('--path <path>', 'Show specific config path (e.g., models.providers)')
.action((options) => handleConfigShow(options))
)
.addCommand(new Command('edit')
.description('Edit configuration')
.option('--path <path>', 'Edit specific config path')
.action((options) => handleConfigEdit(options))
)
.addCommand(new Command('validate')
.description('Validate configuration')
.option('--strict', 'Enable strict validation')
.action((options) => handleConfigValidate(options))
)
.addCommand(new Command('reset')
.description('Reset configuration to defaults')
.option('--confirm', 'Skip confirmation prompt')
.action((options) => handleConfigReset(options))
)
.addCommand(new Command('get')
.description('Get a specific configuration value')
.argument('<path>', 'Configuration path (e.g., model_routing.default)')
.action((path) => handleConfigGet(path))
)
.addCommand(new Command('set')
.description('Set a configuration value')
.argument('<path>', 'Configuration path (e.g., model_routing.default)')
.argument('<value>', 'Value to set')
.action((path, value) => handleConfigSet(path, value))
);
/**
* Handle config show
*/
async function handleConfigShow(options) {
const configManager = new ConfigManager();
try {
await configManager.load();
if (options.json) {
if (options.path) {
const value = configManager.get(options.path);
console.log(JSON.stringify(value, null, 2));
} else {
console.log(JSON.stringify(configManager.config, null, 2));
}
return;
}
log.section('OpenClaw Configuration');
if (options.path) {
const value = configManager.get(options.path);
console.log(`\n ${options.path}: ${JSON.stringify(value, null, 2)}`);
} else {
printConfigSummary(configManager.config);
}
} catch (error) {
log.error(`Failed to show configuration: ${error.message}`);
process.exit(1);
}
}
/**
* Print configuration summary
*/
function printConfigSummary(config) {
console.log(`
Version: ${config.version || 'unknown'}
Collective: ${config.collective?.name || 'unknown'}
Models:
Providers: ${Object.keys(config.models?.providers || {}).join(', ') || 'none'}
Model Routing:
Default: ${config.model_routing?.default || 'not set'}
Failover: ${config.model_routing?.aliases?.failover || 'not set'}
Agents: ${config.agents?.length || 0}
`);
if (config.agents?.length > 0) {
console.log(' Agent List:');
config.agents.forEach(agent => {
console.log(` - ${agent.id} (${agent.role}): ${agent.model}`);
});
}
}
/**
* Handle config edit
*/
async function handleConfigEdit(options) {
const configManager = new ConfigManager();
try {
await configManager.load();
log.info('Opening configuration editor...');
console.log(' Edit the configuration and save to apply changes.\n');
// In a real implementation, this would open the system editor
// For now, we'll show the config file path
console.log(` Configuration file: ${configManager.configPath}`);
console.log('\n To edit manually:');
console.log(` nano ${configManager.configPath}`);
console.log(' # or');
console.log(` code ${configManager.configPath}`);
} catch (error) {
log.error(`Failed to edit configuration: ${error.message}`);
process.exit(1);
}
}
/**
* Handle config validate
*/
async function handleConfigValidate(options) {
const configManager = new ConfigManager();
try {
await configManager.load();
log.section('Validating Configuration');
const result = configManager.validate();
if (result.valid) {
log.success('Configuration is valid');
} else {
log.error('Configuration validation failed:');
result.errors.forEach(e => log.error(`${e}`));
if (result.warnings.length > 0) {
log.warn('Warnings:');
result.warnings.forEach(w => log.warn(`${w}`));
}
process.exit(1);
}
} catch (error) {
log.error(`Validation failed: ${error.message}`);
process.exit(1);
}
}
/**
* Handle config reset
*/
async function handleConfigReset(options) {
const configManager = new ConfigManager();
if (!options.confirm) {
const confirmed = await promptConfirm(
'Reset configuration to defaults? This will overwrite current settings.',
{ default: false }
);
if (!confirmed) {
log.info('Reset cancelled');
return;
}
}
try {
log.section('Resetting Configuration');
const defaultConfig = configManager.createDefault();
await configManager.save(defaultConfig);
log.success('Configuration reset to defaults');
} catch (error) {
log.error(`Reset failed: ${error.message}`);
process.exit(1);
}
}
/**
* Handle config get
*/
async function handleConfigGet(path) {
const configManager = new ConfigManager();
try {
await configManager.load();
const value = configManager.get(path);
if (value === undefined) {
log.warn(`Configuration path not found: ${path}`);
} else {
console.log(typeof value === 'object'
? JSON.stringify(value, null, 2)
: value);
}
} catch (error) {
log.error(`Failed to get configuration: ${error.message}`);
process.exit(1);
}
}
/**
* Handle config set
*/
async function handleConfigSet(path, value) {
const configManager = new ConfigManager();
try {
await configManager.load();
// Parse value type
let parsedValue;
try {
parsedValue = JSON.parse(value);
} catch {
parsedValue = value;
}
configManager.set(path, parsedValue);
await configManager.save();
log.success(`Set ${path} = ${JSON.stringify(parsedValue)}`);
} catch (error) {
log.error(`Failed to set configuration: ${error.message}`);
process.exit(1);
}
}
export default command;
+216
View File
@@ -0,0 +1,216 @@
/**
* Deploy Command
*
* Deploy OpenClaw using the configured deployment type.
*/
import { Command } from 'commander';
import log from '../lib/logger.js';
import DeploymentManager, { DeploymentType } from '../lib/deployment-manager.js';
import { promptSelect, promptConfirm } from '../lib/prompts.js';
const command = new Command('deploy');
command
.description('Deploy OpenClaw')
.argument('[type]', 'Deployment type (docker, bare-metal, kubernetes, aws, gcp, azure)')
.option('-c, --config <path>', 'Configuration file path')
.option('--build', 'Build images before deployment (Docker)')
.option('--force-recreate', 'Force recreate containers (Docker)')
.option('--pull', 'Pull latest images before deployment (Docker)')
.option('--method <method>', 'Deployment method (helm, kustomize) for Kubernetes')
.option('--auto-approve', 'Auto-approve Terraform changes (Cloud)')
.option('-y, --yes', 'Skip confirmation prompts')
.action(async (type, options) => {
await handleDeploy(type, options);
});
/**
* Handle deploy command
*/
async function handleDeploy(type, options) {
log.section('Deploying OpenClaw');
// Determine deployment type
let deploymentType = type;
if (!deploymentType) {
// Try to read from config
deploymentType = await detectDeploymentType(options.config);
if (!deploymentType) {
// Interactive selection
deploymentType = await promptSelect(
'Select deployment type:',
[
{ name: 'Docker Compose', value: DeploymentType.DOCKER },
{ name: 'Bare Metal', value: DeploymentType.BARE_METAL },
{ name: 'Kubernetes', value: DeploymentType.KUBERNETES },
{ name: 'AWS', value: DeploymentType.AWS },
{ name: 'GCP', value: DeploymentType.GCP },
{ name: 'Azure', value: DeploymentType.AZURE },
]
);
}
}
// Validate deployment type
if (!Object.values(DeploymentType).includes(deploymentType)) {
log.error(`Unknown deployment type: ${deploymentType}`);
printUsage();
process.exit(1);
}
// Confirm deployment
if (!options.yes) {
const confirmed = await promptConfirm(
`Deploy to ${deploymentType}? This may take several minutes.`,
{ default: true }
);
if (!confirmed) {
log.info('Deployment cancelled');
return;
}
}
// Create deployment manager
const manager = new DeploymentManager({
rootDir: process.cwd(),
deploymentType,
configPath: options.config,
});
// Build deploy options
const deployOptions = {
build: options.build,
forceRecreate: options.forceRecreate,
pull: options.pull,
method: options.method,
autoApprove: options.autoApprove,
};
// Execute deployment
try {
const success = await manager.deploy(deployOptions);
if (success) {
log.success(`Deployment to ${deploymentType} completed successfully!`);
// Show status
await showPostDeployStatus(manager);
} else {
log.error('Deployment failed');
process.exit(1);
}
} catch (error) {
log.error(`Deployment failed: ${error.message}`);
log.debug(error.stack);
process.exit(1);
}
}
/**
* Detect deployment type from config or environment
*/
async function detectDeploymentType(configPath) {
// Check for docker-compose.yml
const fs = await import('fs-extra');
if (await fs.pathExists('docker-compose.yml')) {
return DeploymentType.DOCKER;
}
// Check for kubernetes manifests
if (await fs.pathExists('charts/openclaw') || await fs.pathExists('deploy/k8s')) {
return DeploymentType.KUBERNETES;
}
// Check for terraform configs
if (await fs.pathExists('terraform/aws')) {
return DeploymentType.AWS;
}
if (await fs.pathExists('terraform/gcp')) {
return DeploymentType.GCP;
}
if (await fs.pathExists('terraform/azure')) {
return DeploymentType.AZURE;
}
// Check config file
if (configPath && await fs.pathExists(configPath)) {
try {
const content = await fs.readFile(configPath, 'utf-8');
const config = JSON.parse(content);
if (config.deployment?.type) {
return config.deployment.type;
}
} catch {
// Ignore parse errors
}
}
return null;
}
/**
* Show post-deployment status
*/
async function showPostDeployStatus(manager) {
log.subheader('Deployment Status');
try {
const status = await manager.status();
const health = await manager.healthCheck();
if (health.healthy) {
log.success('All services are healthy');
} else {
log.warn('Some services may not be healthy yet');
}
// Print status details based on deployment type
if (status.containers) {
console.log('\nContainers:');
status.containers.forEach(c => {
const statusSymbol = c.State?.includes('running') ? '✓' : '⚠';
console.log(` ${statusSymbol} ${c.Names}`);
});
}
if (status.pods) {
console.log('\nPods:');
status.pods.forEach(p => {
const statusSymbol = p.ready ? '✓' : '⚠';
console.log(` ${statusSymbol} ${p.name} (${p.status})`);
});
}
} catch (error) {
log.debug(`Could not retrieve status: ${error.message}`);
}
}
/**
* Print usage information
*/
function printUsage() {
console.log(`
Usage: openclaw deploy [type] [options]
Deployment Types:
docker Deploy using Docker Compose
bare-metal Deploy directly on host system
kubernetes Deploy to Kubernetes cluster
aws Deploy to AWS using Terraform
gcp Deploy to GCP using Terraform
azure Deploy to Azure using Terraform
Examples:
openclaw deploy docker
openclaw deploy kubernetes --method helm
openclaw deploy aws --auto-approve
openclaw deploy --build --force-recreate
`);
}
export default command;
+230
View File
@@ -0,0 +1,230 @@
/**
* Health Command
*
* Run health checks on OpenClaw services.
*/
import { Command } from 'commander';
import chalk from 'chalk';
import log from '../lib/logger.js';
import HealthChecker from '../lib/health-checker.js';
const command = new Command('health');
command
.description('Run health checks')
.addCommand(new Command('check')
.description('Run all health checks')
.option('--service <name>', 'Check specific service only')
.option('--json', 'Output as JSON')
.action((options) => handleHealthCheck(options))
)
.addCommand(new Command('watch')
.description('Continuously monitor health')
.option('--interval <seconds>', 'Check interval in seconds', '30')
.action((options) => handleHealthWatch(options))
)
.addCommand(new Command('report')
.description('Generate health report')
.option('--output <file>', 'Save report to file')
.action((options) => handleHealthReport(options))
);
/**
* Handle health check
*/
async function handleHealthCheck(options) {
const checker = new HealthChecker();
try {
let results;
if (options.service) {
// Check specific service
results = {
checks: {
[options.service]: await checker.checkService(options.service),
},
};
results.healthy = Object.values(results.checks).every(c => c.healthy);
} else {
// Check all services
results = await checker.checkAll();
}
if (options.json) {
console.log(JSON.stringify(results, null, 2));
return;
}
printHealthResults(results);
} catch (error) {
log.error(`Health check failed: ${error.message}`);
process.exit(1);
}
}
/**
* Print health results
*/
function printHealthResults(results) {
log.section('OpenClaw Health Status');
const overallStatus = results.healthy
? chalk.green('● HEALTHY')
: chalk.red('● UNHEALTHY');
console.log(`\n Overall Status: ${overallStatus}`);
console.log(` Timestamp: ${results.timestamp || new Date().toISOString()}`);
console.log('');
// Service status table
console.log(' ┌─────────────────┬─────────────────┬─────────────────────────────────┐');
console.log(' │ Service │ Status │ Details │');
console.log(' ├─────────────────┼─────────────────┼─────────────────────────────────┤');
for (const [service, check] of Object.entries(results.checks || {})) {
const serviceName = service.padEnd(15);
const statusSymbol = check.healthy ? chalk.green('✓') : chalk.red('✗');
const statusText = check.healthy ? 'Healthy' : 'Unhealthy';
const status = `${statusSymbol} ${statusText}`.padEnd(15);
// Build details string
let details = [];
if (check.modelCount !== undefined) details.push(`Models: ${check.modelCount}`);
if (check.agentCount !== undefined) details.push(`Agents: ${check.agentCount}`);
if (check.pgvector !== undefined) details.push(`pgvector: ${check.pgvector ? 'yes' : 'no'}`);
if (check.memoryUsed !== undefined) details.push(`Memory: ${check.memoryUsed}`);
if (check.responseTime !== undefined) details.push(`Response: ${check.responseTime}`);
if (check.error) details.push(chalk.red(check.error));
const detailsText = details.join(', ').substring(0, 31).padEnd(31);
console.log(`${serviceName}${status}${detailsText}`);
}
console.log(' └─────────────────┴─────────────────┴─────────────────────────────────┘');
// Print recommendations for unhealthy services
const unhealthyServices = Object.entries(results.checks || {})
.filter(([, check]) => !check.healthy);
if (unhealthyServices.length > 0) {
console.log('\n');
log.subheader('Recommendations');
for (const [service, check] of unhealthyServices) {
const recommendation = getRecommendation(service, check);
console.log(` ${chalk.yellow('→')} ${service.toUpperCase()}: ${recommendation}`);
}
}
// Exit with error if unhealthy
if (!results.healthy) {
process.exit(1);
}
}
/**
* Get recommendation for unhealthy service
*/
function getRecommendation(service, check) {
const recommendations = {
gateway: 'Check if Gateway is running on port 18789',
litellm: 'Verify LiteLLM is running and API key is correct',
postgres: 'Ensure PostgreSQL is running and credentials are correct',
redis: 'Ensure Redis is running on port 6379',
ollama: 'Start Ollama service: ollama serve',
langfuse: 'Check Langfuse deployment and configuration',
agents: 'Verify agents are deployed and connected to Gateway',
};
return recommendations[service] || `Check ${service} logs for more information`;
}
/**
* Handle health watch
*/
async function handleHealthWatch(options) {
const interval = parseInt(options.interval, 10) * 1000;
const checker = new HealthChecker();
log.section('Health Monitor');
log.info(`Watching services (interval: ${options.interval}s)`);
log.info('Press Ctrl+C to stop\n');
const watch = async () => {
try {
const results = await checker.checkAll();
// Clear screen and print status
process.stdout.write('\x1Bc');
console.log(`Health Monitor (Ctrl+C to stop) - ${new Date().toISOString()}`);
console.log('');
printHealthResults(results);
} catch (error) {
log.error(`Health check failed: ${error.message}`);
}
};
// Initial check
await watch();
// Interval checks
setInterval(watch, interval);
}
/**
* Handle health report
*/
async function handleHealthReport(options) {
const checker = new HealthChecker();
try {
const report = await checker.generateReport();
if (options.output) {
const fs = await import('fs-extra');
await fs.writeFile(options.output, JSON.stringify(report, null, 2));
log.success(`Report saved to: ${options.output}`);
} else {
printHealthReport(report);
}
} catch (error) {
log.error(`Failed to generate report: ${error.message}`);
process.exit(1);
}
}
/**
* Print health report
*/
function printHealthReport(report) {
log.section('Health Report');
const summary = report.summary;
console.log(`
Generated: ${summary.timestamp}
Status: ${summary.healthy ? chalk.green('HEALTHY') : chalk.red('UNHEALTHY')}
Checks: ${summary.healthyChecks}/${summary.totalChecks} passed
`);
if (report.recommendations.length > 0) {
log.subheader('Issues & Recommendations');
for (const rec of report.recommendations) {
console.log(`
${chalk.yellow(rec.service.toUpperCase())}
Issue: ${rec.issue}
Action: ${rec.action}
`);
}
} else {
log.success('All services are healthy - no issues found');
}
}
export default command;
+327
View File
@@ -0,0 +1,327 @@
/**
* Init Command
*
* Initialize deployment configuration with interactive setup wizard.
*/
import { Command } from 'commander';
import fs from 'fs-extra';
import path from 'path';
import log from '../lib/logger.js';
import ConfigManager from '../lib/config-manager.js';
import { promptSelect, promptConfirm, promptText, promptPassword, promptSequence } from '../lib/prompts.js';
const command = new Command('init');
command
.description('Initialize deployment configuration')
.option('-t, --type <type>', 'Deployment type (docker, bare-metal, kubernetes, aws, gcp, azure)')
.option('-o, --output <path>', 'Output directory for configuration')
.option('-n, --non-interactive', 'Non-interactive mode (use defaults)')
.option('--skip-validation', 'Skip configuration validation')
.action(async (options) => {
await handleInit(options);
});
/**
* Handle init command
*/
async function handleInit(options) {
log.section('OpenClaw Initialization');
const configManager = new ConfigManager({
rootDir: options.output || process.cwd(),
});
// Non-interactive mode
if (options.nonInteractive) {
return await initNonInteractive(configManager, options);
}
// Interactive mode
return await initInteractive(configManager, options);
}
/**
* Interactive initialization
*/
async function initInteractive(configManager, options) {
log.info('Starting interactive setup wizard...\n');
const config = {
version: '2.0.0',
collective: {
name: 'OpenClaw Collective',
description: 'Self-improving autonomous agent collective',
version: '2.0.0',
},
models: {
providers: {},
},
agents: [],
model_routing: {
default: '',
aliases: {
failover: '',
},
},
deployment: {
type: options.type || 'docker',
},
};
// Step 1: Welcome
console.log(`
${log.symbols.info} Welcome to Heretek OpenClaw!
This wizard will guide you through the setup process.
`);
// Step 2: Deployment type selection
log.subheader('Step 1: Deployment Type');
const deploymentType = await promptSelect(
'Select deployment type:',
[
{ name: 'Docker (Recommended for local development)', value: 'docker' },
{ name: 'Bare Metal (Direct installation)', value: 'bare-metal' },
{ name: 'Kubernetes (Production)', value: 'kubernetes' },
{ name: 'AWS Cloud', value: 'aws' },
{ name: 'GCP Cloud', value: 'gcp' },
{ name: 'Azure Cloud', value: 'azure' },
]
);
config.deployment.type = deploymentType;
log.success(`Deployment type: ${deploymentType}`);
// Step 3: AI Provider selection
log.subheader('Step 2: AI Provider Configuration');
const primaryProvider = await promptSelect(
'Select primary AI provider:',
[
{ name: 'MiniMax (Recommended)', value: 'minimax' },
{ name: 'z.ai', value: 'zai' },
{ name: 'OpenAI', value: 'openai' },
{ name: 'Anthropic', value: 'anthropic' },
{ name: 'Google', value: 'google' },
{ name: 'Ollama (Local/Free)', value: 'ollama' },
]
);
log.success(`Primary provider: ${primaryProvider}`);
// Set default model based on provider
const defaultModels = {
minimax: 'minimax/minimax-abab6.5s',
zai: 'zai/glm-5-1',
openai: 'openai/gpt-4o',
anthropic: 'anthropic/claude-sonnet-4-20250514',
google: 'google/gemini-2.5-pro',
ollama: 'ollama/llama2',
};
config.model_routing.default = defaultModels[primaryProvider];
config.model_routing.aliases.failover = defaultModels[primaryProvider];
// Step 4: API Keys
log.subheader('Step 3: API Key Configuration');
const apiKey = await promptPassword(
`${primaryProvider.toUpperCase()} API Key:`,
{ mask: '*' }
);
// Store in environment variable reference
const envVarMap = {
minimax: 'MINIMAX_API_KEY',
zai: 'ZAI_API_KEY',
openai: 'OPENAI_API_KEY',
anthropic: 'ANTHROPIC_API_KEY',
google: 'GOOGLE_API_KEY',
};
const envVar = envVarMap[primaryProvider];
if (envVar) {
configManager.setEnv(envVar, apiKey);
log.success(`API key configured for ${primaryProvider}`);
}
// Step 5: Agent selection
log.subheader('Step 4: Agent Configuration');
const enableAllAgents = await promptConfirm(
'Enable all agents? (recommended for full functionality)',
{ default: true }
);
const availableAgents = [
{ id: 'steward', name: 'Steward', role: 'Orchestrator' },
{ id: 'alpha', name: 'Alpha', role: 'Triad' },
{ id: 'beta', name: 'Beta', role: 'Triad' },
{ id: 'charlie', name: 'Charlie', role: 'Triad' },
{ id: 'examiner', name: 'Examiner', role: 'Interrogator' },
{ id: 'explorer', name: 'Explorer', role: 'Scout' },
{ id: 'historian', name: 'Historian', role: 'Archivist' },
];
if (enableAllAgents) {
config.agents = availableAgents.map((agent, index) => ({
id: agent.id,
name: agent.name,
role: agent.role,
model: 'agent/' + agent.id,
port: 18790 + index,
}));
log.success(`All ${config.agents.length} agents enabled`);
} else {
const selectedAgents = await promptSelect(
'Select agents to enable:',
[
{ name: 'Steward only (minimal)', value: ['steward'] },
{ name: 'Core triad (Steward, Alpha, Beta)', value: ['steward', 'alpha', 'beta'] },
{ name: 'Full collective', value: availableAgents.map(a => a.id) },
]
);
config.agents = selectedAgents.map((id, index) => {
const agent = availableAgents.find(a => a.id === id);
return {
id: agent.id,
name: agent.name,
role: agent.role,
model: 'agent/' + agent.id,
port: 18790 + index,
};
});
log.success(`${config.agents.length} agents enabled`);
}
// Step 6: Observability
log.subheader('Step 5: Observability');
const enableLangfuse = await promptConfirm(
'Enable Langfuse observability?',
{ default: false }
);
if (enableLangfuse) {
config.observability = {
langfuse: {
enabled: true,
host: 'http://localhost:3000',
},
};
log.success('Langfuse observability enabled');
}
// Step 7: Review and confirm
log.subheader('Step 6: Review Configuration');
console.log(`
Configuration Summary:
Deployment Type: ${config.deployment.type}
Primary Provider: ${primaryProvider}
Default Model: ${config.model_routing.default}
Agents: ${config.agents.length}
Observability: ${enableLangfuse ? 'Enabled' : 'Disabled'}
`);
const confirm = await promptConfirm('Proceed with this configuration?', { default: true });
if (!confirm) {
log.warn('Setup cancelled');
return;
}
// Step 8: Write configuration
log.subheader('Writing Configuration');
// Initialize directories
await configManager.initConfigDir();
// Save openclaw.json
await configManager.save(config);
// Generate .env file
await configManager.saveEnv();
log.success('Configuration files written');
// Step 9: Next steps
log.subheader('Setup Complete!');
printNextSteps(config.deployment.type);
}
/**
* Non-interactive initialization
*/
async function initNonInteractive(configManager, options) {
log.info('Running non-interactive initialization...');
const config = configManager.createDefault();
config.deployment = {
type: options.type || 'docker',
};
// Initialize directories
await configManager.initConfigDir();
// Save configuration
await configManager.save(config);
// Validate if not skipped
if (!options.skipValidation) {
const validation = configManager.validate(config);
if (!validation.valid) {
log.error('Configuration validation failed:');
validation.errors.forEach(e => log.error(` - ${e}`));
throw new Error('Configuration validation failed');
}
log.success('Configuration validated');
}
log.success('Non-interactive initialization complete');
}
/**
* Print next steps
*/
function printNextSteps(deploymentType) {
const steps = {
docker: [
'Start Docker services: docker compose up -d',
'Verify services: docker compose ps',
'Install Gateway: curl -fsSL https://openclaw.ai/install.sh | bash',
'Initialize Gateway: openclaw onboard --install-daemon',
],
'bare-metal': [
'Run system setup: sudo ./scripts/install/ubuntu-deps.sh',
'Install application: npm install --production',
'Start services: sudo systemctl start openclaw-gateway',
],
kubernetes: [
'Apply manifests: kubectl apply -f deploy/k8s/',
'Or use Helm: helm install openclaw ./charts/openclaw',
],
};
console.log(`
${log.symbols.info} Next Steps:
`);
const typeSteps = steps[deploymentType] || steps.docker;
typeSteps.forEach((step, i) => {
console.log(` ${i + 1}. ${step}`);
});
console.log(`
Documentation:
- Local Deployment: docs/deployment/LOCAL_DEPLOYMENT.md
- Setup Wizard: docs/deployment/SETUP_WIZARD.md
- Configuration: docs/CONFIGURATION.md
`);
}
export default command;
+159
View File
@@ -0,0 +1,159 @@
/**
* Logs Command
*
* View and follow logs from OpenClaw services.
*/
import { Command } from 'commander';
import { execa } from 'execa';
import log from '../lib/logger.js';
import DeploymentManager from '../lib/deployment-manager.js';
const command = new Command('logs');
command
.description('View logs from OpenClaw services')
.argument('[service]', 'Service name (gateway, litellm, postgres, redis, ollama, etc.)')
.option('-t, --type <type>', 'Deployment type (auto-detect if not specified)')
.option('-f, --follow', 'Follow log output (tail -f mode)')
.option('-n, --tail <lines>', 'Number of lines to show', '100')
.option('--since <time>', 'Show logs since timestamp (e.g., 2024-01-01T00:00:00)')
.option('--until <time>', 'Show logs until timestamp')
.option('--timestamps', 'Show timestamps in logs')
.option('--level <level>', 'Filter by log level (error, warn, info, debug)')
.option('--grep <pattern>', 'Filter logs by pattern')
.option('--json', 'Output as JSON (if supported)')
.action(async (service, options) => {
await handleLogs(service, options);
});
/**
* Handle logs command
*/
async function handleLogs(service, options) {
const manager = new DeploymentManager({
rootDir: process.cwd(),
deploymentType: options.type,
});
try {
// Determine deployment type and get logs
const deployer = manager.deployer;
if (deployer.logs) {
// Use deployer's logs method
await deployer.logs({
services: service ? [service] : [],
follow: options.follow,
tail: options.tail,
timestamps: options.timestamps,
});
} else {
// Fallback to direct log viewing
await viewLogsDirect(service, options);
}
} catch (error) {
log.error(`Failed to view logs: ${error.message}`);
process.exit(1);
}
}
/**
* View logs directly from files or system
*/
async function viewLogsDirect(service, options) {
const logDir = '/var/log/openclaw';
const fs = await import('fs-extra');
// Check if log directory exists
if (await fs.pathExists(logDir)) {
const logFile = service
? `${logDir}/${service}.log`
: `${logDir}/openclaw.log`;
if (await fs.pathExists(logFile)) {
if (options.follow) {
// Follow mode using tail -f
const args = ['-f', logFile];
if (options.tail !== 'all') {
args.unshift('-n', options.tail);
}
await execa('tail', args, { stdio: 'inherit' });
} else {
// Read file directly
const content = await fs.readFile(logFile, 'utf-8');
console.log(content);
}
return;
}
}
// Try journalctl for systemd services
if (service) {
try {
const args = ['--no-pager'];
if (options.follow) {
args.push('-f');
}
if (options.tail && options.tail !== 'all') {
args.push('-n', options.tail);
}
if (options.since) {
args.push('--since', options.since);
}
args.push('-u', `openclaw-${service}`);
await execa('journalctl', args, { stdio: 'inherit' });
return;
} catch {
// journalctl not available
}
}
log.error(`No logs found for service: ${service}`);
console.log(`
Available log sources:
- Docker: openclaw logs --type docker
- Systemd: Check /var/log/openclaw/
- Kubernetes: openclaw logs --type kubernetes
`);
}
/**
* Filter logs by level
*/
function filterByLevel(content, level) {
if (!level) return content;
const levelPatterns = {
error: /ERROR|error|Error|\[E\]/i,
warn: /WARN|warn|Warn|WARNING|warning|\[W\]/i,
info: /INFO|info|Info|\[I\]/i,
debug: /DEBUG|debug|Debug|\[D\]/i,
};
const pattern = levelPatterns[level];
if (!pattern) return content;
return content.split('\n')
.filter(line => pattern.test(line))
.join('\n');
}
/**
* Grep logs by pattern
*/
function grepLogs(content, pattern) {
if (!pattern) return content;
const regex = new RegExp(pattern, 'i');
return content.split('\n')
.filter(line => regex.test(line))
.join('\n');
}
export default command;
+233
View File
@@ -0,0 +1,233 @@
/**
* Status Command
*
* Check deployment status and display service health.
*/
import { Command } from 'commander';
import chalk from 'chalk';
import log from '../lib/logger.js';
import DeploymentManager from '../lib/deployment-manager.js';
import HealthChecker from '../lib/health-checker.js';
const command = new Command('status');
command
.description('Check deployment status')
.option('-t, --type <type>', 'Deployment type (auto-detect if not specified)')
.option('--services', 'Show service status only')
.option('--agents', 'Show agent status only')
.option('--resources', 'Show resource usage')
.option('--json', 'Output as JSON')
.action(async (options) => {
await handleStatus(options);
});
/**
* Handle status command
*/
async function handleStatus(options) {
const manager = new DeploymentManager({
rootDir: process.cwd(),
deploymentType: options.type,
});
try {
// Get deployment status
const status = await manager.status();
const health = await manager.healthCheck();
if (options.json) {
console.log(JSON.stringify({ status, health }, null, 2));
return;
}
// Print status based on options
if (options.services) {
printServiceStatus(status);
} else if (options.agents) {
printAgentStatus(status);
} else if (options.resources) {
printResourceStatus(status);
} else {
printFullStatus(status, health);
}
} catch (error) {
log.error(`Failed to get status: ${error.message}`);
process.exit(1);
}
}
/**
* Print full status
*/
function printFullStatus(status, health) {
log.section('OpenClaw Status');
// Overall status
const overallHealthy = health?.healthy || false;
const statusLine = overallHealthy
? chalk.green('● HEALTHY')
: chalk.red('● UNHEALTHY');
console.log(`\n Status: ${statusLine}`);
console.log(` Last Check: ${health?.timestamp || new Date().toISOString()}`);
// Service status
printServiceStatus(status);
// Health check summary
if (health?.checks) {
console.log('\n');
log.subheader('Health Checks');
for (const [service, check] of Object.entries(health.checks)) {
const symbol = check.healthy ? chalk.green('✓') : chalk.red('✗');
console.log(` ${symbol} ${service.toUpperCase()}`);
if (!check.healthy && check.error) {
console.log(` Error: ${check.error}`);
}
}
}
}
/**
* Print service status
*/
function printServiceStatus(status) {
log.subheader('Services');
const services = [];
// Docker containers
if (status.containers) {
for (const container of status.containers) {
services.push({
name: container.Names?.replace('openclaw-', '') || container.name,
status: container.State || container.status,
healthy: container.State?.includes('running') || container.status?.includes('running'),
});
}
}
// Kubernetes pods
if (status.pods) {
for (const pod of status.pods) {
services.push({
name: pod.name.replace('openclaw-', ''),
status: pod.status,
healthy: pod.ready,
});
}
}
// Systemd services
if (Array.isArray(status) && status.length > 0) {
for (const service of status) {
services.push({
name: service.name,
status: service.status,
healthy: service.active,
});
}
}
// Print service table
if (services.length === 0) {
console.log(' No services found');
return;
}
console.log(' ┌─────────────────┬─────────────────┬──────────┐');
console.log(' │ Service │ Status │ Health │');
console.log(' ├─────────────────┼─────────────────┼──────────┤');
for (const service of services) {
const name = service.name.padEnd(15);
const statusText = (service.status || 'unknown').substring(0, 15).padEnd(15);
const health = service.healthy
? chalk.green('Healthy'.padEnd(8))
: chalk.red('Unhealthy'.padEnd(8));
console.log(`${name}${statusText}${health}`);
}
console.log(' └─────────────────┴─────────────────┴──────────┘');
}
/**
* Print agent status
*/
function printAgentStatus(status) {
log.subheader('Agents');
const agents = [];
// From health checks
if (status.checks?.agents?.agents) {
for (const agent of status.checks.agents.agents) {
agents.push({
name: agent.agent_name || agent,
status: 'active',
});
}
}
if (agents.length === 0) {
console.log(' No agents registered');
return;
}
console.log(' ┌─────────────────┬─────────────────┐');
console.log(' │ Agent │ Status │');
console.log(' ├─────────────────┼─────────────────┤');
for (const agent of agents) {
const name = agent.name.padEnd(15);
const statusText = chalk.green(agent.status.padEnd(15));
console.log(`${name}${statusText}`);
}
console.log(' └─────────────────┴─────────────────┘');
}
/**
* Print resource status
*/
function printResourceStatus(status) {
log.subheader('Resource Usage');
// Docker stats
if (status.containers) {
console.log('\n Container Resources:');
console.log(' ┌─────────────────┬───────────────┬───────────────┐');
console.log(' │ Container │ CPU │ Memory │');
console.log(' ├─────────────────┼───────────────┼───────────────┤');
for (const container of status.containers) {
// Note: Actual CPU/Memory would require docker stats
console.log(`${(container.Names || 'unknown').padEnd(15)} │ N/A │ N/A │`);
}
console.log(' └─────────────────┴───────────────┴───────────────┘');
}
// Kubernetes resources
if (status.pods) {
console.log('\n Pod Resources:');
console.log(' ┌─────────────────┬───────────────┬───────────────┐');
console.log(' │ Pod │ CPU │ Memory │');
console.log(' ├─────────────────┼───────────────┼───────────────┤');
for (const pod of status.pods) {
console.log(`${pod.name.substring(0, 15).padEnd(15)} │ N/A │ N/A │`);
}
console.log(' └─────────────────┴───────────────┴───────────────┘');
}
console.log('\n Note: Real-time resource usage requires running containers/pods');
}
export default command;
+102
View File
@@ -0,0 +1,102 @@
/**
* Stop Command
*
* Stop OpenClaw deployment gracefully.
*/
import { Command } from 'commander';
import log from '../lib/logger.js';
import DeploymentManager from '../lib/deployment-manager.js';
import { promptConfirm } from '../lib/prompts.js';
const command = new Command('stop');
command
.description('Stop OpenClaw deployment')
.option('-t, --type <type>', 'Deployment type (auto-detect if not specified)')
.option('-f, --force', 'Force stop (kill containers/processes)')
.option('--volumes', 'Remove volumes (Docker only)')
.option('--backup', 'Create backup before stopping')
.option('-y, --yes', 'Skip confirmation prompt')
.action(async (options) => {
await handleStop(options);
});
/**
* Handle stop command
*/
async function handleStop(options) {
log.section('Stopping OpenClaw');
// Confirm stop
if (!options.yes) {
const confirmed = await promptConfirm(
'Are you sure you want to stop OpenClaw? This will stop all services.',
{ default: false }
);
if (!confirmed) {
log.info('Stop cancelled');
return;
}
}
// Create backup if requested
if (options.backup) {
log.info('Creating backup before stopping...');
try {
const BackupManager = (await import('../lib/backup-manager.js')).default;
const backupManager = new BackupManager();
await backupManager.create({ type: 'incremental' });
log.success('Backup created');
} catch (error) {
log.warn(`Backup failed: ${error.message}`);
// Continue with stop even if backup fails
}
}
const manager = new DeploymentManager({
rootDir: process.cwd(),
deploymentType: options.type,
});
try {
log.info('Stopping services...');
const success = await manager.stop({
removeVolumes: options.volumes,
});
if (success) {
log.success('OpenClaw stopped successfully');
// Show post-stop message
printPostStopMessage(options);
} else {
log.error('Failed to stop OpenClaw');
process.exit(1);
}
} catch (error) {
log.error(`Failed to stop: ${error.message}`);
log.debug(error.stack);
process.exit(1);
}
}
/**
* Print post-stop message
*/
function printPostStopMessage(options) {
console.log(`
${log.symbols.info} OpenClaw has been stopped.
To start again:
${options.type === 'docker' ? 'docker compose up -d' : 'openclaw deploy'}
${options.volumes ? `
Note: Volumes were removed. Data may need to be restored from backup.` : `
Data is preserved. To remove data as well, use --volumes flag.`}
`);
}
export default command;
+303
View File
@@ -0,0 +1,303 @@
/**
* Update Command
*
* Check for and apply OpenClaw updates.
*/
import { Command } from 'commander';
import { execa } from 'execa';
import fs from 'fs-extra';
import path from 'path';
import log from '../lib/logger.js';
import { promptConfirm } from '../lib/prompts.js';
const command = new Command('update');
command
.description('Update OpenClaw')
.addCommand(new Command('check')
.description('Check for available updates')
.option('--json', 'Output as JSON')
.action((options) => handleUpdateCheck(options))
)
.addCommand(new Command('apply')
.description('Apply available updates')
.option('--dry-run', 'Show what would be updated without applying')
.option('--rollback', 'Rollback to previous version')
.option('--confirm', 'Skip confirmation prompt')
.action((options) => handleUpdateApply(options))
)
.addCommand(new Command('rollback')
.description('Rollback to previous version')
.option('--version <version>', 'Specific version to rollback to')
.option('--confirm', 'Skip confirmation prompt')
.action((options) => handleUpdateRollback(options))
)
.addCommand(new Command('history')
.description('Show update history')
.action(() => handleUpdateHistory())
);
/**
* Handle update check
*/
async function handleUpdateCheck(options) {
log.section('Checking for Updates');
try {
const currentVersion = await getCurrentVersion();
const latestVersion = await getLatestVersion();
const updateAvailable = compareVersions(currentVersion, latestVersion) < 0;
if (options.json) {
console.log(JSON.stringify({
currentVersion,
latestVersion,
updateAvailable,
}, null, 2));
return;
}
console.log(`
Current Version: ${currentVersion}
Latest Version: ${latestVersion}
`);
if (updateAvailable) {
log.success('Update available!');
console.log(`
To apply the update, run:
openclaw update apply
`);
} else {
log.info('You are running the latest version');
}
} catch (error) {
log.error(`Failed to check for updates: ${error.message}`);
process.exit(1);
}
}
/**
* Handle update apply
*/
async function handleUpdateApply(options) {
log.section('Applying Updates');
const currentVersion = await getCurrentVersion();
const latestVersion = await getLatestVersion();
if (compareVersions(currentVersion, latestVersion) >= 0) {
log.info('No updates available');
return;
}
console.log(`
Current Version: ${currentVersion}
Updating to: ${latestVersion}
`);
if (!options.confirm && !options.dryRun) {
const confirmed = await promptConfirm(
'Apply update?',
{ default: true }
);
if (!confirmed) {
log.info('Update cancelled');
return;
}
}
if (options.dryRun) {
log.info('Dry run - showing what would be updated:');
console.log(`
Would update from ${currentVersion} to ${latestVersion}
Files that would be updated:
- CLI binaries
- Deployment scripts
- Configuration templates
`);
return;
}
try {
// Create backup before update
log.info('Creating backup before update...');
const BackupManager = (await import('../lib/backup-manager.js')).default;
const backupManager = new BackupManager();
await backupManager.create({ type: 'incremental', verify: false });
log.success('Backup created');
// Apply update
log.info('Applying update...');
// Update npm dependencies
await execa('npm', ['install', '--production'], {
cwd: process.cwd(),
stdio: 'inherit',
});
// Update version file
await fs.writeFile(
path.join(process.cwd(), 'VERSION'),
`${latestVersion}\n`,
'utf-8'
);
log.success(`Updated to version ${latestVersion}`);
console.log(`
${log.symbols.info} Update complete! Restart services to apply changes:
Docker: docker compose restart
Bare Metal: sudo systemctl restart openclaw-gateway
Kubernetes: kubectl rollout restart deployment/openclaw
`);
} catch (error) {
log.error(`Update failed: ${error.message}`);
log.info('You can rollback using: openclaw update rollback');
process.exit(1);
}
}
/**
* Handle update rollback
*/
async function handleUpdateRollback(options) {
log.section('Rolling Back Update');
if (!options.confirm) {
const confirmed = await promptConfirm(
'Rollback to previous version? This may cause data loss.',
{ default: false }
);
if (!confirmed) {
log.info('Rollback cancelled');
return;
}
}
try {
const BackupManager = (await import('../lib/backup-manager.js')).default;
const backupManager = new BackupManager();
// Find latest backup
const backups = await backupManager.list();
if (backups.length === 0) {
log.error('No backups available for rollback');
process.exit(1);
}
const latestBackup = backups[0];
log.info(`Found backup: ${latestBackup.name}`);
// Restore from backup
await backupManager.restore(latestBackup.name, { confirm: true });
log.success('Rollback complete');
} catch (error) {
log.error(`Rollback failed: ${error.message}`);
process.exit(1);
}
}
/**
* Handle update history
*/
async function handleUpdateHistory() {
log.section('Update History');
try {
const historyFile = path.join(process.cwd(), '.update-history.json');
if (!await fs.pathExists(historyFile)) {
console.log(' No update history available');
return;
}
const history = await fs.readFile(historyFile, 'utf-8');
const updates = JSON.parse(history);
console.log(' ┌─────────────────────────────────────────────────────────────┐');
console.log(' │ Date │ From │ To │ Status │');
console.log(' ├─────────────────────────────────────────────────────────────┤');
for (const update of updates.slice(-10)) {
const date = update.date.substring(0, 19).padEnd(19);
const from = (update.from || 'unknown').padEnd(11);
const to = (update.to || 'unknown').padEnd(11);
const status = (update.success ? 'Success' : 'Failed').padEnd(9);
console.log(`${date}${from}${to}${status}`);
}
console.log(' └─────────────────────────────────────────────────────────────┘');
} catch (error) {
log.error(`Failed to read update history: ${error.message}`);
}
}
/**
* Get current version
*/
async function getCurrentVersion() {
try {
// Try VERSION file first
const versionFile = path.join(process.cwd(), 'VERSION');
if (await fs.pathExists(versionFile)) {
const content = await fs.readFile(versionFile, 'utf-8');
return content.trim();
}
// Try package.json
const pkgFile = path.join(process.cwd(), 'package.json');
if (await fs.pathExists(pkgFile)) {
const pkg = JSON.parse(await fs.readFile(pkgFile, 'utf-8'));
return pkg.version || 'unknown';
}
return 'unknown';
} catch {
return 'unknown';
}
}
/**
* Get latest version
*/
async function getLatestVersion() {
try {
// In production, this would fetch from npm or GitHub
// For now, return current version as placeholder
const pkgFile = path.join(process.cwd(), 'package.json');
if (await fs.pathExists(pkgFile)) {
const pkg = JSON.parse(await fs.readFile(pkgFile, 'utf-8'));
return pkg.version || '1.0.0';
}
return '1.0.0';
} catch {
return '1.0.0';
}
}
/**
* Compare version strings
*/
function compareVersions(v1, v2) {
const parts1 = v1.replace(/[^\d.]/g, '').split('.').map(Number);
const parts2 = v2.replace(/[^\d.]/g, '').split('.').map(Number);
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
const a = parts1[i] || 0;
const b = parts2[i] || 0;
if (a !== b) return a - b;
}
return 0;
}
export default command;
+724
View File
@@ -0,0 +1,724 @@
/**
* Backup Manager
*
* Manages OpenClaw backups including creation, restoration, listing,
* and scheduling.
*/
import { execa } from 'execa';
import fs from 'fs-extra';
import path from 'path';
import log from './logger.js';
class BackupManager {
constructor(options = {}) {
this.rootDir = options.rootDir || process.cwd();
this.backupDir = options.backupDir || path.join(process.env.HOME || '', '.openclaw', 'backups');
this.tempDir = options.tempDir || '/tmp/openclaw-backup';
this.retentionDays = options.retentionDays || 30;
// Database settings
this.postgresHost = options.postgresHost || 'localhost';
this.postgresPort = options.postgresPort || 5432;
this.postgresUser = options.postgresUser || 'openclaw';
this.postgresDb = options.postgresDb || 'openclaw';
this.redisHost = options.redisHost || 'localhost';
this.redisPort = options.redisPort || 6379;
}
/**
* Initialize backup directories
*/
async init() {
log.info('Initializing backup directories...');
await fs.ensureDir(this.backupDir);
await fs.ensureDir(path.join(this.backupDir, 'postgresql'));
await fs.ensureDir(path.join(this.backupDir, 'redis'));
await fs.ensureDir(path.join(this.backupDir, 'workspace'));
await fs.ensureDir(path.join(this.backupDir, 'agent-state'));
await fs.ensureDir(path.join(this.backupDir, 'config'));
await fs.ensureDir(this.tempDir);
log.success('Backup directories initialized');
}
/**
* Generate backup name
*/
generateBackupName(type = 'incremental') {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T').join('_');
const dayOfWeek = new Date().getDay();
// Full backup on Sunday (0), incremental otherwise
const backupType = type === 'auto'
? (dayOfWeek === 0 ? 'full' : 'incremental')
: type;
return `openclaw_${backupType}_${timestamp}`;
}
/**
* Create backup
*/
async create(options = {}) {
const {
type = 'incremental',
verify = false,
compress = true,
} = options;
log.section('Creating Backup');
await this.init();
const backupName = this.generateBackupName(type);
log.info(`Creating ${type} backup: ${backupName}`);
const results = {
name: backupName,
type,
timestamp: new Date().toISOString(),
components: {},
errors: [],
};
// Backup PostgreSQL
try {
results.components.postgresql = await this.backupPostgres(backupName, compress);
} catch (error) {
results.errors.push({ component: 'postgresql', error: error.message });
}
// Backup Redis
try {
results.components.redis = await this.backupRedis(backupName);
} catch (error) {
results.errors.push({ component: 'redis', error: error.message });
}
// Backup workspace
try {
results.components.workspace = await this.backupWorkspace(backupName, compress);
} catch (error) {
results.errors.push({ component: 'workspace', error: error.message });
}
// Backup agent state
try {
results.components.agentState = await this.backupAgentState(backupName, compress);
} catch (error) {
results.errors.push({ component: 'agent-state', error: error.message });
}
// Backup configuration
try {
results.components.config = await this.backupConfig(backupName, compress);
} catch (error) {
results.errors.push({ component: 'config', error: error.message });
}
// Generate checksums
await this.generateChecksums(backupName);
// Verify if requested
if (verify) {
results.verification = await this.verify(backupName);
}
results.success = results.errors.length === 0;
if (results.success) {
log.success(`Backup created: ${backupName}`);
} else {
log.warn(`Backup completed with ${results.errors.length} error(s)`);
}
return results;
}
/**
* Backup PostgreSQL database
*/
async backupPostgres(backupName, compress = true) {
log.info('Backing up PostgreSQL...');
const outputFile = path.join(this.backupDir, 'postgresql', `${backupName}_postgresql.sql`);
try {
// Use pg_dump for backup
const password = process.env.POSTGRES_PASSWORD || '';
const env = { ...process.env, PGPASSWORD: password };
await execa('pg_dump', [
'-h', this.postgresHost,
'-p', this.postgresPort.toString(),
'-U', this.postgresUser,
'-d', this.postgresDb,
'-F', 'c', // Custom format
'-f', `${outputFile}.dump`,
], { env });
if (compress) {
await execa('gzip', ['-f', `${outputFile}.dump`]);
}
// Also create SQL text backup
await execa('pg_dump', [
'-h', this.postgresHost,
'-p', this.postgresPort.toString(),
'-U', this.postgresUser,
'-d', this.postgresDb,
'-f', outputFile,
], { env });
if (compress) {
await execa('gzip', ['-f', outputFile]);
}
log.success('PostgreSQL backup created');
return {
success: true,
files: [
`${outputFile}.dump.gz`,
`${outputFile}.gz`,
],
};
} catch (error) {
log.warn(`PostgreSQL backup skipped: ${error.message}`);
return { success: false, error: error.message };
}
}
/**
* Backup Redis database
*/
async backupRedis(backupName) {
log.info('Backing up Redis...');
const outputFile = path.join(this.backupDir, 'redis', `${backupName}_redis.rdb`);
try {
// Trigger BGSAVE
await execa('redis-cli', [
'-h', this.redisHost,
'-p', this.redisPort.toString(),
'BGSAVE',
]);
log.info('Waiting for Redis BGSAVE to complete...');
await new Promise(resolve => setTimeout(resolve, 5000));
// Try to copy RDB file
const rdbPaths = ['/var/lib/redis/dump.rdb', '/var/lib/redis/6379/dump.rdb'];
for (const rdbPath of rdbPaths) {
if (await fs.pathExists(rdbPath)) {
await fs.copy(rdbPath, outputFile);
log.success('Redis backup created');
return { success: true, files: [outputFile] };
}
}
// Fallback: Use --rdb option
await execa('redis-cli', [
'-h', this.redisHost,
'-p', this.redisPort.toString(),
'--rdb', outputFile,
]);
log.success('Redis backup created');
return { success: true, files: [outputFile] };
} catch (error) {
log.warn(`Redis backup skipped: ${error.message}`);
return { success: false, error: error.message };
}
}
/**
* Backup workspace
*/
async backupWorkspace(backupName, compress = true) {
log.info('Backing up workspace...');
const outputFile = path.join(this.backupDir, 'workspace', `${backupName}_workspace.tar.gz`);
const excludePatterns = [
'node_modules',
'.git',
'*.log',
'*.tmp',
'.next',
'dist',
'build',
];
const tarArgs = ['-cf', outputFile];
for (const pattern of excludePatterns) {
tarArgs.push('--exclude', pattern);
}
tarArgs.push('-C', this.rootDir, '.');
try {
await execa('tar', tarArgs);
log.success('Workspace backup created');
return { success: true, files: [outputFile] };
} catch (error) {
log.warn(`Workspace backup skipped: ${error.message}`);
return { success: false, error: error.message };
}
}
/**
* Backup agent state
*/
async backupAgentState(backupName, compress = true) {
log.info('Backing up agent state...');
const outputFile = path.join(this.backupDir, 'agent-state', `${backupName}_agent-state.tar.gz`);
const homeDir = process.env.HOME || '';
const statePaths = [
path.join(homeDir, '.openclaw', 'agents'),
path.join(homeDir, '.openclaw', 'cron'),
].filter(p => p && !p.includes('undefined'));
if (statePaths.length === 0) {
log.info('No agent state paths found');
return { success: true, files: [], skipped: true };
}
try {
await execa('tar', ['-czf', outputFile, ...statePaths]);
log.success('Agent state backup created');
return { success: true, files: [outputFile] };
} catch (error) {
log.warn(`Agent state backup skipped: ${error.message}`);
return { success: false, error: error.message };
}
}
/**
* Backup configuration
*/
async backupConfig(backupName, compress = true) {
log.info('Backing up configuration...');
const outputFile = path.join(this.backupDir, 'config', `${backupName}_config.tar.gz`);
const configFiles = [
path.join(this.rootDir, 'openclaw.json'),
path.join(this.rootDir, '.env'),
path.join(this.rootDir, 'litellm_config.yaml'),
path.join(process.env.HOME || '', '.openclaw', 'openclaw.json'),
].filter(f => f && !f.includes('undefined'));
const existingFiles = [];
for (const file of configFiles) {
if (await fs.pathExists(file)) {
existingFiles.push(file);
}
}
if (existingFiles.length === 0) {
log.info('No configuration files found');
return { success: true, files: [], skipped: true };
}
try {
await execa('tar', ['-czf', outputFile, ...existingFiles]);
log.success('Configuration backup created');
return { success: true, files: [outputFile] };
} catch (error) {
log.warn(`Configuration backup skipped: ${error.message}`);
return { success: false, error: error.message };
}
}
/**
* Generate checksums for backup
*/
async generateChecksums(backupName) {
log.info('Generating checksums...');
const checksumFile = path.join(this.backupDir, `${backupName}_checksums.sha256`);
try {
const { stdout } = await execa('find', [
this.backupDir,
'-maxdepth', '2',
'-name', `${backupName}*`,
'-type', 'f',
'-exec', 'sha256sum', '{}', ';',
]);
await fs.writeFile(checksumFile, stdout);
log.success('Checksums generated');
return checksumFile;
} catch (error) {
log.warn(`Checksum generation failed: ${error.message}`);
return null;
}
}
/**
* Verify backup
*/
async verify(backupName) {
log.info(`Verifying backup: ${backupName}`);
const results = {
valid: true,
checks: [],
};
// Check PostgreSQL backup
const pgDump = path.join(this.backupDir, 'postgresql', `${backupName}_postgresql.sql.gz`);
if (await fs.pathExists(pgDump)) {
try {
await execa('gzip', ['-t', pgDump]);
results.checks.push({ name: 'postgresql', valid: true });
} catch (error) {
results.checks.push({ name: 'postgresql', valid: false, error: error.message });
results.valid = false;
}
}
// Check workspace backup
const workspaceTar = path.join(this.backupDir, 'workspace', `${backupName}_workspace.tar.gz`);
if (await fs.pathExists(workspaceTar)) {
try {
await execa('tar', ['-tzf', workspaceTar]);
results.checks.push({ name: 'workspace', valid: true });
} catch (error) {
results.checks.push({ name: 'workspace', valid: false, error: error.message });
results.valid = false;
}
}
// Verify checksums
const checksumFile = path.join(this.backupDir, `${backupName}_checksums.sha256`);
if (await fs.pathExists(checksumFile)) {
try {
await execa('sha256sum', ['-c', path.basename(checksumFile)], {
cwd: path.dirname(checksumFile),
});
results.checks.push({ name: 'checksums', valid: true });
} catch (error) {
results.checks.push({ name: 'checksums', valid: false, error: error.message });
results.valid = false;
}
}
if (results.valid) {
log.success('Backup verification passed');
} else {
log.warn('Backup verification failed');
}
return results;
}
/**
* List backups
*/
async list() {
log.info('Listing backups...');
try {
const files = await fs.readdir(this.backupDir);
const backups = [];
const backupGroups = {};
for (const file of files) {
if (file.startsWith('openclaw_') && !file.endsWith('.sha256')) {
const stat = await fs.stat(path.join(this.backupDir, file));
const type = file.includes('full') ? 'full' : 'incremental';
const date = file.match(/_(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2})/)?.[1];
if (!backupGroups[date]) {
backupGroups[date] = { name: file, type, date, size: stat.size, components: [] };
}
backupGroups[date].components.push(file);
}
}
for (const [date, backup] of Object.entries(backupGroups)) {
backups.push(backup);
}
return backups.sort((a, b) => new Date(b.date) - new Date(a.date));
} catch (error) {
log.error(`Failed to list backups: ${error.message}`);
return [];
}
}
/**
* Restore from backup
*/
async restore(backupName, options = {}) {
const { components = ['all'], confirm = false } = options;
log.section('Restoring Backup');
log.info(`Restoring from: ${backupName}`);
if (!confirm) {
log.warn('This will overwrite existing data. Use --confirm to proceed.');
return { success: false, error: 'Confirmation required' };
}
// Stop services before restore
log.info('Stopping services...');
try {
await execa('systemctl', ['stop', 'openclaw-gateway']);
} catch {
log.debug('Gateway service not running');
}
const results = {
name: backupName,
timestamp: new Date().toISOString(),
components: {},
errors: [],
};
// Restore PostgreSQL
if (components.includes('all') || components.includes('postgresql')) {
try {
results.components.postgresql = await this.restorePostgres(backupName);
} catch (error) {
results.errors.push({ component: 'postgresql', error: error.message });
}
}
// Restore Redis
if (components.includes('all') || components.includes('redis')) {
try {
results.components.redis = await this.restoreRedis(backupName);
} catch (error) {
results.errors.push({ component: 'redis', error: error.message });
}
}
// Restore workspace
if (components.includes('all') || components.includes('workspace')) {
try {
results.components.workspace = await this.restoreWorkspace(backupName);
} catch (error) {
results.errors.push({ component: 'workspace', error: error.message });
}
}
// Restore configuration
if (components.includes('all') || components.includes('config')) {
try {
results.components.config = await this.restoreConfig(backupName);
} catch (error) {
results.errors.push({ component: 'config', error: error.message });
}
}
// Start services after restore
log.info('Starting services...');
try {
await execa('systemctl', ['start', 'openclaw-gateway']);
} catch {
log.debug('Could not start gateway service');
}
results.success = results.errors.length === 0;
if (results.success) {
log.success(`Backup restored: ${backupName}`);
} else {
log.warn(`Restore completed with ${results.errors.length} error(s)`);
}
return results;
}
/**
* Restore PostgreSQL
*/
async restorePostgres(backupName) {
log.info('Restoring PostgreSQL...');
const dumpFile = path.join(this.backupDir, 'postgresql', `${backupName}_postgresql.sql.gz`);
if (!await fs.pathExists(dumpFile)) {
return { success: false, error: 'Backup file not found' };
}
try {
const password = process.env.POSTGRES_PASSWORD || '';
const env = { ...process.env, PGPASSWORD: password };
// Decompress and restore
const gunzip = execa('gunzip', ['-c', dumpFile]);
const psql = execa('psql', [
'-h', this.postgresHost,
'-p', this.postgresPort.toString(),
'-U', this.postgresUser,
'-d', this.postgresDb,
], { env });
gunzip.stdout.pipe(psql.stdin);
await psql;
log.success('PostgreSQL restored');
return { success: true };
} catch (error) {
log.error(`PostgreSQL restore failed: ${error.message}`);
return { success: false, error: error.message };
}
}
/**
* Restore Redis
*/
async restoreRedis(backupName) {
log.info('Restoring Redis...');
const rdbFile = path.join(this.backupDir, 'redis', `${backupName}_redis.rdb`);
if (!await fs.pathExists(rdbFile)) {
return { success: false, error: 'Backup file not found' };
}
try {
// Stop Redis
await execa('systemctl', ['stop', 'redis-server']);
// Copy RDB file
const redisDir = '/var/lib/redis';
await fs.copy(rdbFile, path.join(redisDir, 'dump.rdb'));
await execa('chown', ['redis:redis', path.join(redisDir, 'dump.rdb')]);
// Start Redis
await execa('systemctl', ['start', 'redis-server']);
log.success('Redis restored');
return { success: true };
} catch (error) {
log.error(`Redis restore failed: ${error.message}`);
return { success: false, error: error.message };
}
}
/**
* Restore workspace
*/
async restoreWorkspace(backupName) {
log.info('Restoring workspace...');
const tarFile = path.join(this.backupDir, 'workspace', `${backupName}_workspace.tar.gz`);
if (!await fs.pathExists(tarFile)) {
return { success: false, error: 'Backup file not found' };
}
try {
await execa('tar', ['-xzf', tarFile, '-C', this.rootDir]);
log.success('Workspace restored');
return { success: true };
} catch (error) {
log.error(`Workspace restore failed: ${error.message}`);
return { success: false, error: error.message };
}
}
/**
* Restore configuration
*/
async restoreConfig(backupName) {
log.info('Restoring configuration...');
const tarFile = path.join(this.backupDir, 'config', `${backupName}_config.tar.gz`);
if (!await fs.pathExists(tarFile)) {
return { success: false, error: 'Backup file not found' };
}
try {
await execa('tar', ['-xzf', tarFile, '-C', this.rootDir]);
log.success('Configuration restored');
return { success: true };
} catch (error) {
log.error(`Configuration restore failed: ${error.message}`);
return { success: false, error: error.message };
}
}
/**
* Delete backup
*/
async delete(backupName) {
log.info(`Deleting backup: ${backupName}`);
try {
const files = await fs.readdir(this.backupDir, { recursive: true });
for (const file of files) {
if (file.includes(backupName)) {
const fullPath = path.join(this.backupDir, file);
await fs.remove(fullPath);
}
}
log.success(`Backup deleted: ${backupName}`);
return true;
} catch (error) {
log.error(`Failed to delete backup: ${error.message}`);
return false;
}
}
/**
* Rotate old backups
*/
async rotate() {
log.info(`Rotating backups older than ${this.retentionDays} days...`);
try {
let deleted = 0;
const files = await fs.readdir(this.backupDir, { recursive: true });
for (const file of files) {
const fullPath = path.join(this.backupDir, file);
const stat = await fs.stat(fullPath);
const age = Date.now() - stat.mtimeMs;
const ageDays = age / (1000 * 60 * 60 * 24);
if (ageDays > this.retentionDays) {
await fs.remove(fullPath);
deleted++;
}
}
log.success(`Rotation complete: ${deleted} file(s) deleted`);
return deleted;
} catch (error) {
log.error(`Rotation failed: ${error.message}`);
return 0;
}
}
/**
* Get backup schedule
*/
getSchedule() {
return {
full: 'Sunday at 2:00 AM',
incremental: 'Daily at 2:00 AM',
retention: `${this.retentionDays} days`,
};
}
}
export default BackupManager;
+461
View File
@@ -0,0 +1,461 @@
/**
* Bare Metal Deployer
*
* Handles bare metal (non-Docker) deployment for OpenClaw.
* Installs and configures services directly on the host system.
*/
import { execa } from 'execa';
import fs from 'fs-extra';
import path from 'path';
import os from 'os';
import log from './logger.js';
class BareMetalDeployer {
constructor(options = {}) {
this.rootDir = options.rootDir || process.cwd();
this.installDir = options.installDir || '/opt/openclaw';
this.configDir = options.configDir || '/etc/openclaw';
this.dataDir = options.dataDir || '/var/lib/openclaw';
this.logDir = options.logDir || '/var/log/openclaw';
this.systemdDir = '/etc/systemd/system';
}
/**
* Detect OS and package manager
*/
async detectOS() {
try {
const { stdout } = await execa('cat', ['/etc/os-release']);
const lines = stdout.split('\n');
const osInfo = {};
for (const line of lines) {
const [key, value] = line.split('=');
if (key && value) {
osInfo[key] = value.replace(/"/g, '');
}
}
let packageManager = 'apt';
let installCommand = 'apt-get install -y';
if (osInfo.ID_LIKE?.includes('rhel') || osInfo.ID === 'rhel' || osInfo.ID === 'centos' || osInfo.ID === 'fedora') {
packageManager = 'dnf';
installCommand = 'dnf install -y';
} else if (osInfo.ID === 'arch' || osInfo.ID_LIKE?.includes('arch')) {
packageManager = 'pacman';
installCommand = 'pacman -S --noconfirm';
}
return {
id: osInfo.ID,
name: osInfo.NAME,
version: osInfo.VERSION_ID,
packageManager,
installCommand,
};
} catch (error) {
log.warn(`Failed to detect OS: ${error.message}`);
return {
id: 'unknown',
name: 'Unknown',
packageManager: 'apt',
installCommand: 'apt-get install -y',
};
}
}
/**
* Check if running as root
*/
checkRoot() {
return process.geteuid && process.geteuid() === 0;
}
/**
* Check prerequisites
*/
async checkPrerequisites() {
const checks = {
root: this.checkRoot(),
node: false,
nodeVersion: null,
postgres: false,
redis: false,
ollama: false,
};
// Check Node.js
try {
const { stdout } = await execa('node', ['--version']);
checks.node = true;
checks.nodeVersion = stdout.trim();
} catch {
checks.node = false;
}
// Check PostgreSQL
try {
await execa('pg_isready', ['-h', 'localhost']);
checks.postgres = true;
} catch {
checks.postgres = false;
}
// Check Redis
try {
const { stdout } = await execa('redis-cli', ['ping']);
checks.redis = stdout.trim() === 'PONG';
} catch {
checks.redis = false;
}
// Check Ollama
try {
await execa('curl', ['-s', 'http://localhost:11434/api/tags']);
checks.ollama = true;
} catch {
checks.ollama = false;
}
return checks;
}
/**
* Install system dependencies
*/
async installDependencies() {
const osInfo = await this.detectOS();
log.info(`Detected ${osInfo.name} - using ${osInfo.packageManager}`);
const packages = [
'nodejs',
'npm',
'postgresql',
'postgresql-contrib',
'redis-server',
'curl',
'wget',
'git',
];
log.info('Installing system dependencies...');
try {
// Update package lists
await execa('apt-get', ['update'], { stdio: 'inherit' });
// Install packages
await execa('apt-get', ['install', '-y', ...packages], { stdio: 'inherit' });
log.success('System dependencies installed');
return true;
} catch (error) {
log.error(`Failed to install dependencies: ${error.message}`);
return false;
}
}
/**
* Install Node.js specific version
*/
async installNodeJS(version = '20') {
log.info(`Installing Node.js v${version}...`);
try {
// Use NodeSource repository
await execa('curl', ['-fsSL', `https://deb.nodesource.com/setup_${version}.x`, '-o', '/tmp/nodesource_setup.sh']);
await execa('bash', ['/tmp/nodesource_setup.sh'], { stdio: 'inherit' });
await execa('apt-get', ['install', '-y', 'nodejs'], { stdio: 'inherit' });
const { stdout } = await execa('node', ['--version']);
log.success(`Node.js ${stdout.trim()} installed`);
return true;
} catch (error) {
log.error(`Failed to install Node.js: ${error.message}`);
return false;
}
}
/**
* Install Ollama
*/
async installOllama() {
log.info('Installing Ollama...');
try {
await execa('curl', ['-fsSL', 'https://ollama.com/install.sh', '-o', '/tmp/ollama_install.sh']);
await execa('bash', ['/tmp/ollama_install.sh'], { stdio: 'inherit' });
log.success('Ollama installed');
return true;
} catch (error) {
log.error(`Failed to install Ollama: ${error.message}`);
return false;
}
}
/**
* Setup PostgreSQL database
*/
async setupPostgres(options = {}) {
const {
user = 'openclaw',
password = this.generatePassword(),
database = 'openclaw'
} = options;
log.info('Setting up PostgreSQL...');
try {
// Start PostgreSQL service
await execa('systemctl', ['start', 'postgresql']);
await execa('systemctl', ['enable', 'postgresql']);
// Create user and database
await execa('sudo', ['-u', 'postgres', 'psql', '-c', `CREATE USER ${user} WITH PASSWORD '${password}';`]);
await execa('sudo', ['-u', 'postgres', 'psql', '-c', `CREATE DATABASE ${database} OWNER ${user};`]);
await execa('sudo', ['-u', 'postgres', 'psql', '-c', `GRANT ALL PRIVILEGES ON DATABASE ${database} TO ${user};`]);
// Install pgvector extension
await execa('sudo', ['-u', 'postgres', 'psql', '-d', database, '-c', 'CREATE EXTENSION IF NOT EXISTS vector;']);
log.success(`PostgreSQL setup complete: database=${database}, user=${user}`);
return { user, password, database, host: 'localhost', port: 5432 };
} catch (error) {
log.error(`Failed to setup PostgreSQL: ${error.message}`);
throw error;
}
}
/**
* Setup Redis
*/
async setupRedis() {
log.info('Setting up Redis...');
try {
// Start Redis service
await execa('systemctl', ['start', 'redis-server']);
await execa('systemctl', ['enable', 'redis-server']);
// Configure Redis
const redisConfig = `
# OpenClaw Redis Configuration
bind 127.0.0.1
port 6379
protected-mode yes
daemonize yes
pidfile /var/run/redis/redis-server.pid
logfile /var/log/redis/redis-server.log
dir /var/lib/redis
`;
await fs.writeFile('/etc/redis/redis.conf', redisConfig);
await execa('systemctl', ['restart', 'redis-server']);
log.success('Redis setup complete');
return { host: 'localhost', port: 6379 };
} catch (error) {
log.error(`Failed to setup Redis: ${error.message}`);
throw error;
}
}
/**
* Create systemd service
*/
async createSystemService(name, config) {
const serviceContent = `[Unit]
Description=OpenClaw ${config.displayName || name}
After=network.target ${config.after || 'postgresql.service redis-server.service'}
${config.wants ? `Wants=${config.wants}` : ''}
[Service]
Type=${config.type || 'simple'}
User=${config.user || 'openclaw'}
Group=${config.group || 'openclaw'}
WorkingDirectory=${config.workingDir || this.installDir}
Environment=${config.environment || 'NODE_ENV=production'}
ExecStart=${config.execStart}
ExecReload=${config.execReload || `/bin/kill -s HUP $MAINPID`}
ExecStop=${config.execStop || '/bin/kill -s TERM $MAINPID'}
Restart=${config.restart || 'always'}
RestartSec=${config.restartSec || '10s'}
StandardOutput=append:${this.logDir}/${name}.log
StandardError=append:${this.logDir}/${name}.error.log
# Security hardening
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
`;
const servicePath = path.join(this.systemdDir, `openclaw-${name}.service`);
await fs.writeFile(servicePath, serviceContent);
// Reload systemd
await execa('systemctl', ['daemon-reload']);
log.success(`Created systemd service: openclaw-${name}.service`);
return servicePath;
}
/**
* Install OpenClaw application
*/
async installApp() {
log.info('Installing OpenClaw application...');
try {
// Create directories
await fs.ensureDir(this.installDir);
await fs.ensureDir(this.configDir);
await fs.ensureDir(this.dataDir);
await fs.ensureDir(this.logDir);
// Copy application files
const files = [
'package.json',
'openclaw.json',
'litellm_config.yaml',
'scripts/',
'agents/',
'dashboard/',
];
for (const file of files) {
const src = path.join(this.rootDir, file);
const dest = path.join(this.installDir, file);
if (await fs.pathExists(src)) {
await fs.copy(src, dest);
}
}
// Install npm dependencies
log.info('Installing npm dependencies...');
await execa('npm', ['install', '--production'], {
cwd: this.installDir,
stdio: 'inherit'
});
// Create system user
try {
await execa('useradd', ['-r', '-s', '/bin/false', 'openclaw']);
} catch {
// User might already exist
log.debug('User openclaw may already exist');
}
// Set permissions
await execa('chown', ['-R', 'openclaw:openclaw', this.installDir]);
await execa('chown', ['-R', 'openclaw:openclaw', this.logDir]);
log.success('OpenClaw application installed');
return true;
} catch (error) {
log.error(`Failed to install application: ${error.message}`);
return false;
}
}
/**
* Start services
*/
async startServices(services = []) {
const defaultServices = ['gateway', 'litellm', 'ollama'];
const toStart = services.length > 0 ? services : defaultServices;
log.info('Starting OpenClaw services...');
for (const service of toStart) {
try {
await execa('systemctl', ['start', `openclaw-${service}`]);
await execa('systemctl', ['enable', `openclaw-${service}`]);
log.success(`Started openclaw-${service}`);
} catch (error) {
log.error(`Failed to start openclaw-${service}: ${error.message}`);
}
}
return true;
}
/**
* Stop services
*/
async stopServices(services = []) {
const defaultServices = ['gateway', 'litellm', 'ollama'];
const toStop = services.length > 0 ? services : defaultServices;
log.info('Stopping OpenClaw services...');
for (const service of toStop) {
try {
await execa('systemctl', ['stop', `openclaw-${service}`]);
log.success(`Stopped openclaw-${service}`);
} catch (error) {
log.error(`Failed to stop openclaw-${service}: ${error.message}`);
}
}
return true;
}
/**
* Get service status
*/
async status() {
const services = ['gateway', 'litellm', 'ollama', 'postgres', 'redis'];
const results = [];
for (const service of services) {
try {
const { stdout } = await execa('systemctl', ['is-active', `openclaw-${service}`]);
results.push({
name: service,
active: stdout.trim() === 'active',
status: stdout.trim(),
});
} catch {
results.push({
name: service,
active: false,
status: 'inactive',
});
}
}
return results;
}
/**
* Generate secure password
*/
generatePassword(length = 24) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
/**
* Health check
*/
async healthCheck() {
const status = await this.status();
const allHealthy = status.every(s => s.active);
return {
healthy: allHealthy,
services: status,
};
}
}
export default BareMetalDeployer;
+646
View File
@@ -0,0 +1,646 @@
/**
* Cloud Deployer
*
* Handles cloud deployment for OpenClaw using Terraform.
* Supports AWS, GCP, and Azure.
*/
import { execa } from 'execa';
import fs from 'fs-extra';
import path from 'path';
import log from './logger.js';
class CloudDeployer {
constructor(options = {}) {
this.rootDir = options.rootDir || process.cwd();
this.provider = options.provider || 'aws';
this.terraformDir = options.terraformDir || path.join(this.rootDir, 'terraform', this.provider);
this.stateDir = options.stateDir || path.join(this.rootDir, '.terraform', this.provider);
this.projectName = options.projectName || 'openclaw';
this.region = options.region || this.getDefaultRegion();
}
/**
* Get default region based on provider
*/
getDefaultRegion() {
const defaults = {
aws: 'us-east-1',
gcp: 'us-central1',
azure: 'eastus',
};
return defaults[this.provider] || 'us-east-1';
}
/**
* Check if Terraform is available
*/
async checkTerraform() {
try {
await execa('terraform', ['version']);
return { available: true };
} catch (error) {
return {
available: false,
error: 'Terraform is not installed or not in PATH'
};
}
}
/**
* Check cloud provider CLI
*/
async checkProviderCLI() {
const cliCommands = {
aws: { command: 'aws', args: ['--version'] },
gcp: { command: 'gcloud', args: ['--version'] },
azure: { command: 'az', args: ['--version'] },
};
const cli = cliCommands[this.provider];
if (!cli) {
return { available: false, error: `Unknown provider: ${this.provider}` };
}
try {
await execa(cli.command, cli.args);
return { available: true };
} catch (error) {
return {
available: false,
error: `${cli.command} CLI is not installed or not in PATH`
};
}
}
/**
* Authenticate with cloud provider
*/
async authenticate() {
log.info(`Authenticating with ${this.provider.toUpperCase()}...`);
const authChecks = {
aws: async () => {
try {
await execa('aws', ['sts', 'get-caller-identity']);
return true;
} catch {
return false;
}
},
gcp: async () => {
try {
await execa('gcloud', ['auth', 'list']);
return true;
} catch {
return false;
}
},
azure: async () => {
try {
await execa('az', ['account', 'show']);
return true;
} catch {
return false;
}
},
};
const isAuthenticated = await authChecks[this.provider]?.();
if (!isAuthenticated) {
log.warn(`Not authenticated with ${this.provider.toUpperCase()}`);
log.info('Run the following command to authenticate:');
const authCommands = {
aws: 'aws configure',
gcp: 'gcloud auth login',
azure: 'az login',
};
console.log(` ${authCommands[this.provider]}`);
return false;
}
log.success(`Authenticated with ${this.provider.toUpperCase()}`);
return true;
}
/**
* Initialize Terraform
*/
async init() {
log.info('Initializing Terraform...');
try {
const terraform = await this.checkTerraform();
if (!terraform.available) {
throw new Error(terraform.error);
}
// Ensure state directory exists
await fs.ensureDir(this.stateDir);
await execa('terraform', ['init'], {
cwd: this.terraformDir,
stdio: 'inherit',
});
log.success('Terraform initialized');
return true;
} catch (error) {
log.error(`Terraform init failed: ${error.message}`);
throw error;
}
}
/**
* Validate Terraform configuration
*/
async validate() {
log.info('Validating Terraform configuration...');
try {
const terraform = await this.checkTerraform();
if (!terraform.available) {
throw new Error(terraform.error);
}
await execa('terraform', ['validate'], {
cwd: this.terraformDir,
});
log.success('Terraform configuration is valid');
return true;
} catch (error) {
log.error(`Terraform validation failed: ${error.message}`);
return false;
}
}
/**
* Plan Terraform deployment
*/
async plan(options = {}) {
const { out = 'tfplan', destroy = false } = options;
log.info(`Creating Terraform ${destroy ? 'destroy' : 'deployment'} plan...`);
try {
const terraform = await this.checkTerraform();
if (!terraform.available) {
throw new Error(terraform.error);
}
const args = ['plan', '-out', out];
if (destroy) {
args.push('-destroy');
}
args.push(
'-var', `project_name=${this.projectName}`,
'-var', `region=${this.region}`
);
await execa('terraform', args, {
cwd: this.terraformDir,
stdio: 'inherit',
});
log.success('Terraform plan created');
return out;
} catch (error) {
log.error(`Terraform plan failed: ${error.message}`);
throw error;
}
}
/**
* Apply Terraform deployment
*/
async apply(options = {}) {
const { autoApprove = false, planFile = null } = options;
log.info('Applying Terraform deployment...');
try {
const terraform = await this.checkTerraform();
if (!terraform.available) {
throw new Error(terraform.error);
}
const args = ['apply'];
if (autoApprove) {
args.push('-auto-approve');
}
if (planFile) {
args.push(planFile);
}
await execa('terraform', args, {
cwd: this.terraformDir,
stdio: 'inherit',
});
log.success('Terraform deployment applied');
return true;
} catch (error) {
log.error(`Terraform apply failed: ${error.message}`);
throw error;
}
}
/**
* Destroy Terraform deployment
*/
async destroy(options = {}) {
const { autoApprove = false } = options;
log.info('Destroying Terraform deployment...');
try {
const terraform = await this.checkTerraform();
if (!terraform.available) {
throw new Error(terraform.error);
}
const args = ['destroy'];
if (autoApprove) {
args.push('-auto-approve');
}
args.push(
'-var', `project_name=${this.projectName}`,
'-var', `region=${this.region}`
);
await execa('terraform', args, {
cwd: this.terraformDir,
stdio: 'inherit',
});
log.success('Terraform deployment destroyed');
return true;
} catch (error) {
log.error(`Terraform destroy failed: ${error.message}`);
throw error;
}
}
/**
* Get deployment outputs
*/
async getOutputs() {
try {
const terraform = await this.checkTerraform();
if (!terraform.available) {
throw new Error(terraform.error);
}
const { stdout } = await execa('terraform', ['output', '-json'], {
cwd: this.terraformDir,
});
return JSON.parse(stdout);
} catch (error) {
log.error(`Failed to get outputs: ${error.message}`);
return {};
}
}
/**
* Get deployment status
*/
async status() {
try {
const outputs = await this.getOutputs();
const status = {
provider: this.provider,
projectName: this.projectName,
region: this.region,
resources: {},
};
// Parse outputs based on provider
if (this.provider === 'aws') {
status.resources = {
eksCluster: outputs.eks_cluster_endpoint?.value,
rdsInstance: outputs.rds_endpoint?.value,
elasticacheCluster: outputs.elasticache_endpoint?.value,
loadBalancer: outputs.load_balancer_dns?.value,
};
} else if (this.provider === 'gcp') {
status.resources = {
gkeCluster: outputs.gke_cluster_endpoint?.value,
cloudSqlInstance: outputs.cloud_sql_connection_name?.value,
memorystoreInstance: outputs.memorystore_host?.value,
loadBalancer: outputs.load_balancer_ip?.value,
};
} else if (this.provider === 'azure') {
status.resources = {
aksCluster: outputs.aks_fqdn?.value,
cosmosDB: outputs.cosmosdb_endpoint?.value,
redisCache: outputs.redis_host?.value,
loadBalancer: outputs.load_balancer_ip?.value,
};
}
return status;
} catch (error) {
return {
provider: this.provider,
projectName: this.projectName,
error: error.message,
};
}
}
/**
* Generate Terraform configuration
*/
async generateConfig(options = {}) {
const {
instanceType,
nodeCount,
storageSize,
enableMonitoring,
enableBackup,
} = options;
log.info('Generating Terraform configuration...');
// Create terraform directory structure
const modulesDir = path.join(this.terraformDir, 'modules');
await fs.ensureDir(modulesDir);
await fs.ensureDir(path.join(modulesDir, 'kubernetes'));
await fs.ensureDir(path.join(modulesDir, 'database'));
await fs.ensureDir(path.join(modulesDir, 'cache'));
// Generate main.tf
const mainTf = this.generateMainTf({ instanceType, nodeCount, storageSize, enableMonitoring, enableBackup });
await fs.writeFile(path.join(this.terraformDir, 'main.tf'), mainTf);
// Generate variables.tf
const variablesTf = this.generateVariablesTf();
await fs.writeFile(path.join(this.terraformDir, 'variables.tf'), variablesTf);
// Generate outputs.tf
const outputsTf = this.generateOutputsTf();
await fs.writeFile(path.join(this.terraformDir, 'outputs.tf'), outputsTf);
log.success('Terraform configuration generated');
return true;
}
/**
* Generate main.tf for AWS
*/
generateMainTf(options = {}) {
const { instanceType = 't3.medium', nodeCount = 2, storageSize = 100 } = options;
return `# OpenClaw AWS Terraform Configuration
# Generated by openclaw CLI
terraform {
required_version = ">= 1.0.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.0"
}
helm = {
source = "hashicorp/helm"
version = "~> 2.0"
}
}
}
provider "aws" {
region = var.region
}
# VPC
module "vpc" {
source = "./modules/vpc"
cidr_block = "10.0.0.0/16"
public_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
private_subnets = ["10.0.10.0/24", "10.0.11.0/24", "10.0.12.0/24"]
tags = {
Name = "\${var.project_name}-vpc"
Project = var.project_name
Environment = var.environment
}
}
# EKS Cluster
module "eks" {
source = "./modules/eks"
cluster_name = var.project_name
cluster_version = "1.28"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnets
node_groups = {
default = {
instance_types = ["${instanceType}"]
capacity_type = "ON_DEMAND"
desired_size = ${nodeCount}
max_size = ${nodeCount + 2}
min_size = ${nodeCount}
}
}
}
# RDS PostgreSQL
module "rds" {
source = "./modules/rds"
identifier = "\${var.project_name}-postgres"
engine = "postgres"
engine_version = "15"
instance_class = "db.t3.medium"
allocated_storage = ${storageSize}
max_allocated_storage = ${storageSize * 2}
db_name = "openclaw"
username = "openclaw"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnets
tags = {
Project = var.project_name
}
}
# ElastiCache Redis
module "elasticache" {
source = "./modules/elasticache"
cluster_id = "\${var.project_name}-redis"
engine = "redis"
engine_version = "7.0"
node_type = "cache.t3.medium"
num_nodes = 1
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnets
tags = {
Project = var.project_name
}
}
# ECR Repository
resource "aws_ecr_repository" "openclaw" {
name = var.project_name
image_tag_mutability = "MUTABLE"
image_scanning_configuration {
scan_on_push = true
}
tags = {
Project = var.project_name
}
}
`;
}
/**
* Generate variables.tf
*/
generateVariablesTf() {
return `# OpenClaw Variables
variable "project_name" {
description = "Project name used for resource naming"
type = string
default = "openclaw"
}
variable "region" {
description = "AWS region"
type = string
default = "us-east-1"
}
variable "environment" {
description = "Environment name"
type = string
default = "production"
}
variable "instance_type" {
description = "EC2 instance type for EKS nodes"
type = string
default = "t3.medium"
}
variable "node_count" {
description = "Number of EKS nodes"
type = number
default = 2
}
variable "storage_size" {
description = "RDS storage size in GB"
type = number
default = 100
}
variable "enable_monitoring" {
description = "Enable CloudWatch monitoring"
type = bool
default = true
}
variable "enable_backup" {
description = "Enable automated backups"
type = bool
default = true
}
`;
}
/**
* Generate outputs.tf
*/
generateOutputsTf() {
return `# OpenClaw Outputs
output "eks_cluster_endpoint" {
description = "EKS cluster endpoint"
value = module.eks.cluster_endpoint
}
output "eks_cluster_name" {
description = "EKS cluster name"
value = module.eks.cluster_name
}
output "rds_endpoint" {
description = "RDS endpoint"
value = module.rds.endpoint
}
output "elasticache_endpoint" {
description = "ElastiCache endpoint"
value = module.elasticache.endpoint
}
output "ecr_repository_url" {
description = "ECR repository URL"
value = aws_ecr_repository.openclaw.repository_url
}
output "vpc_id" {
description = "VPC ID"
value = module.vpc.vpc_id
}
`;
}
/**
* Health check
*/
async healthCheck() {
try {
const status = await this.status();
if (status.error) {
return { healthy: false, error: status.error };
}
// Check if all resources are available
const allResourcesAvailable = Object.values(status.resources).every(r => r);
return {
healthy: allResourcesAvailable,
details: status,
};
} catch (error) {
return {
healthy: false,
error: error.message,
};
}
}
}
export default CloudDeployer;
+363
View File
@@ -0,0 +1,363 @@
/**
* Configuration Manager
*
* Manages OpenClaw configuration files including openclaw.json, .env,
* and deployment-specific configurations.
*/
import fs from 'fs-extra';
import path from 'path';
import { fileURLToPath } from 'url';
import yaml from 'yaml';
import dotenv from 'dotenv';
import log from './logger.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Default paths
const HOME_DIR = process.env.HOME || process.env.USERPROFILE || '';
const DEFAULT_OPENCLAW_DIR = path.join(HOME_DIR, '.openclaw');
const DEFAULT_WORKSPACE_DIR = path.join(DEFAULT_OPENCLAW_DIR, 'workspace');
const DEFAULT_AGENTS_DIR = path.join(DEFAULT_OPENCLAW_DIR, 'agents');
// Configuration schema
const configSchema = {
version: { type: 'string', required: true },
collective: {
type: 'object',
required: true,
properties: {
name: { type: 'string', required: true },
description: { type: 'string', required: true },
version: { type: 'string', required: true },
},
},
models: { type: 'object', required: true },
agents: { type: 'array', required: true },
model_routing: { type: 'object', required: false },
};
class ConfigManager {
constructor(options = {}) {
this.rootDir = options.rootDir || process.cwd();
this.openclawDir = options.openclawDir || DEFAULT_OPENCLAW_DIR;
this.configPath = options.configPath || path.join(this.rootDir, 'openclaw.json');
this.envPath = options.envPath || path.join(this.rootDir, '.env');
this.config = null;
this.env = {};
}
/**
* Load configuration from openclaw.json
*/
async load() {
try {
if (!await fs.pathExists(this.configPath)) {
log.warn(`Configuration file not found: ${this.configPath}`);
return null;
}
const content = await fs.readFile(this.configPath, 'utf-8');
this.config = JSON.parse(content);
log.debug(`Loaded configuration from ${this.configPath}`);
return this.config;
} catch (error) {
log.error(`Failed to load configuration: ${error.message}`);
throw error;
}
}
/**
* Save configuration to openclaw.json
*/
async save(config = null) {
try {
const configToSave = config || this.config;
if (!configToSave) {
throw new Error('No configuration to save');
}
await fs.ensureDir(path.dirname(this.configPath));
await fs.writeFile(this.configPath, JSON.stringify(configToSave, null, 2), 'utf-8');
log.success(`Configuration saved to ${this.configPath}`);
return configToSave;
} catch (error) {
log.error(`Failed to save configuration: ${error.message}`);
throw error;
}
}
/**
* Load environment variables from .env file
*/
async loadEnv() {
try {
if (!await fs.pathExists(this.envPath)) {
log.debug(`Environment file not found: ${this.envPath}`);
return {};
}
const content = await fs.readFile(this.envPath, 'utf-8');
this.env = dotenv.parse(content);
// Merge with process.env
Object.entries(this.env).forEach(([key, value]) => {
if (!process.env[key]) {
process.env[key] = value;
}
});
log.debug(`Loaded environment from ${this.envPath}`);
return this.env;
} catch (error) {
log.error(`Failed to load environment: ${error.message}`);
throw error;
}
}
/**
* Save environment variables to .env file
*/
async saveEnv(env = null) {
try {
const envToSave = env || this.env;
// Generate .env content
let content = '# Heretek OpenClaw Environment Configuration\n';
content += `# Generated on ${new Date().toISOString()}\n\n`;
// Group environment variables by category
const categories = {
'LiteLLM Gateway': ['LITELLM_MASTER_KEY', 'LITELLM_SALT_KEY', 'LITELLM_HOST'],
'AI Provider API Keys': ['MINIMAX_API_KEY', 'ZAI_API_KEY', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'GOOGLE_API_KEY', 'OLLAMA_HOST'],
'Database': ['POSTGRES_USER', 'POSTGRES_PASSWORD', 'POSTGRES_DB', 'DATABASE_URL', 'REDIS_URL'],
'OpenClaw': ['OPENCLAW_DIR', 'OPENCLAW_WORKSPACE', 'DEPLOYMENT_TYPE'],
'Observability': ['LANGFUSE_ENABLED', 'LANGFUSE_PUBLIC_KEY', 'LANGFUSE_SECRET_KEY', 'LANGFUSE_HOST'],
};
for (const [category, keys] of Object.entries(categories)) {
content += `# =============================================================================\n`;
content += `# ${category}\n`;
content += `# =============================================================================\n`;
for (const key of keys) {
if (envToSave[key] !== undefined) {
content += `${key}=${envToSave[key]}\n`;
} else if (process.env[key]) {
content += `${key}=${process.env[key]}\n`;
}
}
content += '\n';
}
await fs.writeFile(this.envPath, content, 'utf-8');
log.success(`Environment saved to ${this.envPath}`);
return envToSave;
} catch (error) {
log.error(`Failed to save environment: ${error.message}`);
throw error;
}
}
/**
* Validate configuration against schema
*/
validate(config = null) {
const configToValidate = config || this.config;
const errors = [];
const warnings = [];
if (!configToValidate) {
errors.push('No configuration to validate');
return { valid: false, errors, warnings };
}
// Check required top-level fields
for (const [field, schema] of Object.entries(configSchema)) {
if (schema.required && !(field in configToValidate)) {
errors.push(`Missing required field: ${field}`);
} else if (field in configToValidate) {
const value = configToValidate[field];
// Type checking
if (schema.type === 'string' && typeof value !== 'string') {
errors.push(`Field '${field}' must be a string`);
} else if (schema.type === 'object' && (typeof value !== 'object' || value === null)) {
errors.push(`Field '${field}' must be an object`);
} else if (schema.type === 'array' && !Array.isArray(value)) {
errors.push(`Field '${field}' must be an array`);
}
// Nested property validation
if (schema.type === 'object' && schema.properties && typeof value === 'object') {
for (const [prop, propSchema] of Object.entries(schema.properties)) {
if (propSchema.required && !(prop in value)) {
errors.push(`Missing required property '${field}.${prop}'`);
}
}
}
}
}
// Validate agents array
if (Array.isArray(configToValidate.agents)) {
const agentIds = new Set();
for (const agent of configToValidate.agents) {
if (!agent.id) {
errors.push('Agent missing required "id" field');
} else if (agentIds.has(agent.id)) {
errors.push(`Duplicate agent ID: ${agent.id}`);
} else {
agentIds.add(agent.id);
}
if (!agent.model) {
warnings.push(`Agent '${agent.id}' missing model assignment`);
}
}
}
// Validate model routing
if (configToValidate.model_routing) {
if (!configToValidate.model_routing.default) {
warnings.push('model_routing.default not set');
}
}
const valid = errors.length === 0;
return { valid, errors, warnings };
}
/**
* Get a specific configuration value
*/
get(key, defaultValue = undefined) {
if (!this.config) {
return defaultValue;
}
const keys = key.split('.');
let value = this.config;
for (const k of keys) {
if (value === undefined || value === null) {
return defaultValue;
}
value = value[k];
}
return value !== undefined ? value : defaultValue;
}
/**
* Set a specific configuration value
*/
set(key, value) {
if (!this.config) {
this.config = {};
}
const keys = key.split('.');
let current = this.config;
for (let i = 0; i < keys.length - 1; i++) {
const k = keys[i];
if (!(k in current)) {
current[k] = {};
}
current = current[k];
}
current[keys[keys.length - 1]] = value;
return this.config;
}
/**
* Get environment variable
*/
getEnv(key, defaultValue = undefined) {
return this.env[key] || process.env[key] || defaultValue;
}
/**
* Set environment variable
*/
setEnv(key, value) {
this.env[key] = value;
process.env[key] = value;
}
/**
* Create default configuration
*/
createDefault() {
return {
version: '2.0.0',
collective: {
name: 'OpenClaw Collective',
description: 'Self-improving autonomous agent collective',
version: '2.0.0',
},
models: {
providers: {
ollama: {
type: 'ollama',
models: [
{ id: 'ollama/llama2', name: 'Llama 2' },
{ id: 'ollama/nomic-embed-text-v2-moe', name: 'Nomic Embed' },
],
},
},
},
agents: [
{
id: 'steward',
name: 'Steward',
role: 'Orchestrator',
model: 'agent/steward',
port: 18790,
},
],
model_routing: {
default: 'ollama/llama2',
aliases: {
failover: 'ollama/llama2',
},
},
};
}
/**
* Initialize configuration directory
*/
async initConfigDir() {
try {
await fs.ensureDir(this.openclawDir);
await fs.ensureDir(DEFAULT_WORKSPACE_DIR);
await fs.ensureDir(DEFAULT_AGENTS_DIR);
log.success(`Initialized OpenClaw directory: ${this.openclawDir}`);
return this.openclawDir;
} catch (error) {
log.error(`Failed to initialize config directory: ${error.message}`);
throw error;
}
}
/**
* Get configuration paths
*/
getPaths() {
return {
rootDir: this.rootDir,
openclawDir: this.openclawDir,
workspaceDir: DEFAULT_WORKSPACE_DIR,
agentsDir: DEFAULT_AGENTS_DIR,
configPath: this.configPath,
envPath: this.envPath,
};
}
}
export default ConfigManager;
+406
View File
@@ -0,0 +1,406 @@
/**
* Deployment Manager
*
* Unified deployment manager that abstracts different deployment types.
* Supports Docker, Bare Metal, Kubernetes, and Cloud deployments.
*/
import path from 'path';
import fs from 'fs-extra';
import log from './logger.js';
import DockerDeployer from './docker-deployer.js';
import BareMetalDeployer from './baremetal-deployer.js';
import KubernetesDeployer from './kubernetes-deployer.js';
import CloudDeployer from './cloud-deployer.js';
// Deployment types
export const DeploymentType = {
DOCKER: 'docker',
BARE_METAL: 'bare-metal',
KUBERNETES: 'kubernetes',
VM: 'vm',
AWS: 'aws',
GCP: 'gcp',
AZURE: 'azure',
};
// Deployment status
export const DeploymentStatus = {
NOT_DEPLOYED: 'not-deployed',
DEPLOYING: 'deploying',
RUNNING: 'running',
STOPPED: 'stopped',
ERROR: 'error',
UNKNOWN: 'unknown',
};
class DeploymentManager {
constructor(options = {}) {
this.rootDir = options.rootDir || process.cwd();
this.configPath = options.configPath || path.join(this.rootDir, 'openclaw.json');
this.deploymentType = options.deploymentType || DeploymentType.DOCKER;
this.deployer = null;
this.config = null;
this.initializeDeployer();
}
/**
* Initialize the appropriate deployer based on deployment type
*/
initializeDeployer() {
const commonOptions = { rootDir: this.rootDir };
switch (this.deploymentType) {
case DeploymentType.DOCKER:
this.deployer = new DockerDeployer(commonOptions);
break;
case DeploymentType.BARE_METAL:
case DeploymentType.VM:
this.deployer = new BareMetalDeployer(commonOptions);
break;
case DeploymentType.KUBERNETES:
this.deployer = new KubernetesDeployer(commonOptions);
break;
case DeploymentType.AWS:
case DeploymentType.GCP:
case DeploymentType.AZURE:
this.deployer = new CloudDeployer({
...commonOptions,
provider: this.deploymentType,
});
break;
default:
throw new Error(`Unknown deployment type: ${this.deploymentType}`);
}
log.debug(`Initialized ${this.deploymentType} deployer`);
}
/**
* Set deployment type
*/
setDeploymentType(type) {
if (this.deploymentType !== type) {
this.deploymentType = type;
this.initializeDeployer();
}
}
/**
* Load configuration
*/
async loadConfig() {
try {
if (!await fs.pathExists(this.configPath)) {
log.warn(`Configuration file not found: ${this.configPath}`);
return null;
}
const content = await fs.readFile(this.configPath, 'utf-8');
this.config = JSON.parse(content);
return this.config;
} catch (error) {
log.error(`Failed to load configuration: ${error.message}`);
throw error;
}
}
/**
* Check prerequisites for deployment
*/
async checkPrerequisites() {
log.info('Checking deployment prerequisites...');
const results = {
config: false,
deployer: false,
environment: false,
};
// Check configuration
try {
await this.loadConfig();
results.config = this.config !== null;
if (results.config) {
log.success('Configuration loaded');
} else {
log.warn('Configuration not found');
}
} catch (error) {
log.error(`Configuration check failed: ${error.message}`);
}
// Check deployer-specific prerequisites
try {
if (this.deployer instanceof DockerDeployer) {
const dockerCheck = await this.deployer.checkDocker();
const composeCheck = await this.deployer.checkDockerCompose();
results.deployer = dockerCheck.available && composeCheck.available;
if (!dockerCheck.available) log.warn('Docker not available');
if (!composeCheck.available) log.warn('Docker Compose not available');
} else if (this.deployer instanceof BareMetalDeployer) {
const checks = await this.deployer.checkPrerequisites();
results.deployer = checks.node;
log.info(`Node.js: ${checks.node ? checks.nodeVersion : 'not installed'}`);
} else if (this.deployer instanceof KubernetesDeployer) {
const kubectlCheck = await this.deployer.checkKubectl();
const clusterCheck = await this.deployer.checkCluster();
results.deployer = kubectlCheck.available && clusterCheck.connected;
} else if (this.deployer instanceof CloudDeployer) {
const terraformCheck = await this.deployer.checkTerraform();
const providerCheck = await this.deployer.checkProviderCLI();
results.deployer = terraformCheck.available && providerCheck.available;
}
if (results.deployer) {
log.success('Deployer prerequisites met');
}
} catch (error) {
log.error(`Deployer check failed: ${error.message}`);
}
// Check environment
results.environment = true;
log.success('Environment check passed');
const allPassed = Object.values(results).every(r => r);
return {
passed: allPassed,
results,
};
}
/**
* Deploy OpenClaw
*/
async deploy(options = {}) {
log.section('Deploying OpenClaw');
// Check prerequisites
const prereqs = await this.checkPrerequisites();
if (!prereqs.passed) {
log.error('Prerequisites check failed. Cannot proceed with deployment.');
return false;
}
try {
if (this.deployer instanceof DockerDeployer) {
return await this.deployDocker(options);
} else if (this.deployer instanceof BareMetalDeployer) {
return await this.deployBareMetal(options);
} else if (this.deployer instanceof KubernetesDeployer) {
return await this.deployKubernetes(options);
} else if (this.deployer instanceof CloudDeployer) {
return await this.deployCloud(options);
}
} catch (error) {
log.error(`Deployment failed: ${error.message}`);
return false;
}
}
/**
* Deploy using Docker
*/
async deployDocker(options = {}) {
const { build = false, forceRecreate = false, pull = true } = options;
log.info('Starting Docker deployment...');
// Pull images
if (pull) {
await this.deployer.pull();
}
// Validate compose file
const validation = await this.deployer.validateComposeFile();
if (!validation.valid) {
throw new Error(validation.error);
}
// Start deployment
const success = await this.deployer.up({ build, forceRecreate });
if (success) {
log.success('Docker deployment complete');
}
return success;
}
/**
* Deploy to Bare Metal
*/
async deployBareMetal(options = {}) {
const { installDeps = true, configureServices = true } = options;
log.info('Starting Bare Metal deployment...');
// Install system dependencies
if (installDeps) {
await this.deployer.installDependencies();
}
// Setup PostgreSQL
const pgConfig = await this.deployer.setupPostgres();
// Setup Redis
await this.deployer.setupRedis();
// Install application
await this.deployer.installApp();
// Create and start services
if (configureServices) {
await this.deployer.startServices();
}
log.success('Bare Metal deployment complete');
return true;
}
/**
* Deploy to Kubernetes
*/
async deployKubernetes(options = {}) {
const {
method = 'helm',
values = [],
set = {},
overlay = 'default',
} = options;
log.info('Starting Kubernetes deployment...');
// Create namespace
await this.deployer.createNamespace();
if (method === 'helm') {
await this.deployer.deployHelm({ values, set });
} else if (method === 'kustomize') {
await this.deployer.deployKustomize({ overlay });
} else {
throw new Error(`Unknown Kubernetes deployment method: ${method}`);
}
log.success('Kubernetes deployment complete');
return true;
}
/**
* Deploy to Cloud
*/
async deployCloud(options = {}) {
const { autoApprove = false, generateConfig = true, configOptions = {} } = options;
log.info('Starting Cloud deployment...');
// Authenticate
const authenticated = await this.deployer.authenticate();
if (!authenticated) {
throw new Error('Cloud authentication failed');
}
// Generate config if needed
if (generateConfig) {
await this.deployer.generateConfig(configOptions);
}
// Initialize Terraform
await this.deployer.init();
// Validate
const valid = await this.deployer.validate();
if (!valid) {
throw new Error('Terraform validation failed');
}
// Plan and apply
const planFile = await this.deployer.plan();
await this.deployer.apply({ autoApprove, planFile });
log.success('Cloud deployment complete');
return true;
}
/**
* Stop deployment
*/
async stop(options = {}) {
log.info('Stopping deployment...');
try {
if (this.deployer instanceof DockerDeployer) {
return await this.deployer.down(options);
} else if (this.deployer instanceof BareMetalDeployer) {
return await this.deployer.stopServices();
} else if (this.deployer instanceof KubernetesDeployer) {
return await this.deployer.delete();
} else if (this.deployer instanceof CloudDeployer) {
return await this.deployer.destroy(options);
}
} catch (error) {
log.error(`Failed to stop deployment: ${error.message}`);
return false;
}
}
/**
* Get deployment status
*/
async status() {
try {
if (this.deployer instanceof DockerDeployer) {
return await this.deployer.status();
} else if (this.deployer instanceof BareMetalDeployer) {
return await this.deployer.status();
} else if (this.deployer instanceof KubernetesDeployer) {
return await this.deployer.status();
} else if (this.deployer instanceof CloudDeployer) {
return await this.deployer.status();
}
} catch (error) {
return { error: error.message };
}
}
/**
* View logs
*/
async logs(options = {}) {
if (this.deployer instanceof DockerDeployer) {
return await this.deployer.logs(options);
} else if (this.deployer instanceof KubernetesDeployer) {
return await this.deployer.logs(options);
} else {
log.warn('Logs command not available for this deployment type');
return null;
}
}
/**
* Run health check
*/
async healthCheck() {
if (this.deployer) {
return await this.deployer.healthCheck();
}
return { healthy: false, error: 'No deployer initialized' };
}
/**
* Get deployment info
*/
async info() {
return {
type: this.deploymentType,
status: await this.status(),
health: await this.healthCheck(),
config: this.config,
};
}
}
export default DeploymentManager;
+363
View File
@@ -0,0 +1,363 @@
/**
* Docker Deployer
*
* Handles Docker Compose deployment for OpenClaw.
*/
import { execa } from 'execa';
import fs from 'fs-extra';
import path from 'path';
import log from './logger.js';
class DockerDeployer {
constructor(options = {}) {
this.composeFile = options.composeFile || 'docker-compose.yml';
this.projectName = options.projectName || 'openclaw';
this.rootDir = options.rootDir || process.cwd();
}
/**
* Check if Docker is available
*/
async checkDocker() {
try {
await execa('docker', ['--version']);
return { available: true };
} catch (error) {
return {
available: false,
error: 'Docker is not installed or not in PATH'
};
}
}
/**
* Check if Docker Compose is available
*/
async checkDockerCompose() {
try {
// Try new compose command first
await execa('docker', ['compose', 'version']);
return { available: true, command: 'docker compose' };
} catch {
try {
// Fallback to docker-compose
await execa('docker-compose', ['--version']);
return { available: true, command: 'docker-compose' };
} catch (error) {
return {
available: false,
error: 'Docker Compose is not installed'
};
}
}
}
/**
* Validate Docker Compose file
*/
async validateComposeFile() {
const composePath = path.join(this.rootDir, this.composeFile);
if (!await fs.pathExists(composePath)) {
return {
valid: false,
error: `Compose file not found: ${composePath}`
};
}
try {
const composeCmd = await this.checkDockerCompose();
if (!composeCmd.available) {
return { valid: false, error: composeCmd.error };
}
await execa(composeCmd.command, ['-f', composePath, 'config'], {
cwd: this.rootDir,
});
return { valid: true };
} catch (error) {
return {
valid: false,
error: `Compose file validation failed: ${error.message}`
};
}
}
/**
* Pull latest images
*/
async pull() {
log.info('Pulling latest Docker images...');
try {
const composeCmd = await this.checkDockerCompose();
if (!composeCmd.available) {
throw new Error(composeCmd.error);
}
await execa(composeCmd.command, ['-f', this.composeFile, 'pull'], {
cwd: this.rootDir,
stdio: 'inherit',
});
log.success('Docker images pulled successfully');
return true;
} catch (error) {
log.error(`Failed to pull images: ${error.message}`);
return false;
}
}
/**
* Start deployment
*/
async up(options = {}) {
const { detach = true, build = false, forceRecreate = false } = options;
log.info('Starting Docker Compose deployment...');
try {
const composeCmd = await this.checkDockerCompose();
if (!composeCmd.available) {
throw new Error(composeCmd.error);
}
const args = ['-f', this.composeFile, 'up'];
if (detach) args.push('-d');
if (build) args.push('--build');
if (forceRecreate) args.push('--force-recreate');
await execa(composeCmd.command, args, {
cwd: this.rootDir,
stdio: 'inherit',
});
log.success('Docker Compose deployment started');
return true;
} catch (error) {
log.error(`Failed to start deployment: ${error.message}`);
return false;
}
}
/**
* Stop deployment
*/
async down(options = {}) {
const { removeVolumes = false, removeOrphans = false } = options;
log.info('Stopping Docker Compose deployment...');
try {
const composeCmd = await this.checkDockerCompose();
if (!composeCmd.available) {
throw new Error(composeCmd.error);
}
const args = ['-f', this.composeFile, 'down'];
if (removeVolumes) args.push('-v');
if (removeOrphans) args.push('--remove-orphans');
await execa(composeCmd.command, args, {
cwd: this.rootDir,
stdio: 'inherit',
});
log.success('Docker Compose deployment stopped');
return true;
} catch (error) {
log.error(`Failed to stop deployment: ${error.message}`);
return false;
}
}
/**
* Restart services
*/
async restart(services = []) {
log.info('Restarting services...');
try {
const composeCmd = await this.checkDockerCompose();
if (!composeCmd.available) {
throw new Error(composeCmd.error);
}
const args = ['-f', this.composeFile, 'restart'];
if (services.length > 0) {
args.push(...services);
}
await execa(composeCmd.command, args, {
cwd: this.rootDir,
stdio: 'inherit',
});
log.success('Services restarted');
return true;
} catch (error) {
log.error(`Failed to restart services: ${error.message}`);
return false;
}
}
/**
* Get service status
*/
async status() {
try {
const composeCmd = await this.checkDockerCompose();
if (!composeCmd.available) {
throw new Error(composeCmd.error);
}
const { stdout } = await execa(composeCmd.command, ['-f', this.composeFile, 'ps'], {
cwd: this.rootDir,
});
return {
running: true,
output: stdout,
};
} catch (error) {
return {
running: false,
error: error.message,
};
}
}
/**
* View logs
*/
async logs(options = {}) {
const { services = [], follow = false, tail = 'all', timestamps = false } = options;
try {
const composeCmd = await this.checkDockerCompose();
if (!composeCmd.available) {
throw new Error(composeCmd.error);
}
const args = ['-f', this.composeFile, 'logs'];
if (follow) args.push('-f');
if (tail !== 'all') args.push('--tail', tail.toString());
if (timestamps) args.push('-t');
if (services.length > 0) {
args.push(...services);
}
const subprocess = execa(composeCmd.command, args, {
cwd: this.rootDir,
stdio: 'inherit',
});
return subprocess;
} catch (error) {
log.error(`Failed to view logs: ${error.message}`);
throw error;
}
}
/**
* Execute command in container
*/
async exec(service, command, options = {}) {
try {
const composeCmd = await this.checkDockerCompose();
if (!composeCmd.available) {
throw new Error(composeCmd.error);
}
const args = ['-f', this.composeFile, 'exec'];
if (options.workdir) args.push('-w', options.workdir);
if (options.user) args.push('-u', options.user);
if (options.env) {
for (const [key, value] of Object.entries(options.env)) {
args.push('-e', `${key}=${value}`);
}
}
args.push(service, ...command);
const { stdout } = await execa(composeCmd.command, args, {
cwd: this.rootDir,
});
return stdout;
} catch (error) {
log.error(`Failed to execute command in ${service}: ${error.message}`);
throw error;
}
}
/**
* Run database migration
*/
async migrate() {
log.info('Running database migrations...');
return this.exec('gateway', ['npm', 'run', 'migrate']);
}
/**
* Health check
*/
async healthCheck() {
const status = await this.status();
if (!status.running) {
return { healthy: false, error: status.error };
}
// Parse ps output to check container health
const lines = status.output.split('\n').filter(l => l.trim());
const unhealthy = lines.some(line =>
line.includes('unhealthy') || line.includes('exited') || line.includes('dead')
);
return {
healthy: !unhealthy,
details: status.output,
};
}
/**
* Get container info
*/
async info() {
try {
const composeCmd = await this.checkDockerCompose();
if (!composeCmd.available) {
throw new Error(composeCmd.error);
}
const { stdout } = await execa(composeCmd.command, [
'-f', this.composeFile, 'ps', '--format', 'json'
], {
cwd: this.rootDir,
});
const containers = stdout.split('\n').filter(l => l.trim()).map(l => JSON.parse(l));
return {
containers,
count: containers.length,
};
} catch (error) {
return {
containers: [],
count: 0,
error: error.message,
};
}
}
}
export default DockerDeployer;
+405
View File
@@ -0,0 +1,405 @@
/**
* Health Checker
*
* Comprehensive health checking for OpenClaw services.
*/
import { execa } from 'execa';
import axios from 'axios';
import log from './logger.js';
class HealthChecker {
constructor(options = {}) {
this.timeout = options.timeout || 5000;
this.gatewayUrl = options.gatewayUrl || 'http://localhost:18789';
this.litellmUrl = options.litellmUrl || 'http://localhost:4000';
this.postgresHost = options.postgresHost || 'localhost';
this.postgresPort = options.postgresPort || 5432;
this.redisHost = options.redisHost || 'localhost';
this.redisPort = options.redisPort || 6379;
this.ollamaUrl = options.ollamaUrl || 'http://localhost:11434';
this.langfuseUrl = options.langfuseUrl || 'http://localhost:3000';
}
/**
* Run all health checks
*/
async checkAll() {
const checks = {
gateway: await this.checkGateway(),
litellm: await this.checkLiteLLM(),
postgres: await this.checkPostgres(),
redis: await this.checkRedis(),
ollama: await this.checkOllama(),
langfuse: await this.checkLangfuse(),
agents: await this.checkAgents(),
};
const allHealthy = Object.values(checks).every(c => c.healthy);
return {
healthy: allHealthy,
timestamp: new Date().toISOString(),
checks,
};
}
/**
* Check Gateway health
*/
async checkGateway() {
try {
const response = await axios.get(`${this.gatewayUrl}/health`, {
timeout: this.timeout,
});
return {
healthy: true,
status: response.status,
responseTime: response.headers['x-response-time'] || 'unknown',
data: response.data,
};
} catch (error) {
return {
healthy: false,
error: error.message,
status: error.response?.status,
};
}
}
/**
* Check LiteLLM health
*/
async checkLiteLLM() {
try {
const response = await axios.get(`${this.litellmUrl}/health`, {
timeout: this.timeout,
headers: {
'Authorization': `Bearer ${process.env.LITELLM_MASTER_KEY || 'heretek-master-key-change-me'}`,
},
});
// Get model count
const modelsResponse = await axios.get(`${this.litellmUrl}/v1/models`, {
timeout: this.timeout,
headers: {
'Authorization': `Bearer ${process.env.LITELLM_MASTER_KEY || 'heretek-master-key-change-me'}`,
},
});
return {
healthy: true,
status: response.status,
modelCount: modelsResponse.data?.data?.length || 0,
responseTime: response.headers['x-response-time'] || 'unknown',
version: response.data?.version || 'unknown',
};
} catch (error) {
return {
healthy: false,
error: error.message,
status: error.response?.status,
};
}
}
/**
* Check PostgreSQL health
*/
async checkPostgres() {
try {
// Try pg_isready
try {
await execa('pg_isready', [
'-h', this.postgresHost,
'-p', this.postgresPort.toString(),
], { timeout: this.timeout / 1000 });
// Check pgvector extension
try {
const { stdout } = await execa('psql', [
'-h', this.postgresHost,
'-p', this.postgresPort.toString(),
'-U', process.env.POSTGRES_USER || 'openclaw',
'-d', process.env.POSTGRES_DB || 'openclaw',
'-t', '-c',
"SELECT 1 FROM pg_extension WHERE extname='vector';",
], { timeout: this.timeout / 1000 });
return {
healthy: true,
pgvector: stdout.trim() === '1',
responseTime: 'unknown',
};
} catch {
return {
healthy: true,
pgvector: false,
responseTime: 'unknown',
};
}
} catch {
// Fallback: try to connect via psql
await execa('psql', [
'-h', this.postgresHost,
'-p', this.postgresPort.toString(),
'-U', process.env.POSTGRES_USER || 'openclaw',
'-d', process.env.POSTGRES_DB || 'openclaw',
'-c', 'SELECT 1;',
], { timeout: this.timeout / 1000 });
return {
healthy: true,
responseTime: 'unknown',
};
}
} catch (error) {
return {
healthy: false,
error: error.message,
};
}
}
/**
* Check Redis health
*/
async checkRedis() {
try {
const { stdout } = await execa('redis-cli', [
'-h', this.redisHost,
'-p', this.redisPort.toString(),
'ping',
], { timeout: this.timeout / 1000 });
if (stdout.trim() === 'PONG') {
// Get memory info
try {
const infoOutput = await execa('redis-cli', [
'-h', this.redisHost,
'-p', this.redisPort.toString(),
'info', 'memory',
], { timeout: this.timeout / 1000 });
const usedMemory = infoOutput.stdout.match(/used_memory_human:([^\r\n]+)/)?.[1]?.trim() || 'unknown';
return {
healthy: true,
memoryUsed: usedMemory,
responseTime: 'unknown',
};
} catch {
return {
healthy: true,
responseTime: 'unknown',
};
}
}
return {
healthy: false,
error: 'Redis returned unexpected response',
};
} catch (error) {
return {
healthy: false,
error: error.message,
};
}
}
/**
* Check Ollama health
*/
async checkOllama() {
try {
const response = await axios.get(`${this.ollamaUrl}/api/tags`, {
timeout: this.timeout,
});
const models = response.data?.models || [];
return {
healthy: true,
modelCount: models.length,
models: models.map(m => m.name),
responseTime: 'unknown',
};
} catch (error) {
return {
healthy: false,
error: error.message,
};
}
}
/**
* Check Langfuse health
*/
async checkLangfuse() {
try {
const response = await axios.get(`${this.langfuseUrl}/api/health`, {
timeout: this.timeout,
});
return {
healthy: true,
status: response.status,
responseTime: 'unknown',
};
} catch (error) {
return {
healthy: false,
error: error.message,
};
}
}
/**
* Check registered agents
*/
async checkAgents() {
try {
const response = await axios.get(`${this.litellmUrl}/v1/agents`, {
timeout: this.timeout,
headers: {
'Authorization': `Bearer ${process.env.LITELLM_MASTER_KEY || 'heretek-master-key-change-me'}`,
},
});
const agents = response.data?.agents || [];
return {
healthy: true,
agentCount: agents.length,
agents: agents.map(a => a.agent_name || a.name),
};
} catch (error) {
return {
healthy: false,
error: error.message,
};
}
}
/**
* Check specific service
*/
async checkService(service) {
const checkers = {
gateway: () => this.checkGateway(),
litellm: () => this.checkLiteLLM(),
postgres: () => this.checkPostgres(),
redis: () => this.checkRedis(),
ollama: () => this.checkOllama(),
langfuse: () => this.checkLangfuse(),
agents: () => this.checkAgents(),
};
const checker = checkers[service];
if (!checker) {
return {
healthy: false,
error: `Unknown service: ${service}`,
};
}
return await checker();
}
/**
* Generate health report
*/
async generateReport() {
const results = await this.checkAll();
const report = {
summary: {
healthy: results.healthy,
timestamp: results.timestamp,
totalChecks: Object.keys(results.checks).length,
healthyChecks: Object.values(results.checks).filter(c => c.healthy).length,
},
details: results.checks,
recommendations: [],
};
// Generate recommendations
for (const [service, check] of Object.entries(results.checks)) {
if (!check.healthy) {
report.recommendations.push({
service,
issue: check.error || 'Service unhealthy',
action: this.getRecommendation(service),
});
}
}
return report;
}
/**
* Get recommendation for a service
*/
getRecommendation(service) {
const recommendations = {
gateway: 'Check if Gateway is running and port 18789 is accessible',
litellm: 'Verify LiteLLM is running and API key is correct',
postgres: 'Ensure PostgreSQL is running and credentials are correct',
redis: 'Ensure Redis is running and accessible',
ollama: 'Start Ollama service and verify it is accessible',
langfuse: 'Check Langfuse deployment and configuration',
agents: 'Verify agents are deployed and connected to Gateway',
};
return recommendations[service] || 'Check service logs for more information';
}
/**
* Print health status
*/
printStatus(results) {
console.log('\n');
log.section('OpenClaw Health Status');
const overallStatus = results.healthy
? chalk.green('HEALTHY')
: chalk.red('UNHEALTHY');
console.log(`Overall Status: ${overallStatus}`);
console.log(`Timestamp: ${results.timestamp}`);
console.log('');
const chalk = (await import('chalk')).default;
for (const [service, check] of Object.entries(results.checks)) {
const status = check.healthy
? chalk.green('✓')
: chalk.red('✗');
console.log(`${status} ${service.toUpperCase()}`);
if (check.healthy) {
if (check.modelCount !== undefined) {
console.log(` Models: ${check.modelCount}`);
}
if (check.agentCount !== undefined) {
console.log(` Agents: ${check.agentCount}`);
}
if (check.pgvector !== undefined) {
console.log(` pgvector: ${check.pgvector ? 'enabled' : 'disabled'}`);
}
if (check.memoryUsed !== undefined) {
console.log(` Memory: ${check.memoryUsed}`);
}
} else {
console.log(` Error: ${check.error}`);
}
}
console.log('');
}
}
export default HealthChecker;
+439
View File
@@ -0,0 +1,439 @@
/**
* Kubernetes Deployer
*
* Handles Kubernetes deployment for OpenClaw using Helm or Kustomize.
*/
import { execa } from 'execa';
import fs from 'fs-extra';
import path from 'path';
import yaml from 'yaml';
import log from './logger.js';
class KubernetesDeployer {
constructor(options = {}) {
this.rootDir = options.rootDir || process.cwd();
this.namespace = options.namespace || 'openclaw';
this.releaseName = options.releaseName || 'openclaw';
this.chartDir = options.chartDir || path.join(this.rootDir, 'charts', 'openclaw');
this.kustomizeDir = options.kustomizeDir || path.join(this.rootDir, 'deploy', 'k8s');
}
/**
* Check if kubectl is available
*/
async checkKubectl() {
try {
await execa('kubectl', ['version', '--client']);
return { available: true };
} catch (error) {
return {
available: false,
error: 'kubectl is not installed or not in PATH'
};
}
}
/**
* Check cluster connectivity
*/
async checkCluster() {
try {
const kubectl = await this.checkKubectl();
if (!kubectl.available) {
return { connected: false, error: kubectl.error };
}
await execa('kubectl', ['cluster-info']);
return { connected: true };
} catch (error) {
return {
connected: false,
error: `Cannot connect to cluster: ${error.message}`
};
}
}
/**
* Check if Helm is available
*/
async checkHelm() {
try {
await execa('helm', ['version']);
return { available: true };
} catch (error) {
return {
available: false,
error: 'Helm is not installed or not in PATH'
};
}
}
/**
* Check if Kustomize is available
*/
async checkKustomize() {
try {
await execa('kustomize', ['version']);
return { available: true };
} catch {
try {
// kubectl has built-in kustomize
await execa('kubectl', ['version', '--client']);
return { available: true, builtin: true };
} catch (error) {
return {
available: false,
error: 'Kustomize is not installed'
};
}
}
}
/**
* Create namespace if not exists
*/
async createNamespace() {
log.info(`Ensuring namespace '${this.namespace}' exists...`);
try {
await execa('kubectl', ['create', 'namespace', this.namespace, '--dry-run=client', '-o', 'yaml']);
await execa('kubectl', ['apply', '-f', '-'], {
input: `apiVersion: v1\nkind: Namespace\nmetadata:\n name: ${this.namespace}\n`,
});
log.success(`Namespace '${this.namespace}' ready`);
return true;
} catch (error) {
log.error(`Failed to create namespace: ${error.message}`);
return false;
}
}
/**
* Deploy using Helm
*/
async deployHelm(options = {}) {
const {
values = [],
set = {},
wait = true,
timeout = '5m',
upgrade = false,
} = options;
const helm = await this.checkHelm();
if (!helm.available) {
throw new Error(helm.error);
}
log.info(`Deploying with Helm to namespace '${this.namespace}'...`);
try {
const args = upgrade ? ['upgrade', '--install'] : ['install'];
args.push(
this.releaseName,
this.chartDir,
'--namespace', this.namespace,
'--create-namespace'
);
if (wait) args.push('--wait');
if (timeout) args.push('--timeout', timeout);
for (const valueFile of values) {
args.push('-f', valueFile);
}
for (const [key, value] of Object.entries(set)) {
args.push('--set', `${key}=${value}`);
}
await execa('helm', args, { stdio: 'inherit' });
log.success(`Helm deployment complete: ${this.releaseName}`);
return true;
} catch (error) {
log.error(`Helm deployment failed: ${error.message}`);
throw error;
}
}
/**
* Deploy using Kustomize
*/
async deployKustomize(options = {}) {
const { overlay = 'default', dryRun = false } = options;
const kustomize = await this.checkKustomize();
if (!kustomize.available) {
throw new Error(kustomize.error);
}
log.info(`Deploying with Kustomize to namespace '${this.namespace}'...`);
try {
const overlayDir = path.join(this.kustomizeDir, 'overlays', overlay);
if (!await fs.pathExists(overlayDir)) {
throw new Error(`Overlay not found: ${overlayDir}`);
}
let manifest;
if (kustomize.builtin) {
const { stdout } = await execa('kubectl', ['kustomize', overlayDir]);
manifest = stdout;
} else {
const { stdout } = await execa('kustomize', ['build', overlayDir]);
manifest = stdout;
}
if (dryRun) {
log.info('Dry run mode - manifest generated but not applied');
console.log(manifest);
return true;
}
// Apply to cluster
await execa('kubectl', ['apply', '-f', '-'], {
input: manifest,
});
log.success('Kustomize deployment complete');
return true;
} catch (error) {
log.error(`Kustomize deployment failed: ${error.message}`);
throw error;
}
}
/**
* Apply raw manifests
*/
async applyManifests(manifestDir) {
log.info(`Applying manifests from ${manifestDir}...`);
try {
await execa('kubectl', ['apply', '-R', '-f', manifestDir], {
cwd: this.rootDir,
stdio: 'inherit',
});
log.success('Manifests applied');
return true;
} catch (error) {
log.error(`Failed to apply manifests: ${error.message}`);
throw error;
}
}
/**
* Get deployment status
*/
async status() {
try {
// Get pods
const podsResult = await execa('kubectl', [
'get', 'pods', '-n', this.namespace, '-o', 'json'
]);
const pods = JSON.parse(podsResult.stdout);
// Get deployments
const deploymentsResult = await execa('kubectl', [
'get', 'deployments', '-n', this.namespace, '-o', 'json'
]);
const deployments = JSON.parse(deploymentsResult.stdout);
// Get services
const servicesResult = await execa('kubectl', [
'get', 'services', '-n', this.namespace, '-o', 'json'
]);
const services = JSON.parse(servicesResult.stdout);
// Get statefulsets
const statefulsetsResult = await execa('kubectl', [
'get', 'statefulsets', '-n', this.namespace, '-o', 'json'
]);
const statefulsets = JSON.parse(statefulsetsResult.stdout);
return {
namespace: this.namespace,
pods: pods.items.map(p => ({
name: p.metadata.name,
status: p.status.phase,
ready: p.status.containerStatuses?.every(cs => cs.ready) || false,
restarts: p.status.containerStatuses?.reduce((acc, cs) => acc + cs.restartCount, 0) || 0,
})),
deployments: deployments.items.map(d => ({
name: d.metadata.name,
ready: d.status.readyReplicas || 0,
replicas: d.status.replicas || 0,
})),
services: services.items.map(s => ({
name: s.metadata.name,
type: s.spec.type,
clusterIP: s.spec.clusterIP,
ports: s.spec.ports?.map(p => p.port) || [],
})),
statefulsets: statefulsets.items.map(s => ({
name: s.metadata.name,
ready: s.status.readyReplicas || 0,
replicas: s.status.replicas || 0,
})),
};
} catch (error) {
return {
namespace: this.namespace,
error: error.message,
};
}
}
/**
* View logs
*/
async logs(options = {}) {
const {
selector,
container,
follow = false,
tail = 100,
timestamps = false,
} = options;
try {
const args = ['logs', '-n', this.namespace];
if (selector) args.push('-l', selector);
if (container) args.push('-c', container);
if (follow) args.push('-f');
if (tail) args.push('--tail', tail.toString());
if (timestamps) args.push('--timestamps');
const subprocess = execa('kubectl', args, {
stdio: 'inherit',
});
return subprocess;
} catch (error) {
log.error(`Failed to view logs: ${error.message}`);
throw error;
}
}
/**
* Scale deployment
*/
async scale(deployment, replicas) {
log.info(`Scaling ${deployment} to ${replicas} replicas...`);
try {
await execa('kubectl', [
'scale', 'deployment', deployment,
'--replicas', replicas.toString(),
'-n', this.namespace,
]);
log.success(`Scaled ${deployment} to ${replicas} replicas`);
return true;
} catch (error) {
log.error(`Failed to scale deployment: ${error.message}`);
return false;
}
}
/**
* Rollback deployment
*/
async rollback(deployment) {
log.info(`Rolling back ${deployment}...`);
try {
await execa('kubectl', [
'rollout', 'undo', 'deployment', deployment,
'-n', this.namespace,
]);
log.success(`Rolling back ${deployment}`);
return true;
} catch (error) {
log.error(`Failed to rollback: ${error.message}`);
return false;
}
}
/**
* Delete deployment
*/
async delete() {
log.info(`Deleting OpenClaw from namespace '${this.namespace}'...`);
try {
// If using Helm
const helm = await this.checkHelm();
if (helm.available) {
await execa('helm', ['uninstall', this.releaseName, '-n', this.namespace], {
stdio: 'inherit',
});
} else {
// Delete all resources
await execa('kubectl', ['delete', 'all', '--all', '-n', this.namespace], {
stdio: 'inherit',
});
}
log.success('Deployment deleted');
return true;
} catch (error) {
log.error(`Failed to delete deployment: ${error.message}`);
return false;
}
}
/**
* Health check
*/
async healthCheck() {
const status = await this.status();
if (status.error) {
return { healthy: false, error: status.error };
}
// Check if all pods are ready
const allPodsReady = status.pods?.every(p => p.ready) || false;
// Check if all deployments have ready replicas
const allDeploymentsReady = status.deployments?.every(d => d.ready > 0) || false;
return {
healthy: allPodsReady && allDeploymentsReady,
details: status,
};
}
/**
* Port forward a service
*/
async portForward(service, localPort, remotePort) {
log.info(`Port forwarding ${service} to localhost:${localPort}...`);
try {
const subprocess = execa('kubectl', [
'port-forward', '-n', this.namespace,
`service/${service}`,
`${localPort}:${remotePort}`,
], {
stdio: 'inherit',
});
return subprocess;
} catch (error) {
log.error(`Failed to port forward: ${error.message}`);
throw error;
}
}
}
export default KubernetesDeployer;
+161
View File
@@ -0,0 +1,161 @@
/**
* Logger Utility
*
* Provides consistent logging with colors and formatting for the CLI.
*/
import chalk from 'chalk';
const symbols = {
success: '✓',
error: '✗',
warning: '⚠',
info: '',
debug: '●',
arrow: '→',
check: '☑',
empty: '☐',
spinner: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
};
const levels = {
error: 'error',
warn: 'warn',
info: 'info',
debug: 'debug',
success: 'success',
};
class Logger {
constructor(options = {}) {
this.level = options.level || levels.info;
this.prefix = options.prefix || '';
this.showTimestamp = options.showTimestamp ?? false;
}
_getTimestamp() {
if (!this.showTimestamp) return '';
return chalk.dim(`[${new Date().toISOString().split('T')[1].split('.')[0]}] `);
}
_log(level, color, message, ...args) {
const timestamp = this._getTimestamp();
const prefix = this.prefix ? `${this.prefix} ` : '';
switch (level) {
case levels.error:
console.error(`${timestamp}${prefix}${chalk.red(symbols.error)} ${color(message)}`, ...args);
break;
case levels.warn:
console.warn(`${timestamp}${prefix}${chalk.yellow(symbols.warning)} ${color(message)}`, ...args);
break;
case levels.info:
console.info(`${timestamp}${prefix}${chalk.blue(symbols.info)} ${color(message)}`, ...args);
break;
case levels.debug:
if (this.level === levels.debug) {
console.debug(`${timestamp}${prefix}${chalk.gray(symbols.debug)} ${color(message)}`, ...args);
}
break;
case levels.success:
console.log(`${timestamp}${prefix}${chalk.green(symbols.success)} ${color(message)}`, ...args);
break;
}
}
error(message, ...args) {
this._log(levels.error, chalk.red, message, ...args);
}
warn(message, ...args) {
this._log(levels.warn, chalk.yellow, message, ...args);
}
info(message, ...args) {
this._log(levels.info, chalk.blue, message, ...args);
}
debug(message, ...args) {
this._log(levels.debug, chalk.gray, message, ...args);
}
success(message, ...args) {
this._log(levels.success, chalk.green, message, ...args);
}
/**
* Print a section header
*/
section(title) {
console.log(`\n${chalk.cyan('═'.repeat(60))}`);
console.log(`${chalk.cyan(symbols.arrow)} ${chalk.bold(title)}`);
console.log(`${chalk.cyan('═'.repeat(60))}\n`);
}
/**
* Print a sub-header
*/
subheader(title) {
console.log(`\n${chalk.yellow('─'.repeat(40))}`);
console.log(`${chalk.yellow('→')} ${chalk.bold(title)}`);
console.log(`${chalk.yellow('─'.repeat(40))}\n`);
}
/**
* Print a list item
*/
listItem(text, options = {}) {
const { indent = 2, symbol = '•', color = chalk.white } = options;
console.log(`${' '.repeat(indent)}${chalk.gray(symbol)} ${color(text)}`);
}
/**
* Print a key-value pair
*/
kv(key, value, options = {}) {
const { indent = 2, keyColor = chalk.cyan, valueColor = chalk.white } = options;
console.log(`${' '.repeat(indent)}${keyColor(`${key}:`)} ${valueColor(value)}`);
}
/**
* Print a box with a message
*/
box(message, options = {}) {
const { title, padding = 1, borderColor = chalk.cyan } = options;
const lines = message.split('\n');
const maxWidth = Math.max(...lines.map(l => l.length), title ? title.length : 0);
const innerWidth = maxWidth + padding * 2;
const horizontal = '─'.repeat(innerWidth);
console.log(borderColor('┌' + '─'.repeat(title ? innerWidth : innerWidth) + '┐'));
if (title) {
const titlePadding = ' '.repeat(Math.floor((innerWidth - title.length) / 2));
console.log(borderColor('│') + titlePadding + chalk.bold(title) + titlePadding + borderColor('│'));
console.log(borderColor('├' + horizontal + '┤'));
}
for (const line of lines) {
const rightPadding = ' '.repeat(innerWidth - line.length - padding);
const leftPadding = ' '.repeat(padding);
console.log(borderColor('│') + leftPadding + line + rightPadding + borderColor('│'));
}
console.log(borderColor('└' + horizontal + '┘'));
}
/**
* Create a progress bar
*/
progress(current, total, options = {}) {
const { width = 30, label = '' } = options;
const percentage = Math.round((current / total) * 100);
const filled = Math.round((width * current) / total);
const empty = width - filled;
const bar = chalk.green('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
console.log(`\r${label} [${bar}] ${percentage}% (${current}/${total})`);
}
}
// Create default logger instance
const log = new Logger();
export { Logger, log, symbols, levels };
export default log;
+191
View File
@@ -0,0 +1,191 @@
/**
* Prompts Utility
*
* Interactive prompts for CLI using Inquirer.
*/
import inquirer from 'inquirer';
/**
* Ask a text input question
*/
export async function promptText(question, options = {}) {
const { default: defaultValue, validate, message: errorMsg } = options;
const result = await inquirer.prompt([
{
type: 'input',
name: 'value',
message: question,
default: defaultValue,
validate: validate ? (input) => {
if (validate(input)) return true;
return errorMsg || 'Invalid input';
} : true,
},
]);
return result.value;
}
/**
* Ask a password question (hidden input)
*/
export async function promptPassword(question, options = {}) {
const { validate, mask = '*' } = options;
const result = await inquirer.prompt([
{
type: 'password',
name: 'value',
message: question,
mask,
validate: validate || ((input) => {
if (input && input.length >= 8) return true;
return 'Password must be at least 8 characters';
}),
},
]);
return result.value;
}
/**
* Ask a single-choice selection question
*/
export async function promptSelect(question, choices, options = {}) {
const { default: defaultValue } = options;
const result = await inquirer.prompt([
{
type: 'list',
name: 'value',
message: question,
choices,
default: defaultValue,
},
]);
return result.value;
}
/**
* Ask a multi-selection question
*/
export async function promptCheckbox(question, choices, options = {}) {
const { default: defaultValue, validate } = options;
const result = await inquirer.prompt([
{
type: 'checkbox',
name: 'value',
message: question,
choices,
default: defaultValue,
validate: validate || ((input) => {
if (input.length > 0) return true;
return 'Please select at least one option';
}),
},
]);
return result.value;
}
/**
* Ask a confirm (yes/no) question
*/
export async function promptConfirm(question, options = {}) {
const { default: defaultValue = false } = options;
const result = await inquirer.prompt([
{
type: 'confirm',
name: 'value',
message: question,
default: defaultValue,
},
]);
return result.value;
}
/**
* Ask for a number input
*/
export async function promptNumber(question, options = {}) {
const { min, max, default: defaultValue, validate } = options;
const result = await inquirer.prompt([
{
type: 'number',
name: 'value',
message: question,
default: defaultValue,
validate: validate || ((input) => {
const num = Number(input);
if (isNaN(num)) return 'Please enter a valid number';
if (min !== undefined && num < min) return `Value must be at least ${min}`;
if (max !== undefined && num > max) return `Value must be at most ${max}`;
return true;
}),
},
]);
return result.value;
}
/**
* Ask for an editor input (opens system editor)
*/
export async function promptEditor(question, options = {}) {
const { default: defaultValue } = options;
const result = await inquirer.prompt([
{
type: 'editor',
name: 'value',
message: question,
default: defaultValue,
},
]);
return result.value;
}
/**
* Run a sequence of prompts (wizard style)
*/
export async function promptSequence(questions) {
const result = await inquirer.prompt(questions);
return result;
}
/**
* Create a progress spinner using Ora
*/
export async function withSpinner(message, action) {
const ora = (await import('ora')).default;
const spinner = ora(message).start();
try {
const result = await action(spinner);
spinner.succeed();
return result;
} catch (error) {
spinner.fail(error.message || 'Operation failed');
throw error;
}
}
export default {
promptText,
promptPassword,
promptSelect,
promptCheckbox,
promptConfirm,
promptNumber,
promptEditor,
promptSequence,
withSpinner,
};
+522
View File
@@ -0,0 +1,522 @@
#!/bin/bash
# ==============================================================================
# Heretek OpenClaw - Bare Metal Installation Script
# ==============================================================================
# This script performs a complete automated installation of OpenClaw on
# bare metal Linux servers. It detects the OS and installs appropriate
# dependencies, then configures all services.
#
# Usage: curl -fsSL https://raw.githubusercontent.com/Heretek-AI/heretek-openclaw/main/scripts/install/bare-metal-install.sh | sudo bash
#
# Or download and run:
# curl -fsSL https://raw.githubusercontent.com/Heretek-AI/heretek-openclaw/main/scripts/install/bare-metal-install.sh -o bare-metal-install.sh
# chmod +x bare-metal-install.sh
# sudo ./bare-metal-install.sh
#
# Version: 1.0.0
# Last Updated: 2026-03-31
# ==============================================================================
set -e # Exit on error
# Script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# Configuration
OPENCLAW_DIR="/root/heretek/heretek-openclaw"
LITELLM_DIR="/opt/litellm"
CONFIG_DIR="/etc/openclaw"
LOG_DIR="/var/log/openclaw"
BACKUP_DIR="/var/backups/openclaw"
# Logging functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
log_step() {
echo -e "${CYAN}[STEP]${NC} $1"
}
# Check if running as root
check_root() {
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root (use sudo)"
exit 1
fi
}
# Detect OS and architecture
detect_os() {
log_step "Detecting operating system..."
if [[ -f /etc/os-release ]]; then
. /etc/os-release
OS=$ID
VERSION=$VERSION_ID
OS_FAMILY=$ID_LIKE
log_info "Detected: $OS $VERSION"
# Determine installer script
if [[ "$OS" == "ubuntu" || "$OS" == "debian" || "$OS_FAMILY" == *"debian"* ]]; then
INSTALLER_SCRIPT="ubuntu-deps.sh"
log_info "Using Ubuntu/Debian installer"
elif [[ "$OS" == "rhel" || "$OS" == "centos" || "$OS" == "rocky" || "$OS" == "almalinux" || "$OS_FAMILY" == *"rhel"* ]]; then
INSTALLER_SCRIPT="rhel-deps.sh"
log_info "Using RHEL installer"
else
log_error "Unsupported OS: $OS"
exit 1
fi
else
log_error "Cannot detect OS. /etc/os-release not found."
exit 1
fi
# Detect architecture
ARCH=$(uname -m)
log_info "Architecture: $ARCH"
# Detect GPU
detect_gpu
}
# Detect GPU
detect_gpu() {
log_step "Detecting GPU..."
GPU_TYPE="none"
# Check for AMD GPU
if lspci | grep -i "vga.*amd\|vga.*advanced micro devices" &>/dev/null; then
GPU_TYPE="amd"
log_info "Detected AMD GPU"
fi
# Check for NVIDIA GPU
if lspci | grep -i "vga.*nvidia\|3d.*nvidia" &>/dev/null; then
GPU_TYPE="nvidia"
log_info "Detected NVIDIA GPU"
fi
# Check for ROCm devices
if [[ -e /dev/kfd && -e /dev/dri ]]; then
GPU_TYPE="amd"
log_info "Detected AMD ROCm devices"
fi
# Check for NVIDIA devices
if [[ -e /dev/nvidia0 ]]; then
GPU_TYPE="nvidia"
log_info "Detected NVIDIA CUDA devices"
fi
log_info "GPU Type: $GPU_TYPE"
}
# Check system requirements
check_requirements() {
log_step "Checking system requirements..."
# Check CPU cores
CPU_CORES=$(nproc)
if [[ $CPU_CORES -lt 2 ]]; then
log_warning "Minimum 2 CPU cores recommended. Found: $CPU_CORES"
fi
log_info "CPU Cores: $CPU_CORES"
# Check RAM
RAM_GB=$(free -g | awk '/^Mem:/{print $2}')
if [[ $RAM_GB -lt 4 ]]; then
log_warning "Minimum 4GB RAM recommended. Found: ${RAM_GB}GB"
fi
log_info "RAM: ${RAM_GB}GB"
# Check disk space
DISK_GB=$(df -BG / | awk 'NR==2 {print $4}' | tr -d 'G')
if [[ $DISK_GB -lt 10 ]]; then
log_warning "Minimum 10GB free disk space recommended. Found: ${DISK_GB}GB"
fi
log_info "Free Disk Space: ${DISK_GB}GB"
}
# Install dependencies
install_dependencies() {
log_step "Installing dependencies..."
# Download and run the appropriate dependencies script
DEPS_URL="https://raw.githubusercontent.com/Heretek-AI/heretek-openclaw/main/scripts/install/$INSTALLER_SCRIPT"
log_info "Downloading $INSTALLER_SCRIPT..."
curl -fsSL "$DEPS_URL" -o "/tmp/$INSTALLER_SCRIPT"
chmod +x "/tmp/$INSTALLER_SCRIPT"
log_info "Running $INSTALLER_SCRIPT..."
"/tmp/$INSTALLER_SCRIPT"
log_success "Dependencies installed"
}
# Clone repository
clone_repository() {
log_step "Cloning OpenClaw repository..."
if [[ -d "$OPENCLAW_DIR" ]]; then
log_warning "Directory $OPENCLAW_DIR already exists"
log_info "Pulling latest changes..."
cd "$OPENCLAW_DIR"
git pull
else
log_info "Cloning repository to $OPENCLAW_DIR..."
git clone https://github.com/Heretek-AI/heretek-openclaw.git "$OPENCLAW_DIR"
fi
log_success "Repository cloned"
}
# Create directories
create_directories() {
log_step "Creating directories..."
mkdir -p "$CONFIG_DIR"
mkdir -p "$LOG_DIR"
mkdir -p "$BACKUP_DIR"
mkdir -p /etc/litellm
mkdir -p /var/log/litellm
# Set permissions
chmod 755 "$CONFIG_DIR"
chmod 755 "$LOG_DIR"
chmod 755 "$BACKUP_DIR"
chmod 755 /etc/litellm
chmod 755 /var/log/litellm
log_success "Directories created"
}
# Configure environment
configure_environment() {
log_step "Configuring environment..."
cd "$OPENCLAW_DIR"
# Copy environment template
if [[ -f ".env.bare-metal.example" ]]; then
cp ".env.bare-metal.example" "$CONFIG_DIR/.env"
log_info "Environment file created: $CONFIG_DIR/.env"
elif [[ -f ".env.example" ]]; then
cp ".env.example" "$CONFIG_DIR/.env"
log_info "Environment file created: $CONFIG_DIR/.env"
else
log_warning "No environment template found. Creating basic template..."
create_basic_env
fi
# Set permissions
chmod 600 "$CONFIG_DIR/.env"
log_success "Environment configured"
}
# Create basic environment file
create_basic_env() {
cat > "$CONFIG_DIR/.env" << 'EOF'
# Heretek OpenClaw - Environment Configuration
# Generated by bare-metal-install.sh
# LiteLLM Gateway
LITELLM_MASTER_KEY=change-me-openssl-rand-hex-32
LITELLM_SALT_KEY=change-me-openssl-rand-hex-32
LITELLM_PORT=4000
# Model Providers
MINIMAX_API_KEY=your-minimax-key-here
ZAI_API_KEY=your-zai-key-here
# Database
POSTGRES_USER=openclaw
POSTGRES_PASSWORD=change-me-secure-password
POSTGRES_DB=openclaw
DATABASE_URL=postgresql://openclaw:change-me-secure-password@localhost:5432/openclaw
# Redis
REDIS_URL=redis://localhost:6379/0
# Ollama
OLLAMA_HOST=http://localhost:11434
# OpenClaw
OPENCLAW_DIR=/root/.openclaw
OPENCLAW_WORKSPACE=/root/.openclaw/agents
EOF
}
# Configure PostgreSQL
configure_postgresql() {
log_step "Configuring PostgreSQL..."
# Generate secure password
DB_PASSWORD=$(openssl rand -hex 16)
# Create database and user
sudo -u postgres psql -c "CREATE DATABASE openclaw;" 2>/dev/null || true
sudo -u postgres psql -c "CREATE USER openclaw WITH PASSWORD '$DB_PASSWORD';" 2>/dev/null || true
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE openclaw TO openclaw;" 2>/dev/null || true
sudo -u postgres psql -d openclaw -c "CREATE EXTENSION IF NOT EXISTS vector;" 2>/dev/null || true
# Update environment file with password
sed -i "s/change-me-secure-password/$DB_PASSWORD/g" "$CONFIG_DIR/.env"
sed -i "s/POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=$DB_PASSWORD/" "$CONFIG_DIR/.env"
sed -i "s/DATABASE_URL=.*/DATABASE_URL=postgresql:\/\/openclaw:$DB_PASSWORD@localhost:5432\/openclaw/" "$CONFIG_DIR/.env"
log_success "PostgreSQL configured"
}
# Configure Redis
configure_redis() {
log_step "Configuring Redis..."
# Generate secure password
REDIS_PASSWORD=$(openssl rand -hex 16)
# Update Redis configuration
if [[ -f /etc/redis/redis.conf ]]; then
sed -i "s/# requirepass/requirepass $REDIS_PASSWORD/" /etc/redis/redis.conf 2>/dev/null || \
echo "requirepass $REDIS_PASSWORD" >> /etc/redis/redis.conf
# Restart Redis
systemctl restart redis
fi
# Update environment file
sed -i "s|REDIS_URL=.*|REDIS_URL=redis://:$REDIS_PASSWORD@localhost:6379/0|" "$CONFIG_DIR/.env"
log_success "Redis configured"
}
# Configure Ollama for GPU
configure_ollama() {
if [[ "$GPU_TYPE" == "amd" ]]; then
log_step "Configuring Ollama for AMD ROCm..."
# Create systemd override
mkdir -p /etc/systemd/system/ollama.service.d
cat > /etc/systemd/system/ollama.service.d/rocm.conf << EOF
[Service]
Environment="HSA_OVERRIDE_GFX_VERSION=10.3.0"
Environment="OLLAMA_HOST=0.0.0.0:11434"
DevicePolicy=closed
DeviceAllow=/dev/kfd rw
DeviceAllow=/dev/dri rw
EOF
systemctl daemon-reload
systemctl restart ollama
log_success "Ollama configured for AMD ROCm"
elif [[ "$GPU_TYPE" == "nvidia" ]]; then
log_step "Configuring Ollama for NVIDIA CUDA..."
# Create systemd override
mkdir -p /etc/systemd/system/ollama.service.d
cat > /etc/systemd/system/ollama.service.d/cuda.conf << EOF
[Service]
Environment="OLLAMA_HOST=0.0.0.0:11434"
Environment="PATH=/usr/bin:/usr/local/cuda/bin"
Environment="LD_LIBRARY_PATH=/usr/local/cuda/lib64"
DevicePolicy=closed
DeviceAllow=/dev/nvidia0 rw
DeviceAllow=/dev/nvidiactl rw
DeviceAllow=/dev/nvidia-uvm rw
EOF
systemctl daemon-reload
systemctl restart ollama
log_success "Ollama configured for NVIDIA CUDA"
else
log_info "No GPU detected. Ollama will run on CPU."
fi
# Pull embedding model
log_info "Pulling embedding model..."
ollama pull nomic-embed-text-v2-moe 2>/dev/null || log_warning "Failed to pull embedding model"
}
# Install systemd services
install_systemd_services() {
log_step "Installing systemd services..."
cd "$OPENCLAW_DIR"
# Copy service files
if [[ -d "systemd" ]]; then
cp systemd/openclaw-gateway.service /etc/systemd/system/ 2>/dev/null || true
cp systemd/litellm.service /etc/systemd/system/ 2>/dev/null || true
# Update service files with correct paths
sed -i "s|WorkingDirectory=.*|WorkingDirectory=$OPENCLAW_DIR|" /etc/systemd/system/openclaw-gateway.service 2>/dev/null || true
sed -i "s|EnvironmentFile=.*|EnvironmentFile=$CONFIG_DIR/.env|" /etc/systemd/system/openclaw-gateway.service 2>/dev/null || true
# Reload systemd
systemctl daemon-reload
# Enable services
systemctl enable openclaw-gateway 2>/dev/null || true
systemctl enable litellm 2>/dev/null || true
log_success "Systemd services installed"
else
log_warning "systemd directory not found. Skipping service installation."
fi
}
# Initialize database
initialize_database() {
log_step "Initializing database..."
cd "$OPENCLAW_DIR"
# Run migrations if available
if [[ -f "package.json" ]] && command -v npm &>/dev/null; then
npm run db:migrate 2>/dev/null || log_warning "Database migration failed"
fi
log_success "Database initialized"
}
# Configure LiteLLM
configure_litellm() {
log_step "Configuring LiteLLM..."
cd "$OPENCLAW_DIR"
# Copy LiteLLM configuration
if [[ -f "litellm_config.yaml" ]]; then
cp "litellm_config.yaml" /etc/litellm/litellm_config.yaml
chown litellm:litellm /etc/litellm/litellm_config.yaml 2>/dev/null || true
log_success "LiteLLM configured"
else
log_warning "litellm_config.yaml not found"
fi
}
# Print installation summary
print_summary() {
echo ""
echo "=============================================="
echo " Heretek OpenClaw - Installation Complete!"
echo "=============================================="
echo ""
log_success "OpenClaw has been installed successfully!"
echo ""
echo "Installation Details:"
echo " - OpenClaw Directory: $OPENCLAW_DIR"
echo " - Configuration Directory: $CONFIG_DIR"
echo " - Log Directory: $LOG_DIR"
echo " - Backup Directory: $BACKUP_DIR"
echo ""
echo "Next Steps:"
echo ""
echo " 1. Edit environment file with your API keys:"
echo " nano $CONFIG_DIR/.env"
echo ""
echo " 2. Start services:"
echo " sudo systemctl start postgresql"
echo " sudo systemctl start redis"
echo " sudo systemctl start ollama"
echo " sudo systemctl start litellm"
echo " sudo systemctl start openclaw-gateway"
echo ""
echo " 3. Verify installation:"
echo " cd $OPENCLAW_DIR"
echo " ./scripts/health-check.sh"
echo ""
echo " 4. Access LiteLLM Dashboard:"
echo " http://localhost:4000/ui"
echo ""
echo " 5. Check OpenClaw status:"
echo " openclaw gateway status"
echo ""
echo "Documentation:"
echo " - Bare Metal Deployment: $OPENCLAW_DIR/docs/deployment/BARE_METAL_DEPLOYMENT.md"
echo " - Troubleshooting: $OPENCLAW_DIR/docs/deployment/NON_DOCKER_TROUBLESHOOTING.md"
echo ""
echo "=============================================="
}
# Main installation function
main() {
echo ""
echo "=============================================="
echo " Heretek OpenClaw - Bare Metal Installer"
echo "=============================================="
echo ""
check_root
detect_os
check_requirements
install_dependencies
clone_repository
create_directories
configure_environment
configure_postgresql
configure_redis
configure_ollama
install_systemd_services
initialize_database
configure_litellm
print_summary
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--gpu)
GPU_TYPE="$2"
shift 2
;;
--no-gpu)
GPU_TYPE="none"
shift
;;
--help)
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " --gpu TYPE Specify GPU type (amd, nvidia, none)"
echo " --no-gpu Skip GPU configuration"
echo " --help Show this help message"
exit 0
;;
*)
log_error "Unknown option: $1"
exit 1
;;
esac
done
# Run main function
main "$@"
+708
View File
@@ -0,0 +1,708 @@
#!/bin/bash
# ==============================================================================
# Heretek OpenClaw - Post-Installation Configuration Script
# ==============================================================================
# This script performs post-installation configuration tasks for OpenClaw
# bare metal and VM deployments. It configures databases, services, and
# performs initial setup validation.
#
# Usage: sudo ./post-install.sh
#
# Version: 1.0.0
# Last Updated: 2026-03-31
# ==============================================================================
set -e # Exit on error
# Script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# Configuration
CONFIG_DIR="/etc/openclaw"
LOG_DIR="/var/log/openclaw"
BACKUP_DIR="/var/backups/openclaw"
# Logging functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
log_step() {
echo -e "${CYAN}[STEP]${NC} $1"
}
# Check if running as root
check_root() {
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root (use sudo)"
exit 1
fi
}
# Verify prerequisites
verify_prerequisites() {
log_step "Verifying prerequisites..."
local missing=()
# Check required commands
for cmd in psql redis-cli ollama openclaw; do
if ! command -v $cmd &>/dev/null; then
missing+=($cmd)
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
log_warning "Missing commands: ${missing[*]}"
log_warning "Please run the dependencies script first."
return 1
fi
log_success "Prerequisites verified"
return 0
}
# Configure PostgreSQL database
configure_postgresql() {
log_step "Configuring PostgreSQL database..."
# Check if database already exists
if sudo -u postgres psql -lqt | cut -d \| -f 1 | grep -qw openclaw; then
log_info "Database 'openclaw' already exists"
else
# Generate secure password
DB_PASSWORD=$(openssl rand -hex 16)
# Create database and user
sudo -u postgres psql -c "CREATE DATABASE openclaw;" 2>/dev/null || true
sudo -u postgres psql -c "CREATE USER openclaw WITH PASSWORD '$DB_PASSWORD';" 2>/dev/null || true
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE openclaw TO openclaw;" 2>/dev/null || true
sudo -u postgres psql -d openclaw -c "CREATE EXTENSION IF NOT EXISTS vector;" 2>/dev/null || true
log_info "Generated PostgreSQL password: $DB_PASSWORD"
# Update environment file if it exists
if [[ -f "$CONFIG_DIR/.env" ]]; then
sed -i "s/POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=$DB_PASSWORD/" "$CONFIG_DIR/.env"
sed -i "s|DATABASE_URL=.*|DATABASE_URL=postgresql://openclaw:$DB_PASSWORD@localhost:5432/openclaw|" "$CONFIG_DIR/.env"
log_success "Environment file updated with database credentials"
fi
fi
# Verify pgvector extension
if sudo -u postgres psql -d openclaw -c "SELECT * FROM pg_extension WHERE extname = 'vector';" | grep -q vector; then
log_success "pgvector extension enabled"
else
log_warning "pgvector extension may not be installed"
fi
log_success "PostgreSQL configured"
}
# Configure Redis
configure_redis() {
log_step "Configuring Redis..."
# Generate secure password
REDIS_PASSWORD=$(openssl rand -hex 16)
# Update Redis configuration
if [[ -f /etc/redis/redis.conf ]]; then
# Check if requirepass already set
if grep -q "^requirepass" /etc/redis/redis.conf; then
log_info "Redis password already configured"
else
echo "requirepass $REDIS_PASSWORD" >> /etc/redis/redis.conf
systemctl restart redis
log_info "Generated Redis password: $REDIS_PASSWORD"
fi
# Update environment file
if [[ -f "$CONFIG_DIR/.env" ]]; then
sed -i "s|REDIS_URL=.*|REDIS_URL=redis://:$REDIS_PASSWORD@localhost:6379/0|" "$CONFIG_DIR/.env"
log_success "Environment file updated with Redis credentials"
fi
else
log_warning "Redis configuration file not found"
fi
# Test Redis connection
if redis-cli -a "$REDIS_PASSWORD" ping 2>/dev/null | grep -q PONG; then
log_success "Redis connection verified"
else
log_warning "Redis connection test failed"
fi
log_success "Redis configured"
}
# Configure Ollama
configure_ollama() {
log_step "Configuring Ollama..."
# Check if Ollama is running
if systemctl is-active --quiet ollama; then
log_success "Ollama service is running"
else
log_info "Starting Ollama service..."
systemctl start ollama
systemctl enable ollama
fi
# Pull required embedding model
log_info "Pulling embedding model..."
if ollama list | grep -q "nomic-embed-text"; then
log_info "Embedding model already exists"
else
ollama pull nomic-embed-text-v2-moe
log_success "Embedding model pulled"
fi
# Test Ollama connection
if curl -s http://localhost:11434/api/tags | grep -q "models"; then
log_success "Ollama connection verified"
else
log_warning "Ollama connection test failed"
fi
log_success "Ollama configured"
}
# Configure LiteLLM
configure_litellm() {
log_step "Configuring LiteLLM..."
# Create directories if needed
mkdir -p /etc/litellm
mkdir -p /var/log/litellm
# Copy configuration if it exists in project
if [[ -f "$PROJECT_DIR/litellm_config.yaml" ]]; then
cp "$PROJECT_DIR/litellm_config.yaml" /etc/litellm/litellm_config.yaml
# Set ownership
if id -u litellm &>/dev/null; then
chown litellm:litellm /etc/litellm/litellm_config.yaml
fi
log_success "LiteLLM configuration copied"
else
log_warning "litellm_config.yaml not found in project"
fi
# Create systemd service if not exists
if [[ ! -f /etc/systemd/system/litellm.service ]]; then
log_info "Creating LiteLLM systemd service..."
create_litellm_service
fi
log_success "LiteLLM configured"
}
# Create LiteLLM systemd service
create_litellm_service() {
cat > /etc/systemd/system/litellm.service << 'EOF'
[Unit]
Description=LiteLLM Proxy Service
Documentation=https://docs.litellm.ai
After=network.target postgresql.service redis.service
Wants=postgresql.service redis.service
[Service]
Type=simple
User=litellm
Group=litellm
# Environment configuration
Environment="LITELLM_CONFIG_PATH=/etc/litellm/litellm_config.yaml"
Environment="DATABASE_URL=postgresql://openclaw:password@localhost:5432/openclaw"
Environment="REDIS_URL=redis://localhost:6379/0"
EnvironmentFile=-/etc/openclaw/.env
# Working directory
WorkingDirectory=/opt/litellm
# Main execution
ExecStart=/opt/litellm/venv/bin/litellm --config /etc/litellm/litellm_config.yaml --port 4000 --num_workers 4
# Restart configuration
Restart=on-failure
RestartSec=5
# Resource limits
LimitNOFILE=65535
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=read-only
PrivateTmp=true
# Allow write access to log directory
ReadWritePaths=/var/log/litellm
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=litellm
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
}
# Configure OpenClaw Gateway
configure_openclaw() {
log_step "Configuring OpenClaw Gateway..."
# Check if OpenClaw is installed
if ! command -v openclaw &>/dev/null; then
log_error "OpenClaw Gateway not installed"
return 1
fi
# Create workspace directory
mkdir -p ~/.openclaw
mkdir -p ~/.openclaw/agents
mkdir -p ~/.openclaw/logs
# Copy configuration if it exists in project
if [[ -f "$PROJECT_DIR/openclaw.json" ]]; then
cp "$PROJECT_DIR/openclaw.json" ~/.openclaw/openclaw.json
log_success "OpenClaw configuration copied"
fi
# Validate configuration
if openclaw gateway validate 2>/dev/null; then
log_success "OpenClaw configuration validated"
else
log_warning "OpenClaw configuration validation failed"
fi
# Create systemd service if not exists
if [[ ! -f /etc/systemd/system/openclaw-gateway.service ]]; then
log_info "Creating OpenClaw Gateway systemd service..."
create_openclaw_service
fi
log_success "OpenClaw Gateway configured"
}
# Create OpenClaw Gateway systemd service
create_openclaw_service() {
cat > /etc/systemd/system/openclaw-gateway.service << 'EOF'
[Unit]
Description=OpenClaw Gateway Service
Documentation=https://github.com/Heretek-AI/heretek-openclaw
After=network.target litellm.service postgresql.service redis.service
Wants=litellm.service postgresql.service redis.service
[Service]
Type=simple
User=root
Group=root
# Environment configuration
Environment="OPENCLAW_DIR=/root/.openclaw"
Environment="OPENCLAW_WORKSPACE=/root/.openclaw/agents"
Environment="NODE_ENV=production"
EnvironmentFile=-/etc/openclaw/.env
# Working directory
WorkingDirectory=/root/heretek/heretek-openclaw
# Main execution
ExecStart=/usr/bin/node /root/heretek/heretek-openclaw/dist/gateway/index.js
Restart=on-failure
RestartSec=10
# Resource limits
LimitNOFILE=65535
LimitNPROC=4096
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=read-only
PrivateTmp=true
# Allow write access to OpenClaw directories
ReadWritePaths=/root/.openclaw
ReadWritePaths=/var/log/openclaw
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=openclaw-gateway
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
}
# Enable and start services
enable_services() {
log_step "Enabling and starting services..."
# List of services to enable
local services=("postgresql" "redis" "ollama" "litellm" "openclaw-gateway")
for service in "${services[@]}"; do
# Check if service file exists
if [[ -f /etc/systemd/system/${service}.service ]] || systemctl list-unit-files | grep -q "^${service}"; then
log_info "Enabling $service..."
systemctl enable "$service" 2>/dev/null || log_warning "Failed to enable $service"
log_info "Starting $service..."
systemctl start "$service" 2>/dev/null || log_warning "Failed to start $service"
# Wait for service to be ready
sleep 2
if systemctl is-active --quiet "$service"; then
log_success "$service is running"
else
log_warning "$service is not running"
fi
else
log_warning "Service $service not found"
fi
done
log_success "Services enabled and started"
}
# Run database migrations
run_migrations() {
log_step "Running database migrations..."
cd "$PROJECT_DIR"
# Check if package.json exists
if [[ -f "package.json" ]]; then
# Check if npm is available
if command -v npm &>/dev/null; then
# Run migrations
if npm run db:migrate 2>/dev/null; then
log_success "Database migrations completed"
else
log_warning "Database migrations failed or not available"
fi
else
log_warning "npm not found. Skipping migrations."
fi
else
log_warning "package.json not found. Skipping migrations."
fi
log_success "Database migrations complete"
}
# Create agent workspaces
create_agent_workspaces() {
log_step "Creating agent workspaces..."
# List of agents to create
local agents=(
"steward:orchestrator"
"alpha:triad"
"beta:triad"
"charlie:triad"
"examiner:interrogator"
"explorer:scout"
"sentinel:guardian"
"coder:artisan"
"dreamer:visionary"
"empath:diplomat"
"historian:archivist"
)
# Check if deploy-agent.sh exists
if [[ -f "$PROJECT_DIR/agents/deploy-agent.sh" ]]; then
for agent_info in "${agents[@]}"; do
agent=$(echo "$agent_info" | cut -d: -f1)
role=$(echo "$agent_info" | cut -d: -f2)
if [[ -d "~/.openclaw/agents/$agent" ]]; then
log_info "Agent workspace already exists: $agent"
else
log_info "Creating agent workspace: $agent ($role)..."
cd "$PROJECT_DIR"
./agents/deploy-agent.sh "$agent" "$role" 2>/dev/null || log_warning "Failed to create agent: $agent"
fi
done
log_success "Agent workspaces created"
else
log_warning "deploy-agent.sh not found. Skipping agent workspace creation."
fi
}
# Configure log rotation
configure_log_rotation() {
log_step "Configuring log rotation..."
cat > /etc/logrotate.d/openclaw << 'EOF'
/var/log/openclaw/*.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
create 0640 root root
sharedscripts
postrotate
systemctl reload openclaw-gateway 2>/dev/null || true
endscript
}
/var/log/litellm/*.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
create 0640 litellm litellm
sharedscripts
postrotate
systemctl reload litellm 2>/dev/null || true
endscript
}
EOF
log_success "Log rotation configured"
}
# Configure backup
configure_backup() {
log_step "Configuring automated backup..."
# Create backup script
cat > /usr/local/bin/openclaw-backup.sh << 'EOF'
#!/bin/bash
BACKUP_DIR="/var/backups/openclaw"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=7
mkdir -p $BACKUP_DIR
# Backup OpenClaw configuration
tar -czf $BACKUP_DIR/openclaw-config-$DATE.tar.gz \
~/.openclaw/ \
/etc/litellm/ \
/etc/openclaw/ 2>/dev/null
# Backup PostgreSQL
pg_dump -U openclaw openclaw 2>/dev/null | gzip > $BACKUP_DIR/openclaw-db-$DATE.sql.gz
# Backup Redis
redis-cli BGSAVE 2>/dev/null
sleep 2
cp /var/lib/redis/dump.rdb $BACKUP_DIR/redis-dump-$DATE.rdb 2>/dev/null
# Remove old backups
find $BACKUP_DIR -name "*.tar.gz" -mtime +$RETENTION_DAYS -delete 2>/dev/null
find $BACKUP_DIR -name "*.sql.gz" -mtime +$RETENTION_DAYS -delete 2>/dev/null
find $BACKUP_DIR -name "*.rdb" -mtime +$RETENTION_DAYS -delete 2>/dev/null
echo "Backup completed: $DATE" >> /var/log/openclaw/backup.log
EOF
chmod +x /usr/local/bin/openclaw-backup.sh
# Create systemd timer
if [[ ! -f /etc/systemd/system/openclaw-backup.timer ]]; then
cat > /etc/systemd/system/openclaw-backup.timer << 'EOF'
[Unit]
Description=Daily OpenClaw Backup
Documentation=file:///root/heretek/heretek-openclaw/docs/operations/AUTOMATED_BACKUP.md
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
EOF
cat > /etc/systemd/system/openclaw-backup.service << 'EOF'
[Unit]
Description=OpenClaw Backup Service
After=postgresql.service redis.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/openclaw-backup.sh
User=root
Group=root
EOF
systemctl daemon-reload
systemctl enable openclaw-backup.timer
systemctl start openclaw-backup.timer
fi
log_success "Automated backup configured"
}
# Run health checks
run_health_checks() {
log_step "Running health checks..."
local checks_passed=0
local checks_failed=0
# Check PostgreSQL
if psql -U openclaw -d openclaw -c "SELECT 1;" &>/dev/null; then
log_success "PostgreSQL: OK"
((checks_passed++))
else
log_error "PostgreSQL: FAILED"
((checks_failed++))
fi
# Check Redis
if redis-cli ping &>/dev/null; then
log_success "Redis: OK"
((checks_passed++))
else
log_error "Redis: FAILED"
((checks_failed++))
fi
# Check Ollama
if curl -s http://localhost:11434/api/tags &>/dev/null; then
log_success "Ollama: OK"
((checks_passed++))
else
log_error "Ollama: FAILED"
((checks_failed++))
fi
# Check LiteLLM
if curl -s http://localhost:4000/health &>/dev/null; then
log_success "LiteLLM: OK"
((checks_passed++))
else
log_warning "LiteLLM: Not running (may need manual start)"
fi
# Check OpenClaw Gateway
if openclaw gateway status &>/dev/null; then
log_success "OpenClaw Gateway: OK"
((checks_passed++))
else
log_warning "OpenClaw Gateway: Not running (may need manual start)"
fi
echo ""
log_info "Health Check Summary: $checks_passed passed, $checks_failed failed"
if [[ $checks_failed -gt 0 ]]; then
return 1
fi
return 0
}
# Print installation summary
print_summary() {
echo ""
echo "=============================================="
echo " Heretek OpenClaw - Post-Installation Complete"
echo "=============================================="
echo ""
log_success "Post-installation configuration completed!"
echo ""
echo "Configuration Summary:"
echo " - PostgreSQL: Configured with pgvector"
echo " - Redis: Configured with password authentication"
echo " - Ollama: Running with embedding model"
echo " - LiteLLM: Configured"
echo " - OpenClaw Gateway: Configured"
echo " - Log Rotation: Configured"
echo " - Automated Backup: Configured"
echo ""
echo "Next Steps:"
echo ""
echo " 1. Edit environment file with your API keys:"
echo " nano $CONFIG_DIR/.env"
echo ""
echo " 2. Start all services:"
echo " sudo systemctl start postgresql redis ollama litellm openclaw-gateway"
echo ""
echo " 3. Verify installation:"
echo " cd $PROJECT_DIR"
echo " ./scripts/health-check.sh"
echo ""
echo " 4. Create agent workspaces (if not done):"
echo " ./agents/deploy-agent.sh steward orchestrator"
echo " (repeat for all agents)"
echo ""
echo "Documentation:"
echo " - BARE_METAL_DEPLOYMENT.md: $PROJECT_DIR/docs/deployment/BARE_METAL_DEPLOYMENT.md"
echo " - TROUBLESHOOTING: $PROJECT_DIR/docs/deployment/NON_DOCKER_TROUBLESHOOTING.md"
echo ""
echo "=============================================="
}
# Main function
main() {
echo ""
echo "=============================================="
echo " Heretek OpenClaw - Post-Installation"
echo "=============================================="
echo ""
check_root
if verify_prerequisites; then
configure_postgresql
configure_redis
configure_ollama
configure_litellm
configure_openclaw
enable_services
run_migrations
create_agent_workspaces
configure_log_rotation
configure_backup
echo ""
run_health_checks
print_summary
else
log_error "Prerequisites not met. Please install dependencies first."
exit 1
fi
}
# Run main function
main "$@"
+435
View File
@@ -0,0 +1,435 @@
#!/bin/bash
# ==============================================================================
# Heretek OpenClaw - RHEL/CentOS Dependencies Installer
# ==============================================================================
# This script installs all required dependencies for OpenClaw bare metal
# deployment on RHEL 9+, CentOS Stream 9, Rocky Linux 9, and AlmaLinux 9.
#
# Usage: sudo ./rhel-deps.sh
#
# Version: 1.0.0
# Last Updated: 2026-03-31
# ==============================================================================
set -e # Exit on error
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Logging functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Check if running as root
check_root() {
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root (use sudo)"
exit 1
fi
}
# Check OS version
check_os() {
if [[ -f /etc/os-release ]]; then
. /etc/os-release
OS=$ID
VERSION=$VERSION_ID
log_info "Detected OS: $OS $VERSION"
# Check for supported versions
if [[ "$OS" == "rhel" ]]; then
if (( $(echo "$VERSION < 9" | bc -l) )); then
log_error "RHEL 9 or higher is required. Found: $VERSION"
exit 1
fi
elif [[ "$OS" == "centos" ]]; then
if [[ "$VERSION_ID" != "9"* ]]; then
log_error "CentOS Stream 9 or higher is required. Found: $VERSION"
exit 1
fi
elif [[ "$OS" == "rocky" ]]; then
if (( $(echo "$VERSION < 9" | bc -l) )); then
log_error "Rocky Linux 9 or higher is required. Found: $VERSION"
exit 1
fi
elif [[ "$OS" == "almalinux" ]]; then
if (( $(echo "$VERSION < 9" | bc -l) )); then
log_error "AlmaLinux 9 or higher is required. Found: $VERSION"
exit 1
fi
else
log_warning "Unsupported OS: $OS. Script may not work correctly."
fi
else
log_error "Cannot detect OS. /etc/os-release not found."
exit 1
fi
}
# Update system packages
update_system() {
log_info "Updating system packages..."
dnf update -y -q
log_success "System packages updated"
}
# Install EPEL repository
install_epel() {
log_info "Installing EPEL repository..."
if ! rpm -q epel-release &>/dev/null; then
dnf install -y -q epel-release
log_success "EPEL repository installed"
else
log_info "EPEL repository already installed"
fi
}
# Install core dependencies
install_core_deps() {
log_info "Installing core dependencies..."
dnf install -y -q \
curl \
git \
wget \
gnupg2 \
ca-certificates \
gcc \
gcc-c++ \
make \
openssl-devel \
libffi-devel \
bzip2-devel \
readline-devel \
sqlite-devel \
ncurses-devel \
xz-devel \
tk-devel \
libxml2-devel \
libxmlsec1-devel \
zlib-devel \
jq \
net-tools \
lsof \
htop \
vim \
unzip \
pkg-config \
policycoreutils-python-utils
log_success "Core dependencies installed"
}
# Install Python 3.10+
install_python() {
log_info "Installing Python 3.10+..."
# Check if Python 3.10+ is already installed
if command -v python3 &> /dev/null; then
PYTHON_VERSION=$(python3 --version | awk '{print $2}')
log_info "Python $PYTHON_VERSION already installed"
# Check if version is 3.10+
if (( $(echo "$PYTHON_VERSION < 3.10" | bc -l) )); then
log_warning "Python 3.10+ required. Installing..."
dnf install -y -q python3 python3-pip python3-devel
fi
else
dnf install -y -q python3 python3-pip python3-devel
fi
# Upgrade pip
python3 -m pip install --upgrade pip --quiet
log_success "Python installed: $(python3 --version)"
}
# Install Node.js 20 LTS
install_nodejs() {
log_info "Installing Node.js 20 LTS..."
# Check if Node.js is already installed
if command -v node &> /dev/null; then
NODE_VERSION=$(node --version | cut -d'v' -f2 | cut -d'.' -f1)
log_info "Node.js $(node --version) already installed"
if [[ $NODE_VERSION -lt 20 ]]; then
log_warning "Node.js 20+ required. Installing..."
else
log_success "Node.js version is adequate"
return 0
fi
fi
# Install Node.js 20 LTS
curl -fsSL https://rpm.nodesource.com/setup_20.x | bash -
dnf install -y -q nodejs
log_success "Node.js installed: $(node --version)"
log_success "npm installed: $(npm --version)"
}
# Install PostgreSQL 15 with pgvector
install_postgresql() {
log_info "Installing PostgreSQL 15 with pgvector..."
# Check if PostgreSQL is already installed
if command -v psql &> /dev/null; then
PG_VERSION=$(psql --version | awk '{print $3}' | cut -d'.' -f1)
log_info "PostgreSQL $PG_VERSION already installed"
if [[ $PG_VERSION -ge 15 ]]; then
log_success "PostgreSQL version is adequate"
return 0
fi
fi
# Add PostgreSQL repository
dnf install -y -q https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm
# Disable default PostgreSQL module
dnf -qy module disable postgresql
# Install PostgreSQL 15
dnf install -y -q postgresql15 postgresql15-contrib postgresql15-pgvector
# Start PostgreSQL
systemctl start postgresql-15
systemctl enable postgresql-15
log_success "PostgreSQL 15 installed with pgvector"
}
# Install Redis 7
install_redis() {
log_info "Installing Redis 7..."
# Check if Redis is already installed
if command -v redis-cli &> /dev/null; then
REDIS_VERSION=$(redis-cli --version | awk '{print $2}')
log_info "Redis $REDIS_VERSION already installed"
if (( $(echo "$REDIS_VERSION >= 7" | bc -l) )); then
log_success "Redis version is adequate"
return 0
fi
fi
# Install Redis from Remi repository
dnf install -y -q dnf-utils
dnf config-manager --set-enabled crb # CodeReady Builder
dnf install -y -q https://rpms.remirepo.net/enterprise/remi-release-9.rpm
dnf module reset redis -y -q
dnf module enable redis:7 -y -q
dnf install -y -q redis
# Start Redis
systemctl start redis
systemctl enable redis
log_success "Redis 7 installed"
}
# Install Ollama
install_ollama() {
log_info "Installing Ollama..."
# Check if Ollama is already installed
if command -v ollama &> /dev/null; then
log_info "Ollama already installed: $(ollama --version 2>/dev/null || echo 'unknown')"
log_success "Ollama installation verified"
return 0
fi
# Install Ollama
curl -fsSL https://ollama.ai/install.sh | sh
# Start Ollama
systemctl start ollama
systemctl enable ollama
log_success "Ollama installed"
}
# Install LiteLLM dependencies
install_litellm_deps() {
log_info "Installing LiteLLM dependencies..."
# Create litellm user if not exists
if ! id -u litellm &>/dev/null; then
useradd -r -s /bin/false litellm
log_info "Created litellm user"
fi
# Create directories
mkdir -p /opt/litellm
mkdir -p /etc/litellm
mkdir -p /var/log/litellm
chown litellm:litellm /opt/litellm
chown litellm:litellm /var/log/litellm
# Create virtual environment
if [[ ! -f /opt/litellm/venv/bin/python ]]; then
python3 -m venv /opt/litellm/venv
log_info "Created LiteLLM virtual environment"
fi
# Install LiteLLM
/opt/litellm/venv/bin/pip install --upgrade pip --quiet
/opt/litellm/venv/bin/pip install \
'litellm[proxy]' \
'litellm[langfuse]' \
'litellm[postgres]' \
'litellm[redis]' \
psycopg2-binary \
redis \
langfuse --quiet
log_success "LiteLLM installed"
}
# Install OpenClaw Gateway
install_openclaw() {
log_info "Installing OpenClaw Gateway..."
# Check if OpenClaw is already installed
if command -v openclaw &> /dev/null; then
log_info "OpenClaw already installed: $(openclaw --version)"
log_success "OpenClaw installation verified"
return 0
fi
# Install OpenClaw
curl -fsSL https://openclaw.ai/install.sh | sh
log_success "OpenClaw Gateway installed"
}
# Configure firewall (firewalld)
configure_firewall() {
log_info "Configuring firewall..."
if command -v firewall-cmd &> /dev/null; then
# Check if firewalld is active
if ! systemctl is-active --quiet firewalld; then
log_warning "firewalld is not active. Skipping firewall configuration."
return 0
fi
# Allow required ports
firewall-cmd --permanent --add-service=ssh
firewall-cmd --permanent --add-port=4000/tcp # LiteLLM
firewall-cmd --permanent --add-port=18789/tcp # OpenClaw Gateway
firewall-cmd --permanent --add-port=3000/tcp # Dashboard (optional)
# Allow localhost for internal services
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="127.0.0.1" port port="5432" protocol="tcp" accept'
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="127.0.0.1" port port="6379" protocol="tcp" accept'
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="127.0.0.1" port port="11434" protocol="tcp" accept'
# Reload firewall
firewall-cmd --reload
log_success "Firewall configured"
else
log_warning "firewalld not installed. Skipping firewall configuration."
fi
}
# Configure SELinux (optional)
configure_selinux() {
log_info "Checking SELinux configuration..."
if command -v getenforce &> /dev/null; then
SELINUX_STATUS=$(getenforce)
log_info "SELinux status: $SELINUX_STATUS"
if [[ "$SELINUX_STATUS" == "Enforcing" ]]; then
log_warning "SELinux is in Enforcing mode. You may need to create custom policies for OpenClaw."
log_info "See docs/deployment/VM_DEPLOYMENT.md for SELinux configuration."
fi
fi
}
# Print installation summary
print_summary() {
echo ""
echo "=============================================="
echo " Heretek OpenClaw - Installation Summary"
echo "=============================================="
echo ""
log_success "All dependencies installed successfully!"
echo ""
echo "Installed components:"
echo " - Python: $(python3 --version)"
echo " - Node.js: $(node --version)"
echo " - npm: $(npm --version)"
echo " - PostgreSQL: $(psql --version)"
echo " - Redis: $(redis-cli --version)"
echo " - Ollama: $(ollama --version 2>/dev/null || echo 'installed')"
echo " - OpenClaw: $(openclaw --version 2>/dev/null || echo 'installed')"
echo ""
echo "Next steps:"
echo " 1. Clone the OpenClaw repository:"
echo " git clone https://github.com/Heretek-AI/heretek-openclaw.git"
echo " cd heretek-openclaw"
echo ""
echo " 2. Copy environment template:"
echo " cp .env.vm.example .env"
echo " nano .env # Edit with your values"
echo ""
echo " 3. Run post-installation script:"
echo " sudo ./scripts/install/post-install.sh"
echo ""
echo " 4. Verify installation:"
echo " ./scripts/health-check.sh"
echo ""
echo "=============================================="
}
# Main installation function
main() {
echo ""
echo "=============================================="
echo " Heretek OpenClaw - RHEL Dependencies"
echo "=============================================="
echo ""
check_root
check_os
update_system
install_epel
install_core_deps
install_python
install_nodejs
install_postgresql
install_redis
install_ollama
install_litellm_deps
install_openclaw
configure_firewall
configure_selinux
print_summary
}
# Run main function
main "$@"
+392
View File
@@ -0,0 +1,392 @@
#!/bin/bash
# ==============================================================================
# Heretek OpenClaw - Ubuntu/Debian Dependencies Installer
# ==============================================================================
# This script installs all required dependencies for OpenClaw bare metal
# deployment on Ubuntu 20.04+ and Debian 11+ systems.
#
# Usage: sudo ./ubuntu-deps.sh
#
# Version: 1.0.0
# Last Updated: 2026-03-31
# ==============================================================================
set -e # Exit on error
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Logging functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Check if running as root
check_root() {
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root (use sudo)"
exit 1
fi
}
# Check OS version
check_os() {
if [[ -f /etc/os-release ]]; then
. /etc/os-release
OS=$ID
VERSION=$VERSION_ID
log_info "Detected OS: $OS $VERSION"
# Check for supported versions
if [[ "$OS" == "ubuntu" ]]; then
if (( $(echo "$VERSION < 20.04" | bc -l) )); then
log_error "Ubuntu 20.04 or higher is required. Found: $VERSION"
exit 1
fi
elif [[ "$OS" == "debian" ]]; then
if (( $(echo "$VERSION < 11" | bc -l) )); then
log_error "Debian 11 or higher is required. Found: $VERSION"
exit 1
fi
else
log_warning "Unsupported OS: $OS. Script may not work correctly."
fi
else
log_error "Cannot detect OS. /etc/os-release not found."
exit 1
fi
}
# Update system packages
update_system() {
log_info "Updating system packages..."
apt-get update -qq
apt-get upgrade -y -qq
log_success "System packages updated"
}
# Install core dependencies
install_core_deps() {
log_info "Installing core dependencies..."
apt-get install -y -qq \
curl \
git \
wget \
gnupg \
ca-certificates \
software-properties-common \
build-essential \
libssl-dev \
libffi-dev \
zlib1g-dev \
libbz2-dev \
libreadline-dev \
libsqlite3-dev \
libncursesw5-dev \
xz-utils \
tk-dev \
libxml2-dev \
libxmlsec1-dev \
liblzma-dev \
jq \
net-tools \
lsof \
htop \
vim \
unzip \
pkg-config
log_success "Core dependencies installed"
}
# Install Python 3.10+
install_python() {
log_info "Installing Python 3.10+..."
# Check if Python 3.10+ is already installed
if command -v python3 &> /dev/null; then
PYTHON_VERSION=$(python3 --version | awk '{print $2}')
log_info "Python $PYTHON_VERSION already installed"
# Check if version is 3.10+
if (( $(echo "$PYTHON_VERSION < 3.10" | bc -l) )); then
log_warning "Python 3.10+ required. Installing..."
apt-get install -y -qq python3 python3-pip python3-venv python3-dev
fi
else
apt-get install -y -qq python3 python3-pip python3-venv python3-dev
fi
# Upgrade pip
python3 -m pip install --upgrade pip --quiet
log_success "Python installed: $(python3 --version)"
}
# Install Node.js 20 LTS
install_nodejs() {
log_info "Installing Node.js 20 LTS..."
# Check if Node.js is already installed
if command -v node &> /dev/null; then
NODE_VERSION=$(node --version | cut -d'v' -f2 | cut -d'.' -f1)
log_info "Node.js $(node --version) already installed"
if [[ $NODE_VERSION -lt 20 ]]; then
log_warning "Node.js 20+ required. Installing..."
else
log_success "Node.js version is adequate"
return 0
fi
fi
# Install Node.js 20 LTS
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt-get install -y -qq nodejs
log_success "Node.js installed: $(node --version)"
log_success "npm installed: $(npm --version)"
}
# Install PostgreSQL 15 with pgvector
install_postgresql() {
log_info "Installing PostgreSQL 15 with pgvector..."
# Check if PostgreSQL is already installed
if command -v psql &> /dev/null; then
PG_VERSION=$(psql --version | awk '{print $3}' | cut -d'.' -f1)
log_info "PostgreSQL $PG_VERSION already installed"
if [[ $PG_VERSION -ge 15 ]]; then
log_success "PostgreSQL version is adequate"
return 0
fi
fi
# Add PostgreSQL repository
sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt '$(lsb_release -cs)'-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
# Update and install
apt-get update -qq
apt-get install -y -qq postgresql-15 postgresql-contrib-15 postgresql-15-pgvector
# Start PostgreSQL
systemctl start postgresql
systemctl enable postgresql
log_success "PostgreSQL 15 installed with pgvector"
}
# Install Redis 7
install_redis() {
log_info "Installing Redis 7..."
# Check if Redis is already installed
if command -v redis-cli &> /dev/null; then
REDIS_VERSION=$(redis-cli --version | awk '{print $2}')
log_info "Redis $REDIS_VERSION already installed"
if (( $(echo "$REDIS_VERSION >= 7" | bc -l) )); then
log_success "Redis version is adequate"
return 0
fi
fi
# Add Redis repository
curl -fsSL https://packages.redis.io/gpg | gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/redis.list
# Update and install
apt-get update -qq
apt-get install -y -qq redis
# Start Redis
systemctl start redis
systemctl enable redis
log_success "Redis 7 installed"
}
# Install Ollama
install_ollama() {
log_info "Installing Ollama..."
# Check if Ollama is already installed
if command -v ollama &> /dev/null; then
log_info "Ollama already installed: $(ollama --version 2>/dev/null || echo 'unknown')"
log_success "Ollama installation verified"
return 0
fi
# Install Ollama
curl -fsSL https://ollama.ai/install.sh | sh
# Start Ollama
systemctl start ollama
systemctl enable ollama
log_success "Ollama installed"
}
# Install LiteLLM dependencies
install_litellm_deps() {
log_info "Installing LiteLLM dependencies..."
# Create litellm user if not exists
if ! id -u litellm &>/dev/null; then
useradd -r -s /bin/false litellm
log_info "Created litellm user"
fi
# Create directories
mkdir -p /opt/litellm
mkdir -p /etc/litellm
mkdir -p /var/log/litellm
chown litellm:litellm /opt/litellm
chown litellm:litellm /var/log/litellm
# Create virtual environment
if [[ ! -f /opt/litellm/venv/bin/python ]]; then
python3 -m venv /opt/litellm/venv
log_info "Created LiteLLM virtual environment"
fi
# Install LiteLLM
/opt/litellm/venv/bin/pip install --upgrade pip --quiet
/opt/litellm/venv/bin/pip install \
'litellm[proxy]' \
'litellm[langfuse]' \
'litellm[postgres]' \
'litellm[redis]' \
psycopg2-binary \
redis \
langfuse --quiet
log_success "LiteLLM installed"
}
# Install OpenClaw Gateway
install_openclaw() {
log_info "Installing OpenClaw Gateway..."
# Check if OpenClaw is already installed
if command -v openclaw &> /dev/null; then
log_info "OpenClaw already installed: $(openclaw --version)"
log_success "OpenClaw installation verified"
return 0
fi
# Install OpenClaw
curl -fsSL https://openclaw.ai/install.sh | sh
log_success "OpenClaw Gateway installed"
}
# Configure firewall (UFW)
configure_firewall() {
log_info "Configuring firewall..."
if command -v ufw &> /dev/null; then
# Check if UFW is active
if ! ufw status | grep -q "Status: active"; then
log_warning "UFW is not active. Skipping firewall configuration."
return 0
fi
# Allow required ports
ufw allow ssh
ufw allow 4000/tcp # LiteLLM
ufw allow 18789/tcp # OpenClaw Gateway
ufw allow 3000/tcp # Dashboard (optional)
# Allow localhost for internal services
ufw allow from 127.0.0.1 to any port 5432 # PostgreSQL
ufw allow from 127.0.0.1 to any port 6379 # Redis
ufw allow from 127.0.0.1 to any port 11434 # Ollama
log_success "Firewall configured"
else
log_warning "UFW not installed. Skipping firewall configuration."
fi
}
# Print installation summary
print_summary() {
echo ""
echo "=============================================="
echo " Heretek OpenClaw - Installation Summary"
echo "=============================================="
echo ""
log_success "All dependencies installed successfully!"
echo ""
echo "Installed components:"
echo " - Python: $(python3 --version)"
echo " - Node.js: $(node --version)"
echo " - npm: $(npm --version)"
echo " - PostgreSQL: $(psql --version)"
echo " - Redis: $(redis-cli --version)"
echo " - Ollama: $(ollama --version 2>/dev/null || echo 'installed')"
echo " - OpenClaw: $(openclaw --version 2>/dev/null || echo 'installed')"
echo ""
echo "Next steps:"
echo " 1. Clone the OpenClaw repository:"
echo " git clone https://github.com/Heretek-AI/heretek-openclaw.git"
echo " cd heretek-openclaw"
echo ""
echo " 2. Copy environment template:"
echo " cp .env.bare-metal.example .env"
echo " nano .env # Edit with your values"
echo ""
echo " 3. Run post-installation script:"
echo " sudo ./scripts/install/post-install.sh"
echo ""
echo " 4. Verify installation:"
echo " ./scripts/health-check.sh"
echo ""
echo "=============================================="
}
# Main installation function
main() {
echo ""
echo "=============================================="
echo " Heretek OpenClaw - Ubuntu Dependencies"
echo "=============================================="
echo ""
check_root
check_os
update_system
install_core_deps
install_python
install_nodejs
install_postgresql
install_redis
install_ollama
install_litellm_deps
install_openclaw
configure_firewall
print_summary
}
# Run main function
main "$@"
+664
View File
@@ -0,0 +1,664 @@
#!/bin/bash
# ==============================================================================
# Heretek OpenClaw - VM Installation Script
# ==============================================================================
# This script performs a complete automated installation of OpenClaw on
# virtual machines (VMs) across different cloud platforms. It detects
# the OS and cloud platform, then installs appropriate dependencies.
#
# Usage: curl -fsSL https://raw.githubusercontent.com/Heretek-AI/heretek-openclaw/main/scripts/install/vm-install.sh | sudo bash
#
# Or download and run:
# curl -fsSL https://raw.githubusercontent.com/Heretek-AI/heretek-openclaw/main/scripts/install/vm-install.sh -o vm-install.sh
# chmod +x vm-install.sh
# sudo ./vm-install.sh [--os OS_TYPE] [--gpu GPU_TYPE] [--cloud CLOUD_PROVIDER]
#
# Options:
# --os OS_TYPE Specify OS type (ubuntu, rhel, auto)
# --gpu GPU_TYPE Specify GPU type (amd, nvidia, none, auto)
# --cloud PROVIDER Specify cloud provider (aws, gcp, azure, auto)
# --help Show this help message
#
# Version: 1.0.0
# Last Updated: 2026-03-31
# ==============================================================================
set -e # Exit on error
# Script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
NC='\033[0m' # No Color
# Configuration
OPENCLAW_DIR="/root/heretek/heretek-openclaw"
LITELLM_DIR="/opt/litellm"
CONFIG_DIR="/etc/openclaw"
LOG_DIR="/var/log/openclaw"
BACKUP_DIR="/var/backups/openclaw"
# Default options
OS_TYPE="auto"
GPU_TYPE="auto"
CLOUD_PROVIDER="auto"
# Logging functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
log_step() {
echo -e "${CYAN}[STEP]${NC} $1"
}
log_cloud() {
echo -e "${MAGENTA}[CLOUD]${NC} $1"
}
# Check if running as root
check_root() {
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root (use sudo)"
exit 1
fi
}
# Detect cloud provider
detect_cloud() {
if [[ "$CLOUD_PROVIDER" != "auto" ]]; then
log_info "Using specified cloud provider: $CLOUD_PROVIDER"
return
fi
log_step "Detecting cloud provider..."
# AWS EC2
if [[ -f /sys/hypervisor/uuid ]] && grep -qi "ec2" /sys/hypervisor/uuid 2>/dev/null; then
CLOUD_PROVIDER="aws"
elif dmidecode -s system-product-name 2>/dev/null | grep -qi "amazon ec2"; then
CLOUD_PROVIDER="aws"
fi
# GCP Compute
if dmidecode -s system-product-name 2>/dev/null | grep -qi "google compute"; then
CLOUD_PROVIDER="gcp"
fi
# Azure VM
if dmidecode -s system-manufacturer 2>/dev/null | grep -qi "microsoft corporation"; then
CLOUD_PROVIDER="azure"
fi
# DigitalOcean
if dmidecode -s system-product-name 2>/dev/null | grep -qi "digitalocean"; then
CLOUD_PROVIDER="digitalocean"
fi
# Linode
if dmidecode -s system-product-name 2>/dev/null | grep -qi "linode"; then
CLOUD_PROVIDER="linode"
fi
if [[ "$CLOUD_PROVIDER" == "auto" ]]; then
CLOUD_PROVIDER="bare-metal"
log_info "No cloud provider detected. Assuming bare metal."
else
log_cloud "Detected cloud provider: $CLOUD_PROVIDER"
fi
}
# Detect OS
detect_os() {
if [[ "$OS_TYPE" != "auto" ]]; then
log_info "Using specified OS type: $OS_TYPE"
return
fi
log_step "Detecting operating system..."
if [[ -f /etc/os-release ]]; then
. /etc/os-release
OS=$ID
VERSION=$VERSION_ID
OS_FAMILY=$ID_LIKE
log_info "Detected: $OS $VERSION"
if [[ "$OS" == "ubuntu" || "$OS" == "debian" || "$OS_FAMILY" == *"debian"* ]]; then
OS_TYPE="ubuntu"
INSTALLER_SCRIPT="ubuntu-deps.sh"
elif [[ "$OS" == "rhel" || "$OS" == "centos" || "$OS" == "rocky" || "$OS" == "almalinux" || "$OS_FAMILY" == *"rhel"* ]]; then
OS_TYPE="rhel"
INSTALLER_SCRIPT="rhel-deps.sh"
else
log_error "Unsupported OS: $OS"
exit 1
fi
else
log_error "Cannot detect OS. /etc/os-release not found."
exit 1
fi
}
# Detect GPU
detect_gpu() {
if [[ "$GPU_TYPE" != "auto" ]]; then
log_info "Using specified GPU type: $GPU_TYPE"
return
fi
log_step "Detecting GPU..."
GPU_TYPE="none"
# Check for AMD GPU
if lspci | grep -i "vga.*amd\|vga.*advanced micro devices" &>/dev/null; then
GPU_TYPE="amd"
log_info "Detected AMD GPU"
fi
# Check for NVIDIA GPU
if lspci | grep -i "vga.*nvidia\|3d.*nvidia" &>/dev/null; then
GPU_TYPE="nvidia"
log_info "Detected NVIDIA GPU"
fi
# Check for ROCm devices
if [[ -e /dev/kfd && -e /dev/dri ]]; then
GPU_TYPE="amd"
log_info "Detected AMD ROCm devices"
fi
# Check for NVIDIA devices
if [[ -e /dev/nvidia0 ]]; then
GPU_TYPE="nvidia"
log_info "Detected NVIDIA CUDA devices"
fi
log_info "GPU Type: $GPU_TYPE"
}
# Check system requirements
check_requirements() {
log_step "Checking system requirements..."
# Check CPU cores
CPU_CORES=$(nproc)
log_info "CPU Cores: $CPU_CORES"
# Check RAM
RAM_GB=$(free -g | awk '/^Mem:/{print $2}')
log_info "RAM: ${RAM_GB}GB"
# Check disk space
DISK_GB=$(df -BG / | awk 'NR==2 {print $4}' | tr -d 'G')
log_info "Free Disk Space: ${DISK_GB}GB"
# Cloud-specific checks
case $CLOUD_PROVIDER in
aws)
log_cloud "AWS EC2 detected. Verifying instance type..."
;;
gcp)
log_cloud "GCP Compute detected. Verifying machine type..."
;;
azure)
log_cloud "Azure VM detected. Verifying VM size..."
;;
esac
# Warnings
if [[ $CPU_CORES -lt 2 ]]; then
log_warning "Minimum 2 CPU cores recommended. Found: $CPU_CORES"
fi
if [[ $RAM_GB -lt 4 ]]; then
log_warning "Minimum 4GB RAM recommended. Found: ${RAM_GB}GB"
fi
if [[ $DISK_GB -lt 10 ]]; then
log_warning "Minimum 10GB free disk space recommended. Found: ${DISK_GB}GB"
fi
}
# Configure cloud-specific settings
configure_cloud() {
log_step "Configuring cloud-specific settings..."
case $CLOUD_PROVIDER in
aws)
configure_aws
;;
gcp)
configure_gcp
;;
azure)
configure_azure
;;
digitalocean)
configure_digitalocean
;;
linode)
configure_linode
;;
bare-metal)
log_info "Bare metal deployment. Skipping cloud configuration."
;;
esac
log_success "Cloud configuration complete"
}
# Configure AWS-specific settings
configure_aws() {
log_cloud "Configuring AWS-specific settings..."
# Install AWS CLI if not present
if ! command -v aws &>/dev/null; then
log_info "Installing AWS CLI..."
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "/tmp/awscliv2.zip"
unzip -q /tmp/awscliv2.zip -d /tmp
/tmp/aws/install
rm -rf /tmp/aws /tmp/awscliv2.zip
fi
# Check instance metadata
if curl -s http://169.254.169.254/latest/meta-data/instance-type &>/dev/null; then
INSTANCE_TYPE=$(curl -s http://169.254.169.254/latest/meta-data/instance-type)
log_cloud "Instance Type: $INSTANCE_TYPE"
fi
}
# Configure GCP-specific settings
configure_gcp() {
log_cloud "Configuring GCP-specific settings..."
# Install GCloud CLI if not present
if ! command -v gcloud &>/dev/null; then
log_info "Installing GCloud CLI..."
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
apt-get update -qq
apt-get install -y -qq google-cloud-cli
fi
# Check instance metadata
if curl -s -H "Metadata-Flavor: Google" http://metadata.google.internal/computeMetadata/v1/instance/machine-type &>/dev/null; then
MACHINE_TYPE=$(curl -s -H "Metadata-Flavor: Google" http://metadata.google.internal/computeMetadata/v1/instance/machine-type)
log_cloud "Machine Type: $MACHINE_TYPE"
fi
}
# Configure Azure-specific settings
configure_azure() {
log_cloud "Configuring Azure-specific settings..."
# Install Azure CLI if not present
if ! command -v az &>/dev/null; then
log_info "Installing Azure CLI..."
curl -sL https://aka.ms/InstallAzureCLIDeb | bash
fi
# Check instance metadata
if curl -s -H "Metadata:true" "http://169.254.169.254/metadata/instance/compute/vmSize?api-version=2021-02-01" &>/dev/null; then
VM_SIZE=$(curl -s -H "Metadata:true" "http://169.254.169.254/metadata/instance/compute/vmSize?api-version=2021-02-01")
log_cloud "VM Size: $VM_SIZE"
fi
}
# Configure DigitalOcean-specific settings
configure_digitalocean() {
log_cloud "Configuring DigitalOcean-specific settings..."
# Check droplet metadata
if curl -s http://169.254.169.254/metadata/v1/id &>/dev/null; then
DROPLET_ID=$(curl -s http://169.254.169.254/metadata/v1/id)
log_cloud "Droplet ID: $DROPLET_ID"
fi
}
# Configure Linode-specific settings
configure_linode() {
log_cloud "Configuring Linode-specific settings..."
# Check instance metadata
if curl -s http://169.254.169.254/metadata/linode/instance &>/dev/null; then
INSTANCE_ID=$(curl -s http://169.254.169.254/metadata/linode/instance)
log_cloud "Instance ID: $INSTANCE_ID"
fi
}
# Install dependencies
install_dependencies() {
log_step "Installing dependencies..."
# Download and run the appropriate dependencies script
DEPS_URL="https://raw.githubusercontent.com/Heretek-AI/heretek-openclaw/main/scripts/install/$INSTALLER_SCRIPT"
log_info "Downloading $INSTALLER_SCRIPT..."
curl -fsSL "$DEPS_URL" -o "/tmp/$INSTALLER_SCRIPT"
chmod +x "/tmp/$INSTALLER_SCRIPT"
log_info "Running $INSTALLER_SCRIPT..."
"/tmp/$INSTALLER_SCRIPT"
log_success "Dependencies installed"
}
# Clone repository
clone_repository() {
log_step "Cloning OpenClaw repository..."
if [[ -d "$OPENCLAW_DIR" ]]; then
log_warning "Directory $OPENCLAW_DIR already exists"
log_info "Pulling latest changes..."
cd "$OPENCLAW_DIR"
git pull
else
log_info "Cloning repository to $OPENCLAW_DIR..."
git clone https://github.com/Heretek-AI/heretek-openclaw.git "$OPENCLAW_DIR"
fi
log_success "Repository cloned"
}
# Create directories
create_directories() {
log_step "Creating directories..."
mkdir -p "$CONFIG_DIR"
mkdir -p "$LOG_DIR"
mkdir -p "$BACKUP_DIR"
mkdir -p /etc/litellm
mkdir -p /var/log/litellm
# Set permissions
chmod 755 "$CONFIG_DIR"
chmod 755 "$LOG_DIR"
chmod 755 "$BACKUP_DIR"
chmod 755 /etc/litellm
chmod 755 /var/log/litellm
log_success "Directories created"
}
# Configure environment
configure_environment() {
log_step "Configuring environment..."
cd "$OPENCLAW_DIR"
# Copy environment template
if [[ -f ".env.vm.example" ]]; then
cp ".env.vm.example" "$CONFIG_DIR/.env"
elif [[ -f ".env.bare-metal.example" ]]; then
cp ".env.bare-metal.example" "$CONFIG_DIR/.env"
elif [[ -f ".env.example" ]]; then
cp ".env.example" "$CONFIG_DIR/.env"
else
log_warning "No environment template found. Creating basic template..."
create_basic_env
fi
# Set permissions
chmod 600 "$CONFIG_DIR/.env"
log_success "Environment configured"
}
# Create basic environment file
create_basic_env() {
cat > "$CONFIG_DIR/.env" << 'EOF'
# Heretek OpenClaw - Environment Configuration
# Generated by vm-install.sh
# LiteLLM Gateway
LITELLM_MASTER_KEY=change-me-openssl-rand-hex-32
LITELLM_SALT_KEY=change-me-openssl-rand-hex-32
LITELLM_PORT=4000
# Model Providers
MINIMAX_API_KEY=your-minimax-key-here
ZAI_API_KEY=your-zai-key-here
# Database
POSTGRES_USER=openclaw
POSTGRES_PASSWORD=change-me-secure-password
POSTGRES_DB=openclaw
DATABASE_URL=postgresql://openclaw:change-me-secure-password@localhost:5432/openclaw
# Redis
REDIS_URL=redis://localhost:6379/0
# Ollama
OLLAMA_HOST=http://localhost:11434
# OpenClaw
OPENCLAW_DIR=/root/.openclaw
OPENCLAW_WORKSPACE=/root/.openclaw/agents
EOF
}
# Configure Ollama for GPU
configure_ollama() {
if [[ "$GPU_TYPE" == "amd" ]]; then
log_step "Configuring Ollama for AMD ROCm..."
mkdir -p /etc/systemd/system/ollama.service.d
cat > /etc/systemd/system/ollama.service.d/rocm.conf << EOF
[Service]
Environment="HSA_OVERRIDE_GFX_VERSION=10.3.0"
Environment="OLLAMA_HOST=0.0.0.0:11434"
DevicePolicy=closed
DeviceAllow=/dev/kfd rw
DeviceAllow=/dev/dri rw
EOF
systemctl daemon-reload
systemctl restart ollama
log_success "Ollama configured for AMD ROCm"
elif [[ "$GPU_TYPE" == "nvidia" ]]; then
log_step "Configuring Ollama for NVIDIA CUDA..."
mkdir -p /etc/systemd/system/ollama.service.d
cat > /etc/systemd/system/ollama.service.d/cuda.conf << EOF
[Service]
Environment="OLLAMA_HOST=0.0.0.0:11434"
DevicePolicy=closed
DeviceAllow=/dev/nvidia0 rw
DeviceAllow=/dev/nvidiactl rw
DeviceAllow=/dev/nvidia-uvm rw
EOF
systemctl daemon-reload
systemctl restart ollama
log_success "Ollama configured for NVIDIA CUDA"
else
log_info "No GPU detected. Ollama will run on CPU."
fi
# Pull embedding model
log_info "Pulling embedding model..."
ollama pull nomic-embed-text-v2-moe 2>/dev/null || log_warning "Failed to pull embedding model"
}
# Configure firewall for cloud
configure_firewall() {
log_step "Configuring firewall..."
case $CLOUD_PROVIDER in
aws)
log_cloud "For AWS, configure Security Groups in the AWS Console:"
log_info " - Allow SSH (22) from your IP"
log_info " - Allow LiteLLM (4000) from your IP"
log_info " - Allow OpenClaw Gateway (18789) from your IP"
;;
gcp)
log_cloud "For GCP, configure Firewall Rules in the GCP Console:"
log_info " - Allow SSH (22) from your IP"
log_info " - Allow LiteLLM (4000) from your IP"
log_info " - Allow OpenClaw Gateway (18789) from your IP"
;;
azure)
log_cloud "For Azure, configure NSG Rules in the Azure Portal:"
log_info " - Allow SSH (22) from your IP"
log_info " - Allow LiteLLM (4000) from your IP"
log_info " - Allow OpenClaw Gateway (18789) from your IP"
;;
esac
# Configure local firewall
if command -v ufw &> /dev/null; then
if ufw status | grep -q "Status: active"; then
ufw allow ssh
ufw allow 4000/tcp
ufw allow 18789/tcp
ufw allow 3000/tcp
log_success "UFW firewall configured"
fi
elif command -v firewall-cmd &> /dev/null; then
if systemctl is-active --quiet firewalld; then
firewall-cmd --permanent --add-service=ssh
firewall-cmd --permanent --add-port=4000/tcp
firewall-cmd --permanent --add-port=18789/tcp
firewall-cmd --permanent --add-port=3000/tcp
firewall-cmd --reload
log_success "firewalld configured"
fi
fi
}
# Print installation summary
print_summary() {
echo ""
echo "=============================================="
echo " Heretek OpenClaw - VM Installation Complete"
echo "=============================================="
echo ""
log_success "OpenClaw has been installed successfully!"
echo ""
echo "Deployment Details:"
echo " - Cloud Provider: $CLOUD_PROVIDER"
echo " - OS Type: $OS_TYPE"
echo " - GPU Type: $GPU_TYPE"
echo " - OpenClaw Directory: $OPENCLAW_DIR"
echo " - Configuration Directory: $CONFIG_DIR"
echo " - Log Directory: $LOG_DIR"
echo ""
echo "Next Steps:"
echo ""
echo " 1. Edit environment file with your API keys:"
echo " nano $CONFIG_DIR/.env"
echo ""
echo " 2. Configure cloud firewall/Security Groups:"
case $CLOUD_PROVIDER in
aws)
echo " AWS Console > EC2 > Security Groups > Add rules"
;;
gcp)
echo " GCP Console > VPC Network > Firewall > Add rule"
;;
azure)
echo " Azure Portal > Network Security Group > Add rule"
;;
esac
echo ""
echo " 3. Start services:"
echo " sudo systemctl start postgresql redis ollama litellm openclaw-gateway"
echo ""
echo " 4. Verify installation:"
echo " cd $OPENCLAW_DIR"
echo " ./scripts/health-check.sh"
echo ""
echo "Documentation:"
echo " - VM Deployment: $OPENCLAW_DIR/docs/deployment/VM_DEPLOYMENT.md"
echo " - Troubleshooting: $OPENCLAW_DIR/docs/deployment/NON_DOCKER_TROUBLESHOOTING.md"
echo ""
echo "=============================================="
}
# Main installation function
main() {
echo ""
echo "=============================================="
echo " Heretek OpenClaw - VM Installer"
echo "=============================================="
echo ""
check_root
detect_cloud
detect_os
detect_gpu
check_requirements
configure_cloud
install_dependencies
clone_repository
create_directories
configure_environment
configure_ollama
configure_firewall
print_summary
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--os)
OS_TYPE="$2"
shift 2
;;
--gpu)
GPU_TYPE="$2"
shift 2
;;
--cloud)
CLOUD_PROVIDER="$2"
shift 2
;;
--no-gpu)
GPU_TYPE="none"
shift
;;
--help)
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " --os OS_TYPE Specify OS type (ubuntu, rhel, auto)"
echo " --gpu GPU_TYPE Specify GPU type (amd, nvidia, none, auto)"
echo " --cloud PROVIDER Specify cloud provider (aws, gcp, azure, auto)"
echo " --no-gpu Skip GPU configuration"
echo " --help Show this help message"
echo ""
echo "Cloud Providers:"
echo " aws, gcp, azure, digitalocean, linode, bare-metal"
exit 0
;;
*)
log_error "Unknown option: $1"
exit 1
;;
esac
done
# Run main function
main "$@"
+46
View File
@@ -0,0 +1,46 @@
[Unit]
Description=LiteLLM Proxy Service
Documentation=https://docs.litellm.ai
After=network.target postgresql.service redis.service
Wants=postgresql.service redis.service
[Service]
Type=simple
User=litellm
Group=litellm
# Environment configuration
Environment="LITELLM_CONFIG_PATH=/etc/litellm/litellm_config.yaml"
Environment="DATABASE_URL=postgresql://openclaw:password@localhost:5432/openclaw"
Environment="REDIS_URL=redis://localhost:6379/0"
EnvironmentFile=-/etc/openclaw/.env
# Working directory
WorkingDirectory=/opt/litellm
# Main execution
ExecStart=/opt/litellm/venv/bin/litellm --config /etc/litellm/litellm_config.yaml --port 4000 --num_workers 4
# Restart configuration
Restart=on-failure
RestartSec=5
# Resource limits
LimitNOFILE=65535
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=read-only
PrivateTmp=true
# Allow write access to log directory
ReadWritePaths=/var/log/litellm
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=litellm
[Install]
WantedBy=multi-user.target
+58
View File
@@ -0,0 +1,58 @@
[Unit]
Description=Ollama Service
Documentation=https://ollama.ai
After=network.target
Wants=network.target
[Service]
Type=notify
User=ollama
Group=ollama
# Environment configuration
Environment="OLLAMA_HOST=0.0.0.0:11434"
Environment="OLLAMA_MODELS=/var/lib/ollama/models"
# Working directory
WorkingDirectory=/var/lib/ollama
# Main execution
ExecStart=/usr/local/bin/ollama serve
# Restart configuration
Restart=on-failure
RestartSec=5
# Resource limits
LimitNOFILE=65535
LimitNPROC=unlimited
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=read-only
PrivateTmp=true
# Allow write access to Ollama directories
ReadWritePaths=/var/lib/ollama
# GPU access (AMD ROCm)
# Uncomment the following lines for AMD GPU support
# DevicePolicy=closed
# DeviceAllow=/dev/kfd rw
# DeviceAllow=/dev/dri rw
# GPU access (NVIDIA CUDA)
# Uncomment the following lines for NVIDIA GPU support
# DevicePolicy=closed
# DeviceAllow=/dev/nvidia0 rw
# DeviceAllow=/dev/nvidiactl rw
# DeviceAllow=/dev/nvidia-uvm rw
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=ollama
[Install]
WantedBy=multi-user.target
+46
View File
@@ -0,0 +1,46 @@
[Unit]
Description=OpenClaw Gateway Service
Documentation=https://github.com/Heretek-AI/heretek-openclaw
After=network.target litellm.service postgresql.service redis.service
Wants=litellm.service postgresql.service redis.service
[Service]
Type=simple
User=root
Group=root
# Environment configuration
Environment="OPENCLAW_DIR=/root/.openclaw"
Environment="OPENCLAW_WORKSPACE=/root/.openclaw/agents"
Environment="NODE_ENV=production"
EnvironmentFile=-/etc/openclaw/.env
# Working directory
WorkingDirectory=/root/heretek/heretek-openclaw
# Main execution
ExecStart=/usr/bin/node /root/heretek/heretek-openclaw/dist/gateway/index.js
Restart=on-failure
RestartSec=10
# Resource limits
LimitNOFILE=65535
LimitNPROC=4096
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=read-only
PrivateTmp=true
# Allow write access to OpenClaw directories
ReadWritePaths=/root/.openclaw
ReadWritePaths=/var/log/openclaw
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=openclaw-gateway
[Install]
WantedBy=multi-user.target
+48
View File
@@ -0,0 +1,48 @@
[Unit]
Description=PostgreSQL RDBMS with pgvector
Documentation=https://www.postgresql.org/docs/
After=network.target
Wants=network.target
[Service]
Type=forking
User=postgres
Group=postgres
# Environment configuration
Environment="PGDATA=/var/lib/postgresql/15/main"
Environment="PGVERSION=15"
# Working directory
WorkingDirectory=/var/lib/postgresql
# Main execution
ExecStart=/usr/lib/postgresql/15/bin/pg_ctl -D /var/lib/postgresql/15/main -o "-c config_file=/etc/postgresql/15/main/postgresql.conf" start
ExecReload=/usr/lib/postgresql/15/bin/pg_ctl -D /var/lib/postgresql/15/main reload
ExecStop=/usr/lib/postgresql/15/bin/pg_ctl -D /var/lib/postgresql/15/main -m fast stop
# Restart configuration
Restart=on-failure
RestartSec=5
# Timeout for startup
TimeoutSec=300
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=read-only
PrivateTmp=true
# Allow write access to PostgreSQL directories
ReadWritePaths=/var/lib/postgresql
ReadWritePaths=/var/log/postgresql
ReadWritePaths=/run/postgresql
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=postgresql
[Install]
WantedBy=multi-user.target
+43
View File
@@ -0,0 +1,43 @@
[Unit]
Description=Redis In-Memory Data Store
Documentation=https://redis.io
After=network.target
Wants=network.target
[Service]
Type=notify
User=redis
Group=redis
# Environment configuration
Environment="REDIS_PORT=6379"
Environment="REDIS_BIND_ADDRESS=127.0.0.1"
# Working directory
WorkingDirectory=/var/lib/redis
# Main execution
ExecStart=/usr/bin/redis-server /etc/redis/redis.conf --daemonize no
# Restart configuration
Restart=on-failure
RestartSec=5
# Resource limits
LimitNOFILE=65535
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=read-only
PrivateTmp=true
ReadWritePaths=/var/lib/redis
ReadWritePaths=/var/log/redis
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=redis
[Install]
WantedBy=multi-user.target