"""
EasyClaw MCP Server — Database Layer
SQLite with WAL mode, FTS5 full-text search, and 18 knowledge guides.
"""

import hashlib
import os
import random
import sqlite3
import uuid
from datetime import datetime, timezone

# ---------------------------------------------------------------------------
# Connection helper
# ---------------------------------------------------------------------------

DB_PATH = os.environ.get("EASYCLAW_DB_PATH", "/data/easyclaw.db")


def _get_conn() -> sqlite3.Connection:
    """Open a fresh SQLite connection with WAL mode and foreign keys."""
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    conn.execute("PRAGMA journal_mode=WAL")
    conn.execute("PRAGMA foreign_keys=ON")
    return conn


# ---------------------------------------------------------------------------
# Schema creation
# ---------------------------------------------------------------------------

def init_db():
    """Create all tables and populate the FTS5 index on startup."""
    conn = _get_conn()
    try:
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS users (
                id TEXT PRIMARY KEY,
                email TEXT UNIQUE NOT NULL,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            );

            CREATE TABLE IF NOT EXISTS subscriptions (
                id TEXT PRIMARY KEY,
                user_id TEXT NOT NULL,
                stripe_customer_id TEXT,
                stripe_subscription_id TEXT,
                tier TEXT NOT NULL DEFAULT 'starter',
                status TEXT NOT NULL DEFAULT 'active',
                api_key TEXT UNIQUE,
                api_key_hash TEXT UNIQUE,
                daily_limit INTEGER NOT NULL DEFAULT 100,
                current_period_end TIMESTAMP,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                FOREIGN KEY (user_id) REFERENCES users(id)
            );

            CREATE TABLE IF NOT EXISTS usage_logs (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                api_key_hash TEXT NOT NULL,
                tool_name TEXT NOT NULL,
                timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                response_time_ms INTEGER,
                success BOOLEAN DEFAULT 1
            );
            CREATE INDEX IF NOT EXISTS idx_usage_hash_time
                ON usage_logs(api_key_hash, timestamp);

            CREATE TABLE IF NOT EXISTS knowledge_entries (
                topic TEXT PRIMARY KEY,
                title TEXT NOT NULL,
                content TEXT NOT NULL,
                source TEXT DEFAULT 'manual',
                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            );

            CREATE TABLE IF NOT EXISTS otp_codes (
                email TEXT NOT NULL,
                code TEXT NOT NULL,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                expires_at TIMESTAMP NOT NULL,
                used BOOLEAN DEFAULT 0
            );
            CREATE INDEX IF NOT EXISTS idx_otp_email
                ON otp_codes(email, used, expires_at);

            CREATE TABLE IF NOT EXISTS guide_purchases (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                email TEXT NOT NULL,
                guide_type TEXT NOT NULL,
                download_token TEXT UNIQUE NOT NULL,
                stripe_session_id TEXT,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                downloaded_at TIMESTAMP
            );
        """)

        # FTS5 virtual table — must create outside executescript
        try:
            conn.execute("""
                CREATE VIRTUAL TABLE IF NOT EXISTS knowledge_fts
                USING fts5(topic, title, content, tokenize='porter unicode61')
            """)
        except sqlite3.OperationalError:
            pass  # Already exists

        conn.commit()
    finally:
        conn.close()

    # Populate / rebuild FTS index
    _rebuild_knowledge_fts()


# ===================================================================
# KNOWLEDGE BASE — 18 Built-in Guides
# ===================================================================

KNOWLEDGE_BASE = {

    # ------------------------------------------------------------------
    # 1. quick_start
    # ------------------------------------------------------------------
    "quick_start": {
        "title": "OpenClaw Quick Start Guide",
        "content": """# OpenClaw Quick Start Guide

## Prerequisites
- Linux server (Ubuntu 22.04+ recommended) or macOS
- Node.js 18+ and npm
- An OpenRouter API key (get one at https://openrouter.ai)
- Root/sudo access for systemd setup

## Installation (5 Minutes)

### Step 1: Install the CLI
```bash
npm install -g @openclaw/cli
```

### Step 2: Initialize Configuration
```bash
openclaw config init
```
This creates `/etc/openclaw/openclaw.json` with sensible defaults.

### Step 3: Set Your API Key
```bash
openclaw config set openrouter.apiKey "sk-or-v1-your-key-here"
```

### Step 4: Generate Authentication Token
```bash
openclaw token generate
```
Outputs a JWT token for API access.

### Step 5: Set Up the Workspace
```bash
openclaw workspace init
```
Creates `/var/lib/openclaw/` with required directories.

### Step 6: Start the Server
```bash
openclaw gateway install
openclaw gateway configure
systemctl start openclaw openclaw-gateway
```

### Step 7: Verify
```bash
openclaw health --full
```
All checks should show OK.

## First Steps After Install
- Edit `SOUL.md` in the workspace to define your AI's personality
- Edit `USER.md` with your preferences and context
- Create `memory/` folder for persistent storage
- Connect a channel (Telegram, Discord, Slack, etc.)

## Key Commands
| Command | Purpose |
|---------|---------|
| `openclaw config init` | Create default config |
| `openclaw config set KEY VALUE` | Update a config field |
| `openclaw health --full` | Full system health check |
| `openclaw channels list` | List connected channels |
| `openclaw mcp status` | Check MCP connection |

## Automated Installation
For a fully guided experience, use the EasyClaw installer:
```bash
curl -fsSL https://easyclaw.help/install.sh | bash
```
This launches an interactive TUI with an AI assistant (Agent Johnny 5) that handles everything automatically.
"""
    },

    # ------------------------------------------------------------------
    # 2. openrouter_models
    # ------------------------------------------------------------------
    "openrouter_models": {
        "title": "OpenRouter Model Guide",
        "content": """# OpenRouter Model Guide for OpenClaw

## How OpenClaw Uses Models
OpenClaw routes all AI requests through OpenRouter, giving you access to 200+ models from major providers. Your `openclaw.json` config specifies which model to use for different tasks.

## Model Tiers & Recommendations

### Budget Tier (< $0.50 / million tokens)
| Model | Input | Output | Context | Best For |
|-------|-------|--------|---------|----------|
| MiniMax M2.1 | $0.10 | $0.30 | 1M | Sub-agents, simple tasks |
| DeepSeek V3.2 | $0.14 | $0.28 | 128K | General chat, coding |
| Qwen 3 235B | $0.20 | $0.60 | 128K | Reasoning, analysis |
| Gemini 2.5 Flash Lite | $0.02 | $0.10 | 1M | High-volume, fast tasks |

### Mid-Range Tier ($0.50–$3.00 / million tokens)
| Model | Input | Output | Context | Best For |
|-------|-------|--------|---------|----------|
| Kimi K2.5 | $0.60 | $2.00 | 128K | Complex reasoning, coding |
| Claude 3.5 Haiku | $0.80 | $4.00 | 200K | Fast, quality responses |
| GPT-4.1 Mini | $0.40 | $1.60 | 1M | Balanced cost/quality |
| Gemini 2.5 Flash | $0.15 | $0.60 | 1M | Multi-modal, fast |

### Premium Tier (> $3.00 / million tokens)
| Model | Input | Output | Context | Best For |
|-------|-------|--------|---------|----------|
| Claude Sonnet 4 | $3.00 | $15.00 | 200K | Best coding, reasoning |
| GPT-4.1 | $2.00 | $8.00 | 1M | All-around premium |
| Gemini 2.5 Pro | $1.25 | $10.00 | 1M | Long context, analysis |
| Claude Opus 4 | $15.00 | $75.00 | 200K | Most capable, complex tasks |

## Recommended Configurations

### For Personal Use (Budget)
```json
{
  "openrouter": {
    "model": "moonshotai/kimi-k2.5",
    "subAgentModel": "minimax/minimax-m2.1",
    "thinkingModel": "qwen/qwen3-235b"
  }
}
```
Estimated cost: $5–15/month at moderate usage.

### For Business Use (Quality)
```json
{
  "openrouter": {
    "model": "anthropic/claude-sonnet-4",
    "subAgentModel": "anthropic/claude-3.5-haiku",
    "thinkingModel": "google/gemini-2.5-pro"
  }
}
```
Estimated cost: $30–100/month.

### For Development (Speed + Quality)
```json
{
  "openrouter": {
    "model": "deepseek/deepseek-v3-0324",
    "subAgentModel": "minimax/minimax-m2.1",
    "thinkingModel": "moonshotai/kimi-k2.5"
  }
}
```
Estimated cost: $3–10/month.

## Model Selection Tips
1. **Start cheap** — Use DeepSeek V3.2 or MiniMax M2.1 while testing
2. **Sub-agents should be fast and cheap** — They handle simple tool calls
3. **Enable thinking mode** for complex tasks only — it costs more
4. **Monitor costs** via OpenRouter dashboard at openrouter.ai/activity
5. **Context window matters** — Use 1M context models for large codebases
"""
    },

    # ------------------------------------------------------------------
    # 3. persistent_memory
    # ------------------------------------------------------------------
    "persistent_memory": {
        "title": "Persistent Memory Configuration",
        "content": """# Persistent Memory Guide

## Overview
OpenClaw supports three memory strategies: file-based, vector-based, and hybrid. Memory enables your AI to retain context across sessions.

## File-Based Memory (Default)

### Workspace Structure
```
/var/lib/openclaw/
├── SOUL.md           # AI personality definition
├── USER.md           # User preferences, timezone, context
├── MEMORY.md         # Long-term important memories
├── memory/
│   ├── projects/     # Project-specific context
│   ├── preferences/  # User preferences, style guides
│   ├── knowledge/    # Research, reference material
│   └── YYYY-MM-DD.md # Daily session notes
└── skills/           # Installed skill packs
```

### Configuration
```json
{
  "memory": {
    "type": "file",
    "path": "/var/lib/openclaw/memory",
    "maxFileSize": "50KB",
    "autoSummarize": true,
    "dailyNotes": true
  }
}
```

### Key Files
- **SOUL.md** — Defines your AI's personality, tone, and behavioral guidelines
- **USER.md** — Your personal info (name, timezone, preferences)
- **MEMORY.md** — Critical long-term memories the AI should always know

## Vector Memory (Advanced)

Uses a vector database (Qdrant) for semantic search over past conversations.

### Configuration
```json
{
  "memory": {
    "type": "vector",
    "provider": "qdrant",
    "url": "http://localhost:6333",
    "collection": "openclaw_memory",
    "embeddingModel": "text-embedding-3-small",
    "topK": 5
  }
}
```

### Setup
```bash
# Start Qdrant
docker run -p 6333:6333 qdrant/qdrant

# Configure OpenClaw
openclaw config set memory.type vector
openclaw config set memory.provider qdrant
```

## Hybrid Memory (Recommended for Production)

Combines file-based (for structured data) with vector (for semantic search).

```json
{
  "memory": {
    "type": "hybrid",
    "file": { "path": "/var/lib/openclaw/memory" },
    "vector": { "provider": "qdrant", "url": "http://localhost:6333" }
  }
}
```

## Best Practices
1. Update daily notes each session
2. Move important items to MEMORY.md weekly
3. Keep individual files under 50KB
4. Use Google Keep or similar for mobile access
5. Back up the memory directory regularly
"""
    },

    # ------------------------------------------------------------------
    # 4. channels
    # ------------------------------------------------------------------
    "channels": {
        "title": "Channel Integration Guide",
        "content": """# Channel Integration Guide

## Overview
OpenClaw supports 5 messaging channels. Each channel connects through the gateway service.

## Telegram (Recommended — Most Stable)

### Setup
1. Message @BotFather on Telegram
2. Send `/newbot` and follow prompts
3. Copy the API token

### Configuration
```json
{
  "channels": {
    "telegram": {
      "enabled": true,
      "token": "BOT_TOKEN_HERE",
      "allowedUsers": [123456789],
      "webhook": true,
      "webhookUrl": "https://your-server.com/webhook/telegram"
    }
  }
}
```

### Commands
```bash
openclaw channels init telegram
openclaw channels list
```

## Discord

### Setup
1. Go to https://discord.com/developers/applications
2. Create application → Add bot
3. Enable Message Content Intent
4. Generate OAuth2 URL with bot permissions
5. Add bot to your server

### Configuration
```json
{
  "channels": {
    "discord": {
      "enabled": true,
      "token": "BOT_TOKEN_HERE",
      "allowedGuilds": ["GUILD_ID"],
      "allowedChannels": ["CHANNEL_ID"],
      "prefix": "!"
    }
  }
}
```

## Slack

### Setup
1. Create app at https://api.slack.com/apps
2. Add Bot Token Scopes: `chat:write`, `app_mentions:read`, `channels:history`
3. Install to workspace
4. Copy Bot User OAuth Token

### Configuration
```json
{
  "channels": {
    "slack": {
      "enabled": true,
      "botToken": "xoxb-TOKEN",
      "signingSecret": "SIGNING_SECRET",
      "allowedChannels": ["C1234567890"]
    }
  }
}
```

## WhatsApp

### Setup (via Evolution API)
1. Deploy Evolution API: `docker run -p 8080:8080 atendai/evolution-api`
2. Create instance and scan QR code
3. Configure webhook URL

### Configuration
```json
{
  "channels": {
    "whatsapp": {
      "enabled": true,
      "provider": "evolution-api",
      "apiUrl": "http://localhost:8080",
      "instance": "openclaw",
      "allowedNumbers": ["+1234567890"]
    }
  }
}
```

## Matrix

### Configuration
```json
{
  "channels": {
    "matrix": {
      "enabled": true,
      "homeserver": "https://matrix.org",
      "userId": "@bot:matrix.org",
      "accessToken": "TOKEN",
      "allowedRooms": ["!roomid:matrix.org"]
    }
  }
}
```

## Security Best Practices
- Always use allowlists (allowedUsers, allowedChannels, etc.)
- Store tokens in environment variables, not config files
- Enable rate limiting per channel
- Use webhook mode for Telegram (more reliable than polling)
- Rotate tokens periodically
"""
    },

    # ------------------------------------------------------------------
    # 5. troubleshooting
    # ------------------------------------------------------------------
    "troubleshooting": {
        "title": "Troubleshooting Guide",
        "content": """# OpenClaw Troubleshooting Guide

## Common Issues & Solutions

### 1. Agent Not Responding
**Symptoms**: No response to messages, commands hang
**Steps**:
```bash
openclaw health --full          # Check overall health
systemctl status openclaw       # Check server service
systemctl status openclaw-gateway  # Check gateway service
journalctl -u openclaw -n 50    # View recent server logs
journalctl -u openclaw-gateway -n 50  # View gateway logs
```
**Common fixes**:
- Restart services: `systemctl restart openclaw openclaw-gateway`
- Check API key validity on OpenRouter dashboard
- Verify port 3100 (server) and 3200 (gateway) aren't blocked

### 2. Memory Not Persisting
**Symptoms**: AI forgets context between sessions
**Steps**:
```bash
ls -la /var/lib/openclaw/memory/   # Check directory exists
cat /var/lib/openclaw/MEMORY.md    # Check memory file
openclaw config validate           # Check config
```
**Common fixes**:
- Create memory directory: `mkdir -p /var/lib/openclaw/memory`
- Fix permissions: `chmod 755 /var/lib/openclaw/memory`
- Ensure `memory.type` is set in config

### 3. API Key Errors
**Symptoms**: "Invalid API key", "Unauthorized", 401 errors
**Steps**:
```bash
openclaw config validate    # Validates config including API key format
curl -H "Authorization: Bearer sk-or-v1-..." https://openrouter.ai/api/v1/models
```
**Common fixes**:
- Regenerate key at openrouter.ai/keys
- Check for trailing whitespace in config
- Ensure key starts with `sk-or-v1-`

### 4. Channel Disconnected
**Symptoms**: Messages not received from channel
**Steps**:
```bash
openclaw channels list           # Check channel status
journalctl -u openclaw-gateway --since "1 hour ago" | grep -i error
```
**Common fixes**:
- Re-verify bot token with the platform
- Check webhook URL is accessible from internet
- Ensure firewall allows inbound connections on webhook port
- For Telegram: check bot hasn't been blocked or deleted

### 5. Port Already in Use
**Symptoms**: `EADDRINUSE` error on startup
**Steps**:
```bash
lsof -i :3100    # Check what's using server port
lsof -i :3200    # Check what's using gateway port
```
**Common fixes**:
- Kill conflicting process: `kill -9 <PID>`
- Change port in config: `openclaw config set server.port 3101`

### 6. Permission Denied (EACCES)
**Symptoms**: Can't write to config/workspace directories
**Steps**:
```bash
ls -la /etc/openclaw/
ls -la /var/lib/openclaw/
whoami
```
**Common fixes**:
- Run as root or use sudo
- Fix ownership: `chown -R root:root /etc/openclaw /var/lib/openclaw`

### 7. Node.js Version Mismatch
**Symptoms**: Syntax errors, unexpected token errors
**Steps**:
```bash
node --version    # Must be >= 18
npm --version
```
**Common fixes**:
- Install Node.js 20 LTS via NodeSource
- Remove old versions: `apt remove nodejs && apt autoremove`

### 8. FTS5 Syntax Error
**Symptoms**: `fts5: syntax error` in MCP tool calls
**Cause**: Search query contains FTS5 operators (`*`, `-`, `OR`, `AND`, `NOT`, `NEAR`)
**Fix**: The MCP server handles this automatically with LIKE fallback

### 9. Gateway Config Rejected
**Symptoms**: Gateway won't start, "invalid configuration" error
**Steps**:
```bash
openclaw config validate
cat /etc/openclaw/openclaw.json | python3 -m json.tool  # Check valid JSON
```
**Common fixes**:
- Fix JSON syntax errors (trailing commas, missing quotes)
- Remove unknown config keys
- Reset to defaults: `openclaw config init --force`

### 10. Systemd Service Won't Start
**Symptoms**: `systemctl start openclaw` fails
**Steps**:
```bash
systemctl status openclaw
journalctl -u openclaw --no-pager -n 100
cat /etc/systemd/system/openclaw.service
```
**Common fixes**:
- Reload daemon: `systemctl daemon-reload`
- Check ExecStart path exists
- Verify Node.js binary path: `which node`
- Check working directory exists

### 11. MCP Connection Timeout
**Symptoms**: Agent can't reach MCP knowledge base
**Steps**:
```bash
curl -s https://mcp.easyclaw.help/health
openclaw mcp status
```
**Common fixes**:
- Check internet connectivity
- Verify DNS resolution: `dig mcp.easyclaw.help`
- Use `--no-mcp` flag to run without MCP temporarily

### 12. High Memory Usage
**Symptoms**: OOM kills, server slowdown
**Steps**:
```bash
free -h
ps aux --sort=-%mem | head -10
```
**Common fixes**:
- Reduce context window size in config
- Use smaller models (MiniMax M2.1 uses less memory)
- Add swap space: `fallocate -l 2G /swapfile && mkswap /swapfile && swapon /swapfile`
"""
    },

    # ------------------------------------------------------------------
    # 6. skills
    # ------------------------------------------------------------------
    "skills": {
        "title": "OpenClaw Skills Guide",
        "content": """# OpenClaw Skills & ClawHub

## What Are Skills?
Skills are modular packages that teach OpenClaw how to use tools, APIs, and workflows. They are installed from ClawHub, the official skill marketplace.

## Installing Skills
```bash
# Browse available skills
clawhub search weather

# Install a skill
clawhub install weather

# List installed skills
clawhub list

# Update all skills
clawhub update
```

## Popular Skills

### Communication
| Skill | Description |
|-------|------------|
| `gmail` | Read, compose, and manage Gmail |
| `calendar` | Google Calendar integration |
| `contacts` | Contact management |
| `slack-tools` | Advanced Slack operations |

### Productivity
| Skill | Description |
|-------|------------|
| `youtube-comment-bot` | YouTube comment management |
| `web-scraper` | Intelligent web scraping |
| `file-manager` | Advanced file operations |
| `task-tracker` | Task and project management |

### Development
| Skill | Description |
|-------|------------|
| `github` | GitHub repo management |
| `docker-manager` | Docker container control |
| `db-query` | Database query execution |
| `api-tester` | API endpoint testing |

### Media
| Skill | Description |
|-------|------------|
| `tts` | Text-to-speech generation |
| `image-gen` | AI image generation |
| `transcribe` | Audio/video transcription |
| `screenshot` | Web page screenshots |

## Creating Custom Skills

### Skill Structure
```
my-skill/
├── SKILL.md          # Instructions for the AI
├── package.json      # Metadata and dependencies
├── tools/
│   └── my-tool.js    # Tool implementations
└── prompts/
    └── default.md    # Default prompts
```

### SKILL.md Example
```markdown
# My Custom Skill

## What This Skill Does
Describe the skill's purpose.

## Tools Available
- `my_tool(param1, param2)` — What it does

## Usage Examples
- "Use my-skill to do X"
- "Run my-tool with param1=value"
```

### Publishing to ClawHub
```bash
clawhub publish ./my-skill
```
"""
    },

    # ------------------------------------------------------------------
    # 7. security_hardening
    # ------------------------------------------------------------------
    "security_hardening": {
        "title": "Security Hardening Guide",
        "content": """# OpenClaw Security Hardening Guide

## Overview
Default OpenClaw installations are functional but not hardened. This guide covers 11 critical security measures.

## 1. API Key Protection
```bash
# Move API key to environment variable
openclaw config set openrouter.apiKey "$OPENROUTER_API_KEY"

# Or use a secrets file
echo "sk-or-v1-your-key" > /etc/openclaw/.api-key
chmod 600 /etc/openclaw/.api-key
openclaw config set openrouter.apiKeyFile "/etc/openclaw/.api-key"
```

## 2. Rate Limiting
```json
{
  "security": {
    "rateLimit": {
      "enabled": true,
      "maxRequestsPerMinute": 30,
      "maxTokensPerDay": 1000000,
      "perUser": true
    }
  }
}
```

## 3. Input Validation
```json
{
  "security": {
    "inputValidation": {
      "maxMessageLength": 4000,
      "stripHtml": true,
      "blockCodeExecution": true,
      "contentFilter": "moderate"
    }
  }
}
```

## 4. Channel Allowlists
Always restrict which users/channels can interact:
```json
{
  "channels": {
    "telegram": {
      "allowedUsers": [123456789, 987654321],
      "blockUnknownUsers": true
    }
  }
}
```

## 5. Network Security
```bash
# Firewall rules (UFW)
ufw allow 22/tcp         # SSH
ufw allow 80/tcp         # HTTP (redirect to HTTPS)
ufw allow 443/tcp        # HTTPS
ufw deny 3100/tcp        # Block direct server access
ufw deny 3200/tcp        # Block direct gateway access
ufw enable
```

## 6. TLS/HTTPS
```bash
# Use Traefik or nginx with Let's Encrypt
# Never expose OpenClaw directly — always behind a reverse proxy
```

## 7. API Key Rotation
```bash
# Generate new API key quarterly
openclaw token generate --rotate
# Update connected services with new key
```

## 8. Logging & Monitoring
```json
{
  "logging": {
    "level": "info",
    "requests": true,
    "sensitiveDataMasking": true,
    "file": "/var/log/openclaw/openclaw.log",
    "maxSize": "100MB",
    "rotate": true
  }
}
```

## 9. File System Permissions
```bash
chmod 750 /etc/openclaw
chmod 640 /etc/openclaw/openclaw.json
chmod 750 /var/lib/openclaw
chown -R openclaw:openclaw /etc/openclaw /var/lib/openclaw
```

## 10. Memory Isolation
```json
{
  "memory": {
    "isolation": true,
    "maxFileSize": "50KB",
    "allowedPaths": ["/var/lib/openclaw/memory"],
    "blockSystemFiles": true
  }
}
```

## 11. Regular Updates
```bash
# Update OpenClaw CLI
npm update -g @openclaw/cli

# Update skills
clawhub update

# Check for security advisories
openclaw health --security
```

## Security Checklist
- [ ] API key stored in env var or secrets file (not plain config)
- [ ] Rate limiting enabled
- [ ] Input validation enabled
- [ ] Channel allowlists configured
- [ ] Firewall rules active
- [ ] TLS/HTTPS via reverse proxy
- [ ] API keys rotated quarterly
- [ ] Logging enabled with data masking
- [ ] File permissions restricted
- [ ] Memory isolation enabled
- [ ] Regular updates scheduled
"""
    },

    # ------------------------------------------------------------------
    # 8. automation_crons
    # ------------------------------------------------------------------
    "automation_crons": {
        "title": "Automation & Scheduled Tasks",
        "content": """# Automation & Scheduled Tasks

## Overview
OpenClaw supports scheduled tasks via cron-style automation. You can schedule regular actions like memory cleanup, reports, and health checks.

## Built-in Automations

### Memory Summarization
```json
{
  "automation": {
    "memorySummarize": {
      "enabled": true,
      "schedule": "0 3 * * *",
      "action": "summarize_daily_notes"
    }
  }
}
```

### Health Check Reports
```json
{
  "automation": {
    "healthReport": {
      "enabled": true,
      "schedule": "0 */6 * * *",
      "notifyChannel": "telegram",
      "onFailure": "alert"
    }
  }
}
```

### Log Rotation
```json
{
  "automation": {
    "logRotation": {
      "enabled": true,
      "schedule": "0 0 * * 0",
      "maxAge": "30d",
      "compress": true
    }
  }
}
```

## Custom Cron Jobs

### Via systemd Timer
```bash
# Create timer unit
cat > /etc/systemd/system/openclaw-cleanup.timer << 'EOF'
[Unit]
Description=OpenClaw Memory Cleanup

[Timer]
OnCalendar=daily
Persistent=true

[Install]
WantedBy=timers.target
EOF

# Create service unit
cat > /etc/systemd/system/openclaw-cleanup.service << 'EOF'
[Unit]
Description=OpenClaw Memory Cleanup Job

[Service]
Type=oneshot
ExecStart=/usr/bin/openclaw memory cleanup --older-than 90d
EOF

systemctl enable --now openclaw-cleanup.timer
```

### Via n8n Workflows
Connect OpenClaw to n8n for visual workflow automation:
1. Use HTTP Request node to call OpenClaw API
2. Schedule with Cron trigger node
3. Handle responses with IF/Switch nodes
4. Send notifications via Telegram/Slack/Email

## Cron Expression Reference
```
┌──────── minute (0-59)
│ ┌────── hour (0-23)
│ │ ┌──── day of month (1-31)
│ │ │ ┌── month (1-12)
│ │ │ │ ┌ day of week (0-6, Sun=0)
│ │ │ │ │
* * * * *
```

| Expression | Meaning |
|------------|---------|
| `0 * * * *` | Every hour |
| `0 3 * * *` | Daily at 3 AM |
| `0 */6 * * *` | Every 6 hours |
| `0 0 * * 0` | Weekly (Sunday midnight) |
| `0 0 1 * *` | Monthly (1st at midnight) |
"""
    },

    # ------------------------------------------------------------------
    # 9. sub_agents
    # ------------------------------------------------------------------
    "sub_agents": {
        "title": "Multi-Agent Orchestration",
        "content": """# Multi-Agent Orchestration Guide

## Overview
OpenClaw supports spawning sub-agents for parallel task execution. The main agent delegates work to cheaper, faster models while maintaining oversight.

## Architecture
```
Main Agent (Kimi K2.5 / Claude Sonnet)
├── Sub-Agent 1 (MiniMax M2.1) → Research task
├── Sub-Agent 2 (MiniMax M2.1) → Data processing
└── Sub-Agent 3 (DeepSeek V3.2) → Code generation
```

## Configuration
```json
{
  "agents": {
    "main": {
      "model": "moonshotai/kimi-k2.5",
      "maxTokens": 8192
    },
    "subAgents": {
      "default": {
        "model": "minimax/minimax-m2.1",
        "maxTokens": 4096,
        "timeout": 30
      },
      "coding": {
        "model": "deepseek/deepseek-v3-0324",
        "maxTokens": 8192,
        "timeout": 60
      }
    },
    "maxConcurrent": 3,
    "maxDepth": 2
  }
}
```

## Usage Patterns

### Parallel Research
Main agent spawns multiple sub-agents to research different aspects simultaneously, then synthesizes results.

### Code Review Pipeline
1. Sub-agent 1: Analyze code structure
2. Sub-agent 2: Check for security issues
3. Sub-agent 3: Suggest optimizations
4. Main agent: Compile review report

### Data Processing
Main agent breaks large datasets into chunks, distributes to sub-agents, collects and merges results.

## Cost Optimization
- Use MiniMax M2.1 ($0.10/M input) for sub-agents
- Reserve premium models for the main agent
- Set strict token limits on sub-agents
- Monitor with: `openclaw usage --by-agent`

## Limitations
- Max 3 concurrent sub-agents (configurable)
- Max 2 levels of nesting (no sub-sub-sub-agents)
- Sub-agents cannot access main agent's conversation history
- Each sub-agent has independent rate limits
"""
    },

    # ------------------------------------------------------------------
    # 10. browser_automation
    # ------------------------------------------------------------------
    "browser_automation": {
        "title": "Browser Automation Guide",
        "content": """# Browser Automation Guide

## Overview
OpenClaw can control headless browsers for web scraping, form filling, screenshot capture, and interaction with web applications.

## Setup

### Install Browserless
```bash
docker run -p 3000:3000 browserless/chrome
```

### Configure OpenClaw
```json
{
  "browser": {
    "enabled": true,
    "provider": "browserless",
    "url": "http://localhost:3000",
    "timeout": 30000,
    "maxPages": 5,
    "defaultViewport": { "width": 1280, "height": 800 }
  }
}
```

## Capabilities
- Navigate to URLs and take screenshots
- Fill forms and click buttons
- Extract text and data from pages
- Wait for dynamic content to load
- Execute JavaScript in page context
- Handle authentication flows

## Usage Examples

### Screenshot a Page
```
"Take a screenshot of https://example.com"
```

### Extract Data
```
"Scrape the product prices from https://store.example.com/category"
```

### Fill a Form
```
"Go to the contact form on example.com and fill in name: John, email: john@example.com"
```

## Security Considerations
- Run browserless in a Docker container (sandboxed)
- Set `maxPages` to prevent resource exhaustion
- Use allowlists for permitted domains
- Enable request logging for audit trail
- Never store credentials in browser automation scripts
"""
    },

    # ------------------------------------------------------------------
    # 11. voice_tts
    # ------------------------------------------------------------------
    "voice_tts": {
        "title": "Voice & Text-to-Speech Guide",
        "content": """# Voice & Text-to-Speech Guide

## Overview
OpenClaw supports text-to-speech (TTS) for voice output and speech-to-text (STT) for voice input.

## TTS Configuration
```json
{
  "voice": {
    "tts": {
      "enabled": true,
      "provider": "openai",
      "model": "tts-1",
      "voice": "alloy",
      "speed": 1.0,
      "format": "mp3"
    }
  }
}
```

## Available Providers

### OpenAI TTS
- Models: `tts-1` (fast), `tts-1-hd` (quality)
- Voices: alloy, echo, fable, onyx, nova, shimmer
- Cost: $15/M characters (tts-1), $30/M characters (tts-1-hd)

### ElevenLabs
- Higher quality voice cloning
- More natural prosody
- Custom voice creation

### Local (Piper)
- Free, runs locally
- Lower quality but zero cost
- Good for high-volume usage

## STT Configuration
```json
{
  "voice": {
    "stt": {
      "enabled": true,
      "provider": "openai",
      "model": "whisper-1",
      "language": "en"
    }
  }
}
```

## Usage
```bash
# Install TTS skill
clawhub install tts

# Test voice output
openclaw tts "Hello, I am OpenClaw"

# In conversation
"Read this message aloud"
"Summarize this article and speak it"
```

## Channel Integration
- Telegram: Voice messages supported natively
- Discord: Voice channel integration via bot
- WhatsApp: Audio message support
"""
    },

    # ------------------------------------------------------------------
    # 12. docker_deployment
    # ------------------------------------------------------------------
    "docker_deployment": {
        "title": "Docker Deployment Guide",
        "content": """# Docker Deployment Guide

## Overview
OpenClaw can be deployed as Docker containers for isolation, easy updates, and reproducible environments.

## Docker Compose Setup

### docker-compose.yml
```yaml
version: '3.8'

services:
  openclaw:
    image: openclaw/server:latest
    container_name: openclaw-server
    ports:
      - "3100:3100"
    volumes:
      - ./config:/etc/openclaw:ro
      - ./data:/var/lib/openclaw
      - ./logs:/var/log/openclaw
    environment:
      - OPENROUTER_API_KEY=${OPENROUTER_API_KEY}
      - NODE_ENV=production
    restart: unless-stopped

  openclaw-gateway:
    image: openclaw/gateway:latest
    container_name: openclaw-gateway
    ports:
      - "3200:3200"
    depends_on:
      - openclaw
    environment:
      - OPENCLAW_SERVER_URL=http://openclaw:3100
      - NODE_ENV=production
    restart: unless-stopped

  # Optional: Qdrant for vector memory
  qdrant:
    image: qdrant/qdrant
    ports:
      - "6333:6333"
    volumes:
      - qdrant_data:/qdrant/storage

volumes:
  qdrant_data:
```

### .env File
```bash
OPENROUTER_API_KEY=sk-or-v1-your-key
```

## Deployment Steps
```bash
# 1. Create project directory
mkdir -p /opt/openclaw && cd /opt/openclaw

# 2. Create config
mkdir -p config data logs
openclaw config init --output ./config/openclaw.json

# 3. Start services
docker compose up -d

# 4. Check health
docker compose logs -f
curl http://localhost:3100/health
```

## With Traefik (HTTPS)
```yaml
services:
  openclaw-gateway:
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.openclaw.rule=Host(`ai.yourdomain.com`)"
      - "traefik.http.routers.openclaw.tls.certresolver=letsencrypt"
      - "traefik.http.services.openclaw.loadbalancer.server.port=3200"
```

## Updating
```bash
docker compose pull
docker compose up -d --force-recreate
```

## Backup
```bash
# Backup data and config
tar czf openclaw-backup-$(date +%Y%m%d).tar.gz config/ data/
```
"""
    },

    # ------------------------------------------------------------------
    # 13. cost_optimization
    # ------------------------------------------------------------------
    "cost_optimization": {
        "title": "Cost Optimization Guide",
        "content": """# Cost Optimization Guide

## Understanding Costs
OpenClaw costs come from three sources:
1. **AI Model usage** (via OpenRouter) — usually 80-95% of total cost
2. **Server hosting** — VPS or cloud instance
3. **Optional services** — vector DB, TTS, browser automation

## Model Cost Comparison (per 1M tokens)

| Model | Input | Output | Quality | Speed |
|-------|-------|--------|---------|-------|
| MiniMax M2.1 | $0.10 | $0.30 | Good | Fast |
| DeepSeek V3.2 | $0.14 | $0.28 | Very Good | Fast |
| Gemini Flash Lite | $0.02 | $0.10 | Good | Fastest |
| Kimi K2.5 | $0.60 | $2.00 | Excellent | Medium |
| Claude Sonnet 4 | $3.00 | $15.00 | Best | Medium |
| GPT-4.1 | $2.00 | $8.00 | Excellent | Medium |

## Cost-Saving Strategies

### 1. Smart Model Routing
Use cheap models for simple tasks, premium only when needed:
```json
{
  "openrouter": {
    "model": "deepseek/deepseek-v3-0324",
    "subAgentModel": "minimax/minimax-m2.1",
    "thinkingModel": "moonshotai/kimi-k2.5"
  }
}
```

### 2. Token Management
```json
{
  "openrouter": {
    "maxTokens": 4096,
    "maxContextTokens": 32000,
    "truncateStrategy": "smart"
  }
}
```

### 3. Caching
```json
{
  "cache": {
    "enabled": true,
    "ttl": 3600,
    "maxSize": "100MB",
    "strategy": "semantic"
  }
}
```

### 4. Rate Limiting
Prevent runaway costs:
```json
{
  "security": {
    "rateLimit": {
      "maxTokensPerDay": 500000,
      "maxCostPerDay": 5.00,
      "alertThreshold": 0.8
    }
  }
}
```

## Monthly Cost Estimates

| Usage Level | Model Tier | Est. Monthly Cost |
|-------------|-----------|------------------|
| Light (100 msgs/day) | Budget | $3–8 |
| Moderate (500 msgs/day) | Budget | $10–25 |
| Moderate (500 msgs/day) | Mid-range | $30–60 |
| Heavy (2000 msgs/day) | Budget | $40–80 |
| Heavy (2000 msgs/day) | Premium | $150–400 |

## Monitoring
```bash
# Check current usage
openclaw usage --today
openclaw usage --month

# OpenRouter dashboard
# https://openrouter.ai/activity
```
"""
    },

    # ------------------------------------------------------------------
    # 14. clawhub_skills_detailed
    # ------------------------------------------------------------------
    "clawhub_skills_detailed": {
        "title": "ClawHub Skill Pack Ecosystem",
        "content": """# ClawHub Skill Pack Ecosystem — Deep Dive

## What is ClawHub?
ClawHub is the official marketplace for OpenClaw skill packs. Skills extend your AI agent's capabilities with new tools, integrations, and workflows.

## Skill Categories

### Communication & Social
- `gmail` — Full Gmail integration (read, compose, search, labels)
- `calendar` — Google Calendar management
- `twitter` — Post, search, engage on Twitter/X
- `linkedin` — Profile management, post creation
- `youtube-comment-bot` — YouTube comment management

### Developer Tools
- `github` — Repo management, issues, PRs, actions
- `docker-manager` — Container lifecycle management
- `db-query` — PostgreSQL, MySQL, SQLite queries
- `api-tester` — HTTP endpoint testing with assertions
- `code-review` — Automated code review with AI

### Productivity
- `notion` — Notion page/database management
- `google-docs` — Document creation and editing
- `trello` — Board/card management
- `asana` — Task and project tracking
- `file-manager` — Local file operations

### Data & Analytics
- `web-scraper` — Intelligent web scraping
- `data-viz` — Chart and graph generation
- `spreadsheet` — Google Sheets / Excel operations
- `pdf-tools` — PDF generation, parsing, merging

### Media
- `tts` — Text-to-speech (OpenAI, ElevenLabs, Piper)
- `image-gen` — AI image generation (DALL-E, Midjourney)
- `transcribe` — Audio/video transcription (Whisper)
- `screenshot` — Web page screenshots

## Installing Skills
```bash
# Search for skills
clawhub search "email"

# Install
clawhub install gmail

# Install specific version
clawhub install gmail@2.1.0

# List installed
clawhub list

# Update all
clawhub update

# Remove
clawhub remove gmail
```

## Creating & Publishing Skills
```bash
# Initialize skill project
clawhub init my-skill

# Test locally
clawhub test ./my-skill

# Publish
clawhub publish ./my-skill
```

## Skill Manifest (package.json)
```json
{
  "name": "my-skill",
  "version": "1.0.0",
  "description": "What this skill does",
  "openclaw": {
    "minVersion": "1.0.0",
    "tools": ["tool1", "tool2"],
    "permissions": ["filesystem", "network"],
    "dependencies": []
  }
}
```
"""
    },

    # ------------------------------------------------------------------
    # 15. mcp_integration
    # ------------------------------------------------------------------
    "mcp_integration": {
        "title": "MCP Integration Guide",
        "content": """# MCP (Model Context Protocol) Integration Guide

## What is MCP?
MCP is a standardized protocol for connecting AI models to external tools and data sources. OpenClaw uses MCP to access the EasyClaw knowledge base and other MCP-compatible services.

## How OpenClaw Uses MCP
1. **EasyClaw Knowledge Hub** — Built-in MCP connection for installation help, troubleshooting, and configuration guidance
2. **Custom MCP Servers** — Connect to your own MCP servers for private tools

## Configuration
```json
{
  "mcp": {
    "servers": [
      {
        "name": "EasyClaw Knowledge Hub",
        "url": "https://mcp.easyclaw.help",
        "auth": {
          "type": "bearer",
          "token": "ec_live_your_api_key"
        }
      },
      {
        "name": "My Custom Tools",
        "url": "http://localhost:3001/mcp",
        "auth": { "type": "none" }
      }
    ],
    "timeout": 30,
    "retryOnFailure": true
  }
}
```

## Building an MCP Server

### Using FastMCP (Python)
```python
from fastmcp import FastMCP

mcp = FastMCP("My Server", stateless_http=True)

@mcp.tool()
def my_tool(query: str) -> str:
    \"\"\"Description of what this tool does\"\"\"
    return f"Result for: {query}"

# Run with: uvicorn server:app
app = mcp.streamable_http_app()
```

### Using MCP SDK (Node.js)
```javascript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';

const server = new Server({ name: 'my-server', version: '1.0.0' });
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{ name: 'my_tool', description: '...', inputSchema: {...} }]
}));
```

## Protocol Details
- Transport: Streamable HTTP (POST to `/mcp`)
- Format: JSON-RPC 2.0
- Methods: `initialize`, `tools/list`, `tools/call`
- Auth: Bearer token in Authorization header

## Troubleshooting MCP
```bash
# Check connection
curl -X POST https://mcp.easyclaw.help/mcp \\
  -H "Authorization: Bearer ec_live_xxx" \\
  -H "Content-Type: application/json" \\
  -d '{"jsonrpc":"2.0","method":"tools/list","id":"1"}'

# Check health
curl https://mcp.easyclaw.help/health
```
"""
    },

    # ------------------------------------------------------------------
    # 16. configuration_reference
    # ------------------------------------------------------------------
    "configuration_reference": {
        "title": "Configuration Reference",
        "content": """# OpenClaw Configuration Reference

## Config File Location
`/etc/openclaw/openclaw.json`

## Complete Field Reference

### Server Settings
```json
{
  "server": {
    "port": 3100,
    "host": "127.0.0.1",
    "workers": 1,
    "cors": {
      "enabled": true,
      "origins": ["*"]
    }
  }
}
```

### Gateway Settings
```json
{
  "gateway": {
    "port": 3200,
    "host": "127.0.0.1",
    "serverUrl": "http://127.0.0.1:3100",
    "timeout": 60000,
    "maxConnections": 100
  }
}
```

### OpenRouter Settings
```json
{
  "openrouter": {
    "apiKey": "sk-or-v1-...",
    "apiKeyFile": "/etc/openclaw/.api-key",
    "model": "moonshotai/kimi-k2.5",
    "subAgentModel": "minimax/minimax-m2.1",
    "thinkingModel": "moonshotai/kimi-k2.5",
    "maxTokens": 8192,
    "maxContextTokens": 128000,
    "temperature": 0.7,
    "topP": 0.9,
    "frequencyPenalty": 0.0,
    "presencePenalty": 0.0
  }
}
```

### Memory Settings
```json
{
  "memory": {
    "type": "file|vector|hybrid",
    "path": "/var/lib/openclaw/memory",
    "maxFileSize": "50KB",
    "autoSummarize": true,
    "dailyNotes": true,
    "vector": {
      "provider": "qdrant",
      "url": "http://localhost:6333",
      "collection": "openclaw_memory"
    }
  }
}
```

### Channel Settings
```json
{
  "channels": {
    "telegram": { "enabled": false, "token": "", "allowedUsers": [] },
    "discord": { "enabled": false, "token": "", "allowedGuilds": [] },
    "slack": { "enabled": false, "botToken": "", "signingSecret": "" },
    "whatsapp": { "enabled": false, "provider": "evolution-api" },
    "matrix": { "enabled": false, "homeserver": "" }
  }
}
```

### Security Settings
```json
{
  "security": {
    "rateLimit": {
      "enabled": true,
      "maxRequestsPerMinute": 30,
      "maxTokensPerDay": 1000000,
      "maxCostPerDay": 10.00
    },
    "inputValidation": {
      "maxMessageLength": 4000,
      "stripHtml": true,
      "contentFilter": "moderate"
    },
    "apiKeyRotation": {
      "enabled": false,
      "intervalDays": 90
    }
  }
}
```

### Automation Settings
```json
{
  "automation": {
    "memorySummarize": { "enabled": true, "schedule": "0 3 * * *" },
    "healthReport": { "enabled": false, "schedule": "0 */6 * * *" },
    "logRotation": { "enabled": true, "schedule": "0 0 * * 0" }
  }
}
```

### Logging Settings
```json
{
  "logging": {
    "level": "info|debug|warn|error",
    "file": "/var/log/openclaw/openclaw.log",
    "maxSize": "100MB",
    "rotate": true,
    "requests": true,
    "sensitiveDataMasking": true
  }
}
```

### Skills Settings
```json
{
  "skills": {
    "directory": "/var/lib/openclaw/skills",
    "autoUpdate": false,
    "allowedSkills": [],
    "blockedSkills": []
  }
}
```

## Environment Variables
| Variable | Purpose | Default |
|----------|---------|---------|
| `OPENROUTER_API_KEY` | OpenRouter API key | — |
| `OPENCLAW_CONFIG` | Config file path | `/etc/openclaw/openclaw.json` |
| `OPENCLAW_DATA` | Data directory | `/var/lib/openclaw` |
| `OPENCLAW_LOG_LEVEL` | Log level override | `info` |
| `NODE_ENV` | Environment | `production` |
"""
    },

    # ------------------------------------------------------------------
    # 17. onboarding_wizard
    # ------------------------------------------------------------------
    "onboarding_wizard": {
        "title": "First-Run Onboarding Wizard",
        "content": """# First-Run Onboarding Wizard

## Overview
The onboarding wizard runs automatically on first launch. It guides you through essential configuration in 5 steps.

## Step 1: API Key Setup
```
Welcome to OpenClaw!
Let's get you set up.

Enter your OpenRouter API key:
(Get one at https://openrouter.ai/keys)
> sk-or-v1-_______________________________
```

The wizard validates the key against the OpenRouter API before proceeding.

## Step 2: Model Selection
```
Choose your AI model:

1. Budget    — DeepSeek V3.2 (~$5/month)
2. Balanced  — Kimi K2.5 (~$15/month)
3. Premium   — Claude Sonnet 4 (~$50/month)
4. Custom    — Enter model ID manually

> 2
```

## Step 3: Channel Setup
```
Connect a messaging channel:

1. Telegram (recommended)
2. Discord
3. Slack
4. WhatsApp
5. Skip for now

> 1

Enter your Telegram bot token:
(Create one via @BotFather)
> 1234567890:ABCdefGHIjklMNO
```

## Step 4: Personality
```
Name your AI assistant:
> Johnny 5

Personality style:
1. Professional — Formal, concise
2. Friendly     — Casual, helpful
3. Technical    — Detailed, precise
4. Custom       — Write your own SOUL.md

> 2
```

## Step 5: Confirmation
```
Configuration Summary:
  Model:      Kimi K2.5
  Channel:    Telegram
  Assistant:  Johnny 5 (Friendly)
  Memory:     File-based (default)

Save and start OpenClaw? [Y/n]
> Y

Starting OpenClaw...
  ✓ Configuration saved
  ✓ Workspace initialized
  ✓ Gateway started
  ✓ Telegram connected
  ✓ Health check passed

OpenClaw is running! Send a message to your Telegram bot to get started.
```

## Re-Running the Wizard
```bash
openclaw configure --wizard
```

## Non-Interactive Mode
```bash
openclaw config init --non-interactive \\
  --model "moonshotai/kimi-k2.5" \\
  --channel telegram \\
  --telegram-token "BOT_TOKEN"
```
"""
    },

    # ------------------------------------------------------------------
    # 18. easyclaw_code_guide
    # ------------------------------------------------------------------
    "easyclaw_code_guide": {
        "title": "EasyClaw Development Patterns",
        "content": """# EasyClaw-Specific Development Patterns

## Overview
This guide covers development patterns for extending OpenClaw and the EasyClaw installer.

## EasyClaw Installer Architecture

### Concurrency Model
| Component | Thread | Why |
|-----------|--------|-----|
| Textual UI | Main asyncio loop | UI must own the event loop |
| StepRunner | Main loop (async) | Uses asyncio subprocess |
| Agent Brain | Background thread | HTTP streaming + file I/O |

### Key Rules
1. **Agent Brain is 100% synchronous** — uses `httpx.Client`, `subprocess.run`
2. **UI updates from agent**: `app.call_from_thread(app.post_message, msg)`
3. **Cross-thread tool execution**: `asyncio.run_coroutine_threadsafe()` from agent → main
4. **Never name methods `_render()`** — conflicts with Textual internals

### Step Implementation Pattern
```python
class MyStep(BaseStep):
    name = "My Step"
    description = "What this step does"
    timeout = 120
    allow_retry = True
    max_retries = 2

    def validate_preconditions(self) -> tuple[bool, str]:
        import shutil
        if not shutil.which("required_binary"):
            return False, "required_binary not found"
        return True, ""

    def command(self) -> str:
        return (
            "set -e\\n"
            "echo 'Running my step...'\\n"
            "my-command --flag\\n"
            "echo 'Done.'"
        )
```

## OpenClaw CLI Development

### Adding a New Subcommand
1. Create `lib/mycommand.js` with an `async function run(args)` export
2. Add case to `bin/openclaw.js` switch statement
3. Add to package.json bin if it needs a standalone entry point

### Module Pattern
```javascript
const fs = require('fs');
const path = require('path');

async function run(args) {
    const subcommand = args[0];
    switch (subcommand) {
        case 'action':
            return doAction(args.slice(1));
        default:
            console.error(`Unknown subcommand: ${subcommand}`);
            process.exit(1);
    }
}

// Guard for direct execution via systemd
if (require.main === module) {
    run(process.argv.slice(2));
}

module.exports = { run };
```

## MCP Tool Development

### Adding a New MCP Tool
```python
@mcp.tool()
def my_new_tool(param: str, optional: str = "default") -> str:
    \"\"\"Description shown to AI agents\"\"\"
    start = time.time()
    auth = _require_auth("my_new_tool", "starter")

    # Tool logic here
    result = do_something(param, optional)

    log_tool_usage(auth, "my_new_tool", int((time.time() - start) * 1000))
    return result
```

## Testing Patterns

### Headless TUI Testing
```python
async def main():
    app = EasyClawApp(api_key="sk-or-v1-...")
    async with app.run_test(size=(120, 40)) as pilot:
        await asyncio.sleep(12)
        app.save_screenshot("/tmp/test.svg")
```

### VPS Deployment Testing
```bash
# Clean artifacts
systemctl stop openclaw openclaw-gateway
rm -rf /etc/openclaw /var/lib/openclaw /var/log/openclaw
npm uninstall -g @openclaw/cli @openclaw/mcp-tools
systemctl daemon-reload

# Reinstall and test
pip install -e /opt/easyclaw-installer
python -m easyclaw_installer --api-key "$(cat /root/.easyclaw/agent.json | python3 -c 'import json,sys; print(json.load(sys.stdin)["openrouter_key"])')"
```
"""
    },
}


# ===================================================================
# FTS5 Index Management
# ===================================================================

def _rebuild_knowledge_fts():
    """Merge static KNOWLEDGE_BASE with dynamic overrides and rebuild FTS5."""
    conn = _get_conn()
    try:
        # Merge: base dict + dynamic table overrides
        combined = {}
        for topic, data in KNOWLEDGE_BASE.items():
            combined[topic] = (topic, data["title"], data["content"])

        # Dynamic overrides (latest wins)
        rows = conn.execute(
            "SELECT topic, title, content FROM knowledge_entries"
        ).fetchall()
        for row in rows:
            combined[row["topic"]] = (row["topic"], row["title"], row["content"])

        # Check if rebuild needed
        existing_count = conn.execute(
            "SELECT COUNT(*) as cnt FROM knowledge_fts"
        ).fetchone()["cnt"]

        if existing_count != len(combined):
            conn.execute("DELETE FROM knowledge_fts")
            for entry in combined.values():
                conn.execute(
                    "INSERT INTO knowledge_fts (topic, title, content) VALUES (?, ?, ?)",
                    entry,
                )
            conn.commit()
    finally:
        conn.close()


def search_knowledge_fts(query: str, limit: int = 5) -> list[dict]:
    """Full-text search with FTS5 + LIKE fallback."""
    conn = _get_conn()
    try:
        try:
            rows = conn.execute(
                """
                SELECT topic, title, content, rank
                FROM knowledge_fts
                WHERE knowledge_fts MATCH ?
                ORDER BY rank
                LIMIT ?
                """,
                (query, limit),
            ).fetchall()
        except sqlite3.OperationalError:
            # FTS5 syntax error (query has operators) — fall back to LIKE
            pattern = f"%{query}%"
            rows = conn.execute(
                """
                SELECT topic, title, content
                FROM knowledge_fts
                WHERE content LIKE ? OR title LIKE ? OR topic LIKE ?
                LIMIT ?
                """,
                (pattern, pattern, pattern, limit),
            ).fetchall()
        return [dict(r) for r in rows]
    finally:
        conn.close()


def get_guide(topic: str) -> dict | None:
    """Get a guide by topic key — checks dynamic overrides first, then static."""
    conn = _get_conn()
    try:
        # Check dynamic override
        row = conn.execute(
            "SELECT topic, title, content FROM knowledge_entries WHERE topic = ?",
            (topic,),
        ).fetchone()
        if row:
            return dict(row)
    finally:
        conn.close()

    # Fall back to static
    if topic in KNOWLEDGE_BASE:
        return {
            "topic": topic,
            "title": KNOWLEDGE_BASE[topic]["title"],
            "content": KNOWLEDGE_BASE[topic]["content"],
        }
    return None


def list_guide_topics() -> list[dict]:
    """Return all available guide topics (static + dynamic)."""
    topics = {}
    # Static first
    for topic, data in KNOWLEDGE_BASE.items():
        topics[topic] = {"topic": topic, "title": data["title"]}
    # Dynamic overrides
    conn = _get_conn()
    try:
        rows = conn.execute(
            "SELECT topic, title FROM knowledge_entries"
        ).fetchall()
        for row in rows:
            topics[row["topic"]] = {"topic": row["topic"], "title": row["title"]}
    finally:
        conn.close()
    return list(topics.values())


# ===================================================================
# User Management
# ===================================================================

def create_user(email: str) -> dict:
    """Create a user account. Returns existing user if email already registered."""
    conn = _get_conn()
    try:
        existing = conn.execute(
            "SELECT id, email, created_at FROM users WHERE email = ?",
            (email,),
        ).fetchone()
        if existing:
            return dict(existing)

        user_id = uuid.uuid4().hex
        conn.execute(
            "INSERT INTO users (id, email) VALUES (?, ?)",
            (user_id, email),
        )
        conn.commit()
        return {"id": user_id, "email": email}
    finally:
        conn.close()


def get_user_by_email(email: str) -> dict | None:
    """Look up a user by email."""
    conn = _get_conn()
    try:
        row = conn.execute(
            "SELECT id, email, created_at FROM users WHERE email = ?",
            (email,),
        ).fetchone()
        return dict(row) if row else None
    finally:
        conn.close()


# ===================================================================
# Subscription Management
# ===================================================================

DAILY_LIMITS = {
    "starter": 100,
    "pro": 1000,
    "business": 999999,
    "subscriber": 5000,
}


def hash_api_key(api_key: str) -> str:
    """SHA-256 hash an API key."""
    import hashlib
    return hashlib.sha256(api_key.encode()).hexdigest()


def create_subscription(
    user_id: str,
    tier: str,
    api_key: str,
    stripe_customer_id: str | None = None,
    stripe_subscription_id: str | None = None,
    current_period_end: str | None = None,
) -> dict:
    """Create subscription. Returns existing active subscription if one exists."""
    conn = _get_conn()
    try:
        # Check for existing active subscription
        existing = conn.execute(
            """
            SELECT s.*, u.email FROM subscriptions s
            JOIN users u ON s.user_id = u.id
            WHERE s.user_id = ? AND s.status = 'active'
            ORDER BY s.created_at DESC LIMIT 1
            """,
            (user_id,),
        ).fetchone()
        if existing:
            return dict(existing)

        sub_id = f"sub_{uuid.uuid4().hex[:12]}"
        key_hash = hash_api_key(api_key)
        daily_limit = DAILY_LIMITS.get(tier, 100)

        conn.execute(
            """
            INSERT INTO subscriptions
                (id, user_id, stripe_customer_id, stripe_subscription_id,
                 tier, status, api_key, api_key_hash, daily_limit, current_period_end)
            VALUES (?, ?, ?, ?, ?, 'active', ?, ?, ?, ?)
            """,
            (sub_id, user_id, stripe_customer_id, stripe_subscription_id,
             tier, api_key, key_hash, daily_limit, current_period_end),
        )
        conn.commit()

        return {
            "id": sub_id,
            "user_id": user_id,
            "tier": tier,
            "status": "active",
            "api_key": api_key,
            "daily_limit": daily_limit,
        }
    finally:
        conn.close()


def get_subscription_by_key_hash(key_hash: str) -> dict | None:
    """Look up subscription by hashed API key. Includes user email via JOIN."""
    conn = _get_conn()
    try:
        row = conn.execute(
            """
            SELECT s.id, s.user_id, s.tier, s.status, s.daily_limit,
                   s.current_period_end, s.api_key_hash, u.email
            FROM subscriptions s
            JOIN users u ON s.user_id = u.id
            WHERE s.api_key_hash = ?
            """,
            (key_hash,),
        ).fetchone()
        return dict(row) if row else None
    finally:
        conn.close()


def update_subscription_status(
    api_key_hash: str | None = None,
    stripe_subscription_id: str | None = None,
    status: str = "active",
) -> bool:
    """Update subscription status by key hash or Stripe ID."""
    conn = _get_conn()
    try:
        if api_key_hash:
            result = conn.execute(
                "UPDATE subscriptions SET status = ? WHERE api_key_hash = ?",
                (status, api_key_hash),
            )
        elif stripe_subscription_id:
            result = conn.execute(
                "UPDATE subscriptions SET status = ? WHERE stripe_subscription_id = ?",
                (status, stripe_subscription_id),
            )
        else:
            return False
        conn.commit()
        return result.rowcount > 0
    finally:
        conn.close()


# ===================================================================
# Usage Logging & Rate Limiting
# ===================================================================

def log_usage(api_key_hash: str, tool_name: str,
              response_time_ms: int = 0, success: bool = True):
    """Record a tool usage event."""
    conn = _get_conn()
    try:
        conn.execute(
            """
            INSERT INTO usage_logs (api_key_hash, tool_name, response_time_ms, success)
            VALUES (?, ?, ?, ?)
            """,
            (api_key_hash, tool_name, response_time_ms, int(success)),
        )
        conn.commit()
    finally:
        conn.close()


def get_daily_usage(api_key_hash: str) -> int:
    """Count today's tool calls for rate limiting."""
    conn = _get_conn()
    try:
        row = conn.execute(
            """
            SELECT COUNT(*) as cnt FROM usage_logs
            WHERE api_key_hash = ? AND date(timestamp) = date('now')
            """,
            (api_key_hash,),
        ).fetchone()
        return row["cnt"] if row else 0
    finally:
        conn.close()


def get_usage_stats(api_key_hash: str, days: int = 30) -> dict:
    """Get usage statistics for the last N days."""
    conn = _get_conn()
    try:
        total = conn.execute(
            """
            SELECT COUNT(*) as cnt FROM usage_logs
            WHERE api_key_hash = ?
              AND timestamp >= datetime('now', ?)
            """,
            (api_key_hash, f"-{days} days"),
        ).fetchone()["cnt"]

        by_tool = conn.execute(
            """
            SELECT tool_name, COUNT(*) as cnt,
                   AVG(response_time_ms) as avg_time
            FROM usage_logs
            WHERE api_key_hash = ?
              AND timestamp >= datetime('now', ?)
            GROUP BY tool_name
            ORDER BY cnt DESC
            """,
            (api_key_hash, f"-{days} days"),
        ).fetchall()

        return {
            "total_calls": total,
            "period_days": days,
            "by_tool": [dict(r) for r in by_tool],
        }
    finally:
        conn.close()


# ===================================================================
# Knowledge Entry Management (Dynamic Overrides)
# ===================================================================

def upsert_knowledge_entry(topic: str, title: str, content: str,
                           source: str = "manual") -> dict:
    """Create or update a dynamic knowledge entry."""
    conn = _get_conn()
    try:
        conn.execute(
            """
            INSERT INTO knowledge_entries (topic, title, content, source, updated_at)
            VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
            ON CONFLICT(topic) DO UPDATE SET
                title = excluded.title,
                content = excluded.content,
                source = excluded.source,
                updated_at = CURRENT_TIMESTAMP
            """,
            (topic, title, content, source),
        )
        conn.commit()
        return {"topic": topic, "title": title, "source": source}
    finally:
        conn.close()


def list_knowledge_entries() -> list[dict]:
    """List all dynamic knowledge entries."""
    conn = _get_conn()
    try:
        rows = conn.execute(
            "SELECT topic, title, source, updated_at FROM knowledge_entries ORDER BY topic"
        ).fetchall()
        return [dict(r) for r in rows]
    finally:
        conn.close()


def delete_knowledge_entry(topic: str) -> bool:
    """Delete a dynamic knowledge entry."""
    conn = _get_conn()
    try:
        result = conn.execute(
            "DELETE FROM knowledge_entries WHERE topic = ?",
            (topic,),
        )
        conn.commit()
        return result.rowcount > 0
    finally:
        conn.close()


# ===================================================================
# OTP Management
# ===================================================================

def generate_otp(email: str, ttl_seconds: int = 600) -> str:
    """Generate a 6-digit OTP with configurable TTL. Invalidates previous codes."""
    conn = _get_conn()
    try:
        # Invalidate previous unused codes
        conn.execute(
            "UPDATE otp_codes SET used = 1 WHERE email = ? AND used = 0",
            (email,),
        )

        code = f"{random.randint(0, 999999):06d}"
        now = datetime.now(timezone.utc).isoformat()
        expires = datetime.fromtimestamp(
            datetime.now(timezone.utc).timestamp() + ttl_seconds,
            tz=timezone.utc,
        ).isoformat()

        conn.execute(
            """
            INSERT INTO otp_codes (email, code, created_at, expires_at)
            VALUES (?, ?, ?, ?)
            """,
            (email, code, now, expires),
        )
        conn.commit()
        return code
    finally:
        conn.close()


def verify_otp(email: str, code: str) -> bool:
    """Verify and consume an OTP. Returns True if valid."""
    conn = _get_conn()
    try:
        now = datetime.now(timezone.utc).isoformat()
        row = conn.execute(
            """
            SELECT rowid FROM otp_codes
            WHERE email = ? AND code = ? AND used = 0 AND expires_at > ?
            ORDER BY created_at DESC LIMIT 1
            """,
            (email, code, now),
        ).fetchone()

        if row:
            conn.execute(
                "UPDATE otp_codes SET used = 1 WHERE rowid = ?",
                (row["rowid"],),
            )
            conn.commit()
            return True
        return False
    finally:
        conn.close()


# ===================================================================
# Guide Purchases
# ===================================================================

def create_guide_purchase(email: str, guide_type: str,
                          stripe_session_id: str | None = None) -> dict:
    """Record a guide purchase and generate a download token."""
    download_token = uuid.uuid4().hex
    conn = _get_conn()
    try:
        conn.execute(
            """
            INSERT INTO guide_purchases (email, guide_type, download_token, stripe_session_id)
            VALUES (?, ?, ?, ?)
            """,
            (email, guide_type, download_token, stripe_session_id),
        )
        conn.commit()
        return {
            "email": email,
            "guide_type": guide_type,
            "download_token": download_token,
        }
    finally:
        conn.close()


def validate_download_token(token: str) -> dict | None:
    """Validate a download token and mark as downloaded."""
    conn = _get_conn()
    try:
        row = conn.execute(
            """
            SELECT id, email, guide_type, download_token, downloaded_at
            FROM guide_purchases WHERE download_token = ?
            """,
            (token,),
        ).fetchone()

        if not row:
            return None

        result = dict(row)

        # Mark as downloaded if first time
        if not result.get("downloaded_at"):
            conn.execute(
                "UPDATE guide_purchases SET downloaded_at = CURRENT_TIMESTAMP WHERE download_token = ?",
                (token,),
            )
            conn.commit()
            result["first_download"] = True
        else:
            result["first_download"] = False

        return result
    finally:
        conn.close()


# ===================================================================
# Lookup (aggregated)
# ===================================================================

def lookup_by_email(email: str) -> dict:
    """Look up all data associated with an email address."""
    conn = _get_conn()
    try:
        user = conn.execute(
            "SELECT id, email, created_at FROM users WHERE email = ?",
            (email,),
        ).fetchone()

        subs = []
        if user:
            rows = conn.execute(
                """
                SELECT id, tier, status, daily_limit, current_period_end, created_at
                FROM subscriptions WHERE user_id = ?
                ORDER BY created_at DESC
                """,
                (user["id"],),
            ).fetchall()
            subs = [dict(r) for r in rows]

        purchases = conn.execute(
            """
            SELECT guide_type, download_token, created_at, downloaded_at
            FROM guide_purchases WHERE email = ?
            ORDER BY created_at DESC
            """,
            (email,),
        ).fetchall()

        return {
            "user": dict(user) if user else None,
            "subscriptions": subs,
            "guide_purchases": [dict(r) for r in purchases],
        }
    finally:
        conn.close()
