Merge pull request #1 from agenciaspace/claude/hostinger-remote-deployment-TGVof
feat: add Hostinger VPS remote deployment tooling
This commit is contained in:
commit
300391cd1a
25 changed files with 4278 additions and 7 deletions
234
.github/workflows/deploy-hostinger.yml
vendored
Normal file
234
.github/workflows/deploy-hostinger.yml
vendored
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
name: Deploy to Hostinger VPS
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main", "claude/hostinger-remote-deployment-*"]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REMOTE_DIR: /opt/picoclaw
|
||||
DEPLOY_METHOD: docker
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy to Hostinger
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install sshpass
|
||||
run: sudo apt-get update -qq && sudo apt-get install -y -qq sshpass rsync
|
||||
|
||||
- name: Verify SSH connection
|
||||
env:
|
||||
SSHPASS: ${{ secrets.HOSTINGER_SSH_PASSWORD }}
|
||||
run: |
|
||||
sshpass -e ssh \
|
||||
-o StrictHostKeyChecking=accept-new \
|
||||
-o ConnectTimeout=15 \
|
||||
-p ${{ secrets.HOSTINGER_SSH_PORT || '22' }} \
|
||||
${{ secrets.HOSTINGER_SSH_USER || 'root' }}@${{ secrets.HOSTINGER_HOST }} \
|
||||
"echo 'SSH connection OK'"
|
||||
|
||||
- name: Sync project files to server
|
||||
env:
|
||||
SSHPASS: ${{ secrets.HOSTINGER_SSH_PASSWORD }}
|
||||
run: |
|
||||
SSH_PORT="${{ secrets.HOSTINGER_SSH_PORT }}"
|
||||
SSH_PORT="${SSH_PORT:-22}"
|
||||
SSH_USER="${{ secrets.HOSTINGER_SSH_USER }}"
|
||||
SSH_USER="${SSH_USER:-root}"
|
||||
|
||||
rsync -az --delete \
|
||||
-e "sshpass -e ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=15 -p ${SSH_PORT}" \
|
||||
--exclude '.git' \
|
||||
--exclude 'build/' \
|
||||
--exclude '.env' \
|
||||
--exclude 'node_modules/' \
|
||||
--exclude '.deploy.env' \
|
||||
./ "${SSH_USER}@${{ secrets.HOSTINGER_HOST }}:${REMOTE_DIR}/src/"
|
||||
|
||||
- name: Copy Docker Compose production config
|
||||
env:
|
||||
SSHPASS: ${{ secrets.HOSTINGER_SSH_PASSWORD }}
|
||||
run: |
|
||||
SSH_PORT="${{ secrets.HOSTINGER_SSH_PORT }}"
|
||||
SSH_PORT="${SSH_PORT:-22}"
|
||||
SSH_USER="${{ secrets.HOSTINGER_SSH_USER }}"
|
||||
SSH_USER="${SSH_USER:-root}"
|
||||
|
||||
sshpass -e scp \
|
||||
-o StrictHostKeyChecking=accept-new \
|
||||
-o ConnectTimeout=15 \
|
||||
-P "${SSH_PORT}" \
|
||||
deploy/hostinger/docker-compose.production.yml \
|
||||
"${SSH_USER}@${{ secrets.HOSTINGER_HOST }}:${REMOTE_DIR}/docker-compose.yml"
|
||||
|
||||
- name: Configure environment variables
|
||||
env:
|
||||
SSHPASS: ${{ secrets.HOSTINGER_SSH_PASSWORD }}
|
||||
run: |
|
||||
SSH_PORT="${{ secrets.HOSTINGER_SSH_PORT }}"
|
||||
SSH_PORT="${SSH_PORT:-22}"
|
||||
SSH_USER="${{ secrets.HOSTINGER_SSH_USER }}"
|
||||
SSH_USER="${SSH_USER:-root}"
|
||||
|
||||
sshpass -e ssh \
|
||||
-o StrictHostKeyChecking=accept-new \
|
||||
-o ConnectTimeout=15 \
|
||||
-p "${SSH_PORT}" \
|
||||
"${SSH_USER}@${{ secrets.HOSTINGER_HOST }}" bash -s <<EOF
|
||||
set -e
|
||||
ENV_FILE="/opt/picoclaw/config/.env"
|
||||
CONFIG_FILE="/opt/picoclaw/config/config.json"
|
||||
|
||||
mkdir -p /opt/picoclaw/config
|
||||
touch "\$ENV_FILE"
|
||||
|
||||
set_env_var() {
|
||||
local key="\$1"
|
||||
local value="\$2"
|
||||
if grep -q "^\${key}=" "\$ENV_FILE" 2>/dev/null; then
|
||||
sed -i "s|^\${key}=.*|\${key}=\${value}|" "\$ENV_FILE"
|
||||
else
|
||||
echo "\${key}=\${value}" >> "\$ENV_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# Telegram Bot Token + Enable
|
||||
TELEGRAM_TOKEN="${{ secrets.PICOCLAW_TELEGRAM_BOT_TOKEN }}"
|
||||
if [ -n "\$TELEGRAM_TOKEN" ]; then
|
||||
set_env_var "PICOCLAW_CHANNELS_TELEGRAM_TOKEN" "\$TELEGRAM_TOKEN"
|
||||
set_env_var "PICOCLAW_CHANNELS_TELEGRAM_ENABLED" "true"
|
||||
echo "Telegram: token and enabled flag configured"
|
||||
else
|
||||
echo "WARNING: PICOCLAW_TELEGRAM_BOT_TOKEN secret is empty - Telegram will not start"
|
||||
fi
|
||||
|
||||
# Anthropic API Key
|
||||
ANTHROPIC_KEY="${{ secrets.ANTHROPIC_API_KEY }}"
|
||||
if [ -n "\$ANTHROPIC_KEY" ]; then
|
||||
set_env_var "ANTHROPIC_API_KEY" "\$ANTHROPIC_KEY"
|
||||
echo "Anthropic: API key configured"
|
||||
fi
|
||||
|
||||
# Create config.json if it doesn't exist (copy from example)
|
||||
if [ ! -f "\$CONFIG_FILE" ]; then
|
||||
if [ -f "/opt/picoclaw/src/config/config.example.json" ]; then
|
||||
cp /opt/picoclaw/src/config/config.example.json "\$CONFIG_FILE"
|
||||
echo "Created config.json from example template"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Verify critical config
|
||||
echo ""
|
||||
echo "=== Deploy Config Verification ==="
|
||||
echo "ENV file exists: \$(test -f "\$ENV_FILE" && echo YES || echo NO)"
|
||||
echo "Config file exists: \$(test -f "\$CONFIG_FILE" && echo YES || echo NO)"
|
||||
echo "Telegram enabled: \$(grep -c 'PICOCLAW_CHANNELS_TELEGRAM_ENABLED=true' "\$ENV_FILE" 2>/dev/null || echo 0)"
|
||||
echo "Telegram token set: \$(grep -c 'PICOCLAW_CHANNELS_TELEGRAM_TOKEN=' "\$ENV_FILE" 2>/dev/null || echo 0)"
|
||||
echo "Anthropic key set: \$(grep -c 'ANTHROPIC_API_KEY=' "\$ENV_FILE" 2>/dev/null || echo 0)"
|
||||
echo "=================================="
|
||||
EOF
|
||||
|
||||
- name: Build and restart Docker container
|
||||
env:
|
||||
SSHPASS: ${{ secrets.HOSTINGER_SSH_PASSWORD }}
|
||||
run: |
|
||||
SSH_PORT="${{ secrets.HOSTINGER_SSH_PORT }}"
|
||||
SSH_PORT="${SSH_PORT:-22}"
|
||||
SSH_USER="${{ secrets.HOSTINGER_SSH_USER }}"
|
||||
SSH_USER="${SSH_USER:-root}"
|
||||
|
||||
sshpass -e ssh \
|
||||
-o StrictHostKeyChecking=accept-new \
|
||||
-o ConnectTimeout=15 \
|
||||
-p "${SSH_PORT}" \
|
||||
"${SSH_USER}@${{ secrets.HOSTINGER_HOST }}" <<'EOF'
|
||||
set -e
|
||||
cd /opt/picoclaw
|
||||
|
||||
echo "Building Docker image..."
|
||||
docker compose build picoclaw-gateway
|
||||
|
||||
echo "Stopping existing container..."
|
||||
docker compose down --timeout 30 2>/dev/null || true
|
||||
|
||||
echo "Starting PicoClaw gateway..."
|
||||
docker compose up -d picoclaw-gateway
|
||||
|
||||
echo "Waiting for container to start..."
|
||||
sleep 5
|
||||
|
||||
if docker compose ps picoclaw-gateway 2>/dev/null | grep -qE "Up|running"; then
|
||||
echo "Container is running!"
|
||||
docker compose ps
|
||||
echo ""
|
||||
echo "=== Recent logs ==="
|
||||
docker compose logs --tail=15 picoclaw-gateway 2>&1 || true
|
||||
else
|
||||
echo "Container FAILED to start!"
|
||||
docker compose ps
|
||||
echo ""
|
||||
echo "Container logs:"
|
||||
docker compose logs --tail=50 picoclaw-gateway
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker image prune -f >/dev/null 2>&1
|
||||
echo "Docker deploy complete"
|
||||
EOF
|
||||
|
||||
- name: Ensure Tailscale serve is active
|
||||
env:
|
||||
SSHPASS: ${{ secrets.HOSTINGER_SSH_PASSWORD }}
|
||||
run: |
|
||||
SSH_PORT="${{ secrets.HOSTINGER_SSH_PORT }}"
|
||||
SSH_PORT="${SSH_PORT:-22}"
|
||||
SSH_USER="${{ secrets.HOSTINGER_SSH_USER }}"
|
||||
SSH_USER="${SSH_USER:-root}"
|
||||
|
||||
sshpass -e ssh \
|
||||
-o StrictHostKeyChecking=accept-new \
|
||||
-o ConnectTimeout=15 \
|
||||
-p "${SSH_PORT}" \
|
||||
"${SSH_USER}@${{ secrets.HOSTINGER_HOST }}" <<'EOF'
|
||||
if command -v tailscale &>/dev/null && tailscale status &>/dev/null 2>&1; then
|
||||
tailscale serve --bg http://localhost:18790 2>/dev/null || true
|
||||
echo "Tailscale serve active on tailnet"
|
||||
tailscale ip -4 2>/dev/null && echo "(use the IP above or tailnet hostname to access)"
|
||||
else
|
||||
echo "::warning::Tailscale not installed or not authenticated. Run setup-server.sh first."
|
||||
fi
|
||||
EOF
|
||||
|
||||
- name: Health check
|
||||
env:
|
||||
SSHPASS: ${{ secrets.HOSTINGER_SSH_PASSWORD }}
|
||||
run: |
|
||||
SSH_PORT="${{ secrets.HOSTINGER_SSH_PORT }}"
|
||||
SSH_PORT="${SSH_PORT:-22}"
|
||||
SSH_USER="${{ secrets.HOSTINGER_SSH_USER }}"
|
||||
SSH_USER="${SSH_USER:-root}"
|
||||
|
||||
HEALTH_OK=false
|
||||
for i in 1 2 3 4 5; do
|
||||
sleep 3
|
||||
if sshpass -e ssh \
|
||||
-o StrictHostKeyChecking=accept-new \
|
||||
-o ConnectTimeout=15 \
|
||||
-p "${SSH_PORT}" \
|
||||
"${SSH_USER}@${{ secrets.HOSTINGER_HOST }}" \
|
||||
"curl -sf http://localhost:18790/health" 2>/dev/null; then
|
||||
HEALTH_OK=true
|
||||
echo ""
|
||||
echo "Health check PASSED!"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for gateway to start... (attempt ${i}/5)"
|
||||
done
|
||||
|
||||
if [ "${HEALTH_OK}" = false ]; then
|
||||
echo "::warning::Health check did not pass after 5 attempts. The service may still be starting up."
|
||||
fi
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -24,6 +24,7 @@ build/
|
|||
|
||||
# Secrets & Config (keep templates, ignore actual secrets)
|
||||
.env
|
||||
.deploy.env
|
||||
config/config.json
|
||||
|
||||
# Test
|
||||
|
|
|
|||
73
Makefile
73
Makefile
|
|
@ -1,4 +1,4 @@
|
|||
.PHONY: all build install uninstall clean help test
|
||||
.PHONY: all build install uninstall clean help test deploy-hostinger deploy-hostinger-full deploy-hostinger-setup deploy-hostinger-status deploy-hostinger-rollback
|
||||
|
||||
# Build variables
|
||||
BINARY_NAME=picoclaw
|
||||
|
|
@ -151,6 +151,65 @@ check: deps fmt vet test
|
|||
run: build
|
||||
@$(BUILD_DIR)/$(BINARY_NAME) $(ARGS)
|
||||
|
||||
# ── Hostinger Deployment ─────────────────────────────
|
||||
HOSTINGER_HOST?=
|
||||
HOSTINGER_USER?=root
|
||||
HOSTINGER_SSH_KEY?=$(HOME)/.ssh/id_rsa
|
||||
HOSTINGER_SSH_PORT?=22
|
||||
HOSTINGER_DEPLOY_METHOD?=docker
|
||||
|
||||
## deploy-hostinger-full: Full deploy (setup + config + build + start) in one command
|
||||
deploy-hostinger-full:
|
||||
@bash deploy/hostinger/full-deploy.sh \
|
||||
-h "$(HOSTINGER_HOST)" \
|
||||
-u "$(HOSTINGER_USER)" \
|
||||
-k "$(HOSTINGER_SSH_KEY)" \
|
||||
-p "$(HOSTINGER_SSH_PORT)" \
|
||||
-m "$(HOSTINGER_DEPLOY_METHOD)"
|
||||
|
||||
## deploy-hostinger-setup: Run initial server setup on Hostinger VPS
|
||||
deploy-hostinger-setup:
|
||||
@bash deploy/hostinger/setup-server.sh
|
||||
|
||||
## setup-telegram: Interactive setup for Telegram bot integration
|
||||
setup-telegram:
|
||||
@bash deploy/hostinger/setup-telegram.sh
|
||||
|
||||
## setup-tailscale: Interactive setup for Tailscale secure access
|
||||
setup-tailscale:
|
||||
@bash deploy/hostinger/setup-tailscale.sh
|
||||
|
||||
## sync-dev: Sync with development branch (claude/hostinger-remote-deployment-TGVof)
|
||||
sync-dev:
|
||||
@bash deploy/sync-dev.sh
|
||||
|
||||
## deploy-hostinger: Deploy PicoClaw to Hostinger VPS
|
||||
deploy-hostinger:
|
||||
@bash deploy/hostinger/deploy.sh \
|
||||
-h "$(HOSTINGER_HOST)" \
|
||||
-u "$(HOSTINGER_USER)" \
|
||||
-k "$(HOSTINGER_SSH_KEY)" \
|
||||
-p "$(HOSTINGER_SSH_PORT)" \
|
||||
-m "$(HOSTINGER_DEPLOY_METHOD)"
|
||||
|
||||
## deploy-hostinger-status: Check PicoClaw status on Hostinger VPS
|
||||
deploy-hostinger-status:
|
||||
@bash deploy/hostinger/status.sh \
|
||||
-h "$(HOSTINGER_HOST)" \
|
||||
-u "$(HOSTINGER_USER)" \
|
||||
-k "$(HOSTINGER_SSH_KEY)" \
|
||||
-p "$(HOSTINGER_SSH_PORT)" \
|
||||
-m "$(HOSTINGER_DEPLOY_METHOD)"
|
||||
|
||||
## deploy-hostinger-rollback: Rollback PicoClaw on Hostinger VPS
|
||||
deploy-hostinger-rollback:
|
||||
@bash deploy/hostinger/rollback.sh \
|
||||
-h "$(HOSTINGER_HOST)" \
|
||||
-u "$(HOSTINGER_USER)" \
|
||||
-k "$(HOSTINGER_SSH_KEY)" \
|
||||
-p "$(HOSTINGER_SSH_PORT)" \
|
||||
-m "$(HOSTINGER_DEPLOY_METHOD)"
|
||||
|
||||
## help: Show this help message
|
||||
help:
|
||||
@echo "picoclaw Makefile"
|
||||
|
|
@ -165,12 +224,22 @@ help:
|
|||
@echo " make build # Build for current platform"
|
||||
@echo " make install # Install to ~/.local/bin"
|
||||
@echo " make uninstall # Remove from /usr/local/bin"
|
||||
@echo " make install-skills # Install skills to workspace"
|
||||
@echo ""
|
||||
@echo "Hostinger Deployment:"
|
||||
@echo " make deploy-hostinger-full HOSTINGER_HOST=1.2.3.4 # Full deploy (all-in-one)"
|
||||
@echo " make deploy-hostinger-setup HOSTINGER_HOST=1.2.3.4 # Initial server setup"
|
||||
@echo " make deploy-hostinger HOSTINGER_HOST=1.2.3.4 # Deploy to VPS"
|
||||
@echo " make deploy-hostinger-status HOSTINGER_HOST=1.2.3.4 # Check status"
|
||||
@echo " make deploy-hostinger-rollback HOSTINGER_HOST=1.2.3.4 # Rollback"
|
||||
@echo ""
|
||||
@echo "Environment Variables:"
|
||||
@echo " INSTALL_PREFIX # Installation prefix (default: ~/.local)"
|
||||
@echo " WORKSPACE_DIR # Workspace directory (default: ~/.picoclaw/workspace)"
|
||||
@echo " VERSION # Version string (default: git describe)"
|
||||
@echo " HOSTINGER_HOST # Hostinger VPS IP address"
|
||||
@echo " HOSTINGER_USER # SSH user (default: root)"
|
||||
@echo " HOSTINGER_SSH_KEY # SSH key path (default: ~/.ssh/id_rsa)"
|
||||
@echo " HOSTINGER_DEPLOY_METHOD # Deploy method: docker or binary (default: docker)"
|
||||
@echo ""
|
||||
@echo "Current Configuration:"
|
||||
@echo " Platform: $(PLATFORM)/$(ARCH)"
|
||||
|
|
|
|||
164
QUICK_REFERENCE.md
Normal file
164
QUICK_REFERENCE.md
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
# ⚡ PicoClaw Quick Reference
|
||||
|
||||
## 🚀 Setup (One-Time Only)
|
||||
|
||||
```bash
|
||||
# 1. Secure VPS with Tailscale
|
||||
make setup-tailscale
|
||||
|
||||
# 2. Configure Telegram Bot
|
||||
make setup-telegram
|
||||
|
||||
# Done! 🎉
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Daily Updates
|
||||
|
||||
```bash
|
||||
# Sync with latest code
|
||||
make sync-dev
|
||||
|
||||
# Check status
|
||||
git status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Common Commands
|
||||
|
||||
```bash
|
||||
# Build locally
|
||||
make build
|
||||
|
||||
# Run locally (development)
|
||||
make run
|
||||
|
||||
# Run tests
|
||||
make test
|
||||
|
||||
# Check code quality
|
||||
make check
|
||||
|
||||
# View logs on server
|
||||
ssh root@YOUR_IP 'docker compose logs picoclaw | tail -50'
|
||||
|
||||
# Restart bot (if needed)
|
||||
ssh root@YOUR_IP 'docker compose restart picoclaw'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 Important Files
|
||||
|
||||
```
|
||||
picoclaw/
|
||||
├── deploy/hostinger/
|
||||
│ ├── setup-telegram.sh ← Run: make setup-telegram
|
||||
│ ├── setup-tailscale.sh ← Run: make setup-tailscale
|
||||
│ ├── setup-server.sh ← Runs on VPS initial setup
|
||||
│ └── docker-compose.production.yml
|
||||
├── .github/workflows/
|
||||
│ └── deploy-hostinger.yml ← Auto-deploys on git push
|
||||
├── config/
|
||||
│ ├── config.json ← Edit on VPS (nano)
|
||||
│ └── .env ← Edit on VPS (nano)
|
||||
└── docs/
|
||||
└── TELEGRAM_SETUP.md ← Full guide with troubleshooting
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Secrets Management
|
||||
|
||||
```bash
|
||||
# Add/update GitHub Secret
|
||||
gh secret set PICOCLAW_TELEGRAM_BOT_TOKEN -b "YOUR_TOKEN"
|
||||
|
||||
# List secrets (values hidden)
|
||||
gh secret list
|
||||
|
||||
# Secrets used in deploy:
|
||||
# - PICOCLAW_TELEGRAM_BOT_TOKEN
|
||||
# - ANTHROPIC_API_KEY
|
||||
# - HOSTINGER_HOST
|
||||
# - HOSTINGER_SSH_USER
|
||||
# - HOSTINGER_SSH_PASSWORD
|
||||
# - HOSTINGER_SSH_PORT
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📱 Telegram Bot
|
||||
|
||||
```bash
|
||||
# Create bot: https://t.me/botfather
|
||||
# Commands: /start, /help, /show, /list
|
||||
|
||||
# Get your Telegram user ID (check logs):
|
||||
ssh root@YOUR_IP 'docker compose logs picoclaw | grep user_id'
|
||||
|
||||
# Add to whitelist (config/config.json):
|
||||
"allow_from": ["123456789", "987654321"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Tailscale
|
||||
|
||||
```bash
|
||||
# Get your Tailnet IP
|
||||
ssh root@YOUR_IP 'tailscale ip -4'
|
||||
|
||||
# Access via Tailnet
|
||||
http://100.x.x.x:18790
|
||||
|
||||
# Or via hostname
|
||||
https://picoclaw.YOUR-TAILNET.ts.net
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚨 If Something Breaks
|
||||
|
||||
```bash
|
||||
# 1. Check logs
|
||||
ssh root@YOUR_IP 'docker compose logs --tail=100 picoclaw'
|
||||
|
||||
# 2. Restart container
|
||||
ssh root@YOUR_IP 'docker compose restart picoclaw'
|
||||
|
||||
# 3. Check if port is open
|
||||
ssh root@YOUR_IP 'netstat -tuln | grep 18790'
|
||||
|
||||
# 4. Verify GitHub Secrets are set
|
||||
gh secret list
|
||||
|
||||
# 5. Force redeploy
|
||||
git commit --allow-empty -m "chore: trigger redeploy"
|
||||
git push origin claude/hostinger-remote-deployment-TGVof
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Full Guides
|
||||
|
||||
- **Setup Complete Guide**: [SETUP_COMPLETE.md](SETUP_COMPLETE.md)
|
||||
- **Sync & Git Guide**: [SYNC_GUIDE.md](SYNC_GUIDE.md)
|
||||
- **Telegram Setup**: [docs/TELEGRAM_SETUP.md](docs/TELEGRAM_SETUP.md)
|
||||
- **Telegram Quickstart**: [TELEGRAM_QUICKSTART.md](TELEGRAM_QUICKSTART.md)
|
||||
|
||||
---
|
||||
|
||||
## 💡 Tips
|
||||
|
||||
1. **Always `make sync-dev` before starting work**
|
||||
2. **Use `make check` to verify code before pushing**
|
||||
3. **GitHub Actions deploys automatically on push**
|
||||
4. **Keep bot token in GitHub Secrets, never in code**
|
||||
5. **Test locally with `make build && make run` first**
|
||||
|
||||
---
|
||||
|
||||
**Need help?** Check the full guides or open an issue! 🚀
|
||||
349
SETUP_COMPLETE.md
Normal file
349
SETUP_COMPLETE.md
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
# 🚀 PicoClaw Complete Setup Guide
|
||||
|
||||
**Secure Telegram Bot on Hostinger VPS with Tailscale**
|
||||
|
||||
---
|
||||
|
||||
## 📋 Setup Overview
|
||||
|
||||
This guide walks you through:
|
||||
1. **🔐 Tailscale** - Secure private network access
|
||||
2. **🤖 Telegram** - Bot integration with @BotFather
|
||||
3. **✅ Verification** - Test everything works
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Prerequisites
|
||||
|
||||
- Hostinger VPS running (Ubuntu 20.04+)
|
||||
- GitHub account and repository
|
||||
- Telegram account
|
||||
- SSH access to your VPS
|
||||
- `make` and `bash` installed locally
|
||||
- `gh` CLI (optional, for GitHub Secrets automation)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Quick Start (3 Steps)
|
||||
|
||||
### Step 1: Secure with Tailscale
|
||||
|
||||
```bash
|
||||
make setup-tailscale
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- ✅ Installs Tailscale on your VPS
|
||||
- ✅ Authenticates your VPS to your Tailnet
|
||||
- ✅ Blocks port 18790 from public internet
|
||||
- ✅ Creates secure tunnel (only accessible via your Tailscale network)
|
||||
|
||||
**Time:** ~5 minutes
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Setup Telegram Bot
|
||||
|
||||
```bash
|
||||
make setup-telegram
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- ✅ Guides you to create bot with @BotFather
|
||||
- ✅ Validates bot token with Telegram API
|
||||
- ✅ Configures GitHub Secrets automatically (if gh CLI available)
|
||||
- ✅ Deploys to your VPS
|
||||
- ✅ Verifies installation
|
||||
|
||||
**Time:** ~10 minutes
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Test Everything
|
||||
|
||||
```bash
|
||||
# Check Tailscale status
|
||||
ssh root@YOUR_IP 'tailscale status'
|
||||
|
||||
# Check Telegram logs
|
||||
ssh root@YOUR_IP 'docker exec picoclaw tail -50 /opt/picoclaw/logs/picoclaw.log | grep -i telegram'
|
||||
|
||||
# Test Telegram bot
|
||||
# Open Telegram and find your bot, send /start
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Step-by-Step Details
|
||||
|
||||
### Phase 1: Initial Server Setup (One-time)
|
||||
|
||||
If this is a fresh VPS:
|
||||
|
||||
```bash
|
||||
# SSH into your server
|
||||
ssh root@YOUR_HOSTINGER_IP
|
||||
|
||||
# Or use GitHub Actions to deploy (easier)
|
||||
git push origin main
|
||||
# Watch deployment at: https://github.com/YOUR_USER/YOUR_REPO/actions
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Tailscale Configuration
|
||||
|
||||
**Option A: Automated (Recommended)**
|
||||
|
||||
```bash
|
||||
# From your local machine
|
||||
make setup-tailscale
|
||||
```
|
||||
|
||||
Follow the interactive prompts. The script will:
|
||||
1. Ask for SSH details
|
||||
2. Install Tailscale
|
||||
3. Open authentication link (click in browser)
|
||||
4. Activate Tailscale serve
|
||||
5. Verify connectivity
|
||||
|
||||
**Option B: Manual (SSH)**
|
||||
|
||||
```bash
|
||||
ssh root@YOUR_IP
|
||||
|
||||
# Install Tailscale
|
||||
curl -fsSL https://tailscale.com/install.sh | sh
|
||||
|
||||
# Authenticate
|
||||
tailscale up --hostname=picoclaw --ssh
|
||||
# (Copy the URL and open in browser)
|
||||
|
||||
# Activate serve
|
||||
tailscale serve --bg http://localhost:18790
|
||||
|
||||
# Verify
|
||||
tailscale ip -4
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Telegram Bot Setup
|
||||
|
||||
**Option A: Automated (Recommended)**
|
||||
|
||||
```bash
|
||||
# From your local machine
|
||||
make setup-telegram
|
||||
```
|
||||
|
||||
Follow the interactive prompts. The script will:
|
||||
1. Guide you to @BotFather
|
||||
2. Validate your bot token
|
||||
3. Save to GitHub Secrets
|
||||
4. Deploy automatically
|
||||
5. Verify installation
|
||||
|
||||
**Option B: Manual (GitHub Secrets)**
|
||||
|
||||
1. **Create bot with @BotFather**
|
||||
- Open Telegram → Search `@BotFather`
|
||||
- Send `/newbot`
|
||||
- Give it a name and username
|
||||
- Copy the token
|
||||
|
||||
2. **Add to GitHub Secrets**
|
||||
```bash
|
||||
gh secret set PICOCLAW_TELEGRAM_BOT_TOKEN -b YOUR_TOKEN -R your-user/picoclaw
|
||||
```
|
||||
|
||||
3. **Deploy**
|
||||
```bash
|
||||
git push origin main
|
||||
```
|
||||
|
||||
4. **Verify**
|
||||
- Open Telegram, find your bot
|
||||
- Send `/start`
|
||||
- Check logs: `ssh root@YOUR_IP tail -f /opt/picoclaw/logs/picoclaw.log | grep telegram`
|
||||
|
||||
**Option C: Manual (Direct SSH)**
|
||||
|
||||
```bash
|
||||
ssh root@YOUR_IP
|
||||
|
||||
# Edit .env
|
||||
nano /opt/picoclaw/config/.env
|
||||
|
||||
# Add these lines:
|
||||
PICOCLAW_CHANNELS_TELEGRAM_ENABLED=true
|
||||
PICOCLAW_CHANNELS_TELEGRAM_TOKEN=YOUR_TOKEN_HERE
|
||||
|
||||
# Save and exit (Ctrl+X, Y, Enter)
|
||||
|
||||
# Restart Docker
|
||||
docker compose -f /opt/picoclaw/docker-compose.yml restart picoclaw
|
||||
|
||||
# Verify
|
||||
docker compose logs picoclaw | grep -i telegram
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Verification Checklist
|
||||
|
||||
After setup is complete, verify everything works:
|
||||
|
||||
### 1. Tailscale
|
||||
|
||||
```bash
|
||||
# Check Tailscale is running
|
||||
ssh root@YOUR_IP 'tailscale status'
|
||||
|
||||
# Check serve is active
|
||||
ssh root@YOUR_IP 'ps aux | grep tailscale'
|
||||
|
||||
# Get your Tailnet IP
|
||||
ssh root@YOUR_IP 'tailscale ip -4'
|
||||
```
|
||||
|
||||
### 2. PicoClaw Gateway
|
||||
|
||||
```bash
|
||||
# Check container is running
|
||||
ssh root@YOUR_IP 'docker compose ps'
|
||||
|
||||
# Check health endpoint (via Tailscale IP)
|
||||
TAILNET_IP=$(ssh root@YOUR_IP 'tailscale ip -4')
|
||||
curl http://$TAILNET_IP:18790/health
|
||||
|
||||
# Check gateway is listening on localhost only
|
||||
ssh root@YOUR_IP 'netstat -tuln | grep 18790'
|
||||
# Should show: 127.0.0.1:18790 (NOT 0.0.0.0)
|
||||
```
|
||||
|
||||
### 3. Telegram Bot
|
||||
|
||||
```bash
|
||||
# Check logs for Telegram initialization
|
||||
ssh root@YOUR_IP 'docker compose logs picoclaw | grep -i telegram'
|
||||
|
||||
# Expected output should include:
|
||||
# "Starting Telegram bot (polling mode)..."
|
||||
# "Telegram bot connected"
|
||||
```
|
||||
|
||||
### 4. Test Telegram Bot
|
||||
|
||||
1. Open Telegram
|
||||
2. Search for your bot (username from @BotFather)
|
||||
3. Click **Start**
|
||||
4. Send a message
|
||||
|
||||
Expected response:
|
||||
```
|
||||
Thinking... 💭
|
||||
[Claude's response]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Security Summary
|
||||
|
||||
| Component | Status | Access Method |
|
||||
|-----------|--------|----------------|
|
||||
| SSH | 🔒 Protected | Tailscale tunnel |
|
||||
| PicoClaw Gateway (18790) | 🔒 Protected | Tailscale tunnel only |
|
||||
| Telegram Bot | 🌐 Public | via Telegram API |
|
||||
| Config/Secrets | 🔐 Encrypted | GitHub Secrets |
|
||||
|
||||
**What's protected:**
|
||||
- ✅ Port 18790 is NOT exposed to the internet
|
||||
- ✅ Accessible only via your Tailscale network
|
||||
- ✅ UFW firewall blocks public access
|
||||
- ✅ Bot token stored in GitHub Secrets (never in code)
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Tailscale not authenticating
|
||||
|
||||
```bash
|
||||
# Check if already authenticated
|
||||
ssh root@YOUR_IP 'tailscale status'
|
||||
|
||||
# If not, try again
|
||||
ssh root@YOUR_IP 'tailscale up --hostname=picoclaw'
|
||||
```
|
||||
|
||||
### Telegram bot not responding
|
||||
|
||||
```bash
|
||||
# Check if Telegram is enabled
|
||||
ssh root@YOUR_IP 'grep TELEGRAM /opt/picoclaw/config/.env'
|
||||
|
||||
# Check logs
|
||||
ssh root@YOUR_IP 'docker compose logs picoclaw | grep -i telegram'
|
||||
|
||||
# Verify token is correct (should start with numbers:)
|
||||
ssh root@YOUR_IP 'grep TELEGRAM_TOKEN /opt/picoclaw/config/.env'
|
||||
```
|
||||
|
||||
### Can't reach PicoClaw via Tailscale
|
||||
|
||||
```bash
|
||||
# Check Tailscale IP
|
||||
ssh root@YOUR_IP 'tailscale ip -4'
|
||||
|
||||
# Check if port is listening
|
||||
ssh root@YOUR_IP 'netstat -tuln | grep 18790'
|
||||
|
||||
# Check if Docker container is running
|
||||
ssh root@YOUR_IP 'docker compose ps'
|
||||
|
||||
# Check logs
|
||||
ssh root@YOUR_IP 'docker compose logs picoclaw | tail -50'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Additional Resources
|
||||
|
||||
- **Tailscale Docs**: https://tailscale.com/kb/
|
||||
- **Telegram Bot API**: https://core.telegram.org/bots
|
||||
- **PicoClaw Telegram Setup**: [docs/TELEGRAM_SETUP.md](docs/TELEGRAM_SETUP.md)
|
||||
- **PicoClaw Quickstart**: [TELEGRAM_QUICKSTART.md](TELEGRAM_QUICKSTART.md)
|
||||
|
||||
---
|
||||
|
||||
## 🎬 Next Steps
|
||||
|
||||
1. **Run `make setup-tailscale`** - Secure your VPS
|
||||
2. **Run `make setup-telegram`** - Add Telegram bot
|
||||
3. **Test your bot** - Send a message on Telegram
|
||||
4. **Configure whitelist** (optional) - Restrict to specific users
|
||||
5. **Set up monitoring** (optional) - Get alerts on failures
|
||||
|
||||
---
|
||||
|
||||
## ❓ FAQ
|
||||
|
||||
**Q: Why Tailscale?**
|
||||
A: It creates a private network between your devices and VPS. Port 18790 stays hidden from the internet while remaining accessible to you.
|
||||
|
||||
**Q: Can others use my bot without Tailscale?**
|
||||
A: Yes! The Telegram bot is public (everyone can talk to it), but the PicoClaw gateway behind it is private (only you can manage it via Tailscale).
|
||||
|
||||
**Q: How much does Tailscale cost?**
|
||||
A: Free for personal use (up to 100 devices). Perfect for this setup.
|
||||
|
||||
**Q: What if I lose my device?**
|
||||
A: Remove it from your Tailnet at https://login.tailscale.com. It will lose access immediately.
|
||||
|
||||
**Q: Can I use a different VPN?**
|
||||
A: Sure, but you'll need to configure a different security tunnel yourself. Tailscale is recommended for simplicity.
|
||||
|
||||
---
|
||||
|
||||
**Happy deploying! 🚀**
|
||||
277
STATUS.md
Normal file
277
STATUS.md
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
# 📊 PicoClaw Deployment Status
|
||||
|
||||
**Last Updated:** 2026-02-17
|
||||
|
||||
---
|
||||
|
||||
## ✅ Completed Setup
|
||||
|
||||
### 🔐 Security & Network
|
||||
|
||||
- [x] **Tailscale Integration**
|
||||
- Port 18790 bound to `127.0.0.1` only (not public)
|
||||
- Tailscale serve configured in deployment workflow
|
||||
- UFW firewall blocking public access
|
||||
- SSH accessible via Tailscale tunnel
|
||||
- Script: `make setup-tailscale`
|
||||
|
||||
- [x] **GitHub Secrets Management**
|
||||
- `PICOCLAW_TELEGRAM_BOT_TOKEN` support added
|
||||
- `ANTHROPIC_API_KEY` support added
|
||||
- Automatic secret injection in deploy workflow
|
||||
- No secrets in version control ✓
|
||||
|
||||
### 🤖 Telegram Bot
|
||||
|
||||
- [x] **Telegram Channel Implementation**
|
||||
- Full bot implementation (polling mode)
|
||||
- Voice message transcription support
|
||||
- Image/document handling
|
||||
- User whitelist (`allow_from` config)
|
||||
- Proxy support for restricted regions
|
||||
- Commands: `/start`, `/help`, `/show`, `/list`
|
||||
- Script: `make setup-telegram`
|
||||
|
||||
- [x] **Configuration Templates**
|
||||
- `config.json` updated with Telegram settings
|
||||
- `.env` template with Telegram variables
|
||||
- Ready for production deployment
|
||||
|
||||
### 📦 Deployment
|
||||
|
||||
- [x] **GitHub Actions Workflow**
|
||||
- Automated deploy on push to dev branch
|
||||
- SSH-based deployment via sshpass
|
||||
- Docker build and restart
|
||||
- Health checks (5 attempts)
|
||||
- Tailscale serve activation
|
||||
- Environment variable injection from secrets
|
||||
|
||||
- [x] **Setup Scripts**
|
||||
- Initial server setup (Tailscale, firewall, Docker)
|
||||
- Interactive Telegram setup
|
||||
- Interactive Tailscale setup
|
||||
- Automated sync script
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- [x] **Comprehensive Guides**
|
||||
- `SETUP_COMPLETE.md` - Full setup walkthrough
|
||||
- `SYNC_GUIDE.md` - Git and synchronization
|
||||
- `QUICK_REFERENCE.md` - One-page cheat sheet
|
||||
- `TELEGRAM_QUICKSTART.md` - Quick 3-step Telegram setup
|
||||
- `docs/TELEGRAM_SETUP.md` - Detailed Telegram reference
|
||||
- `STATUS.md` - This file
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Getting Started (for you)
|
||||
|
||||
### Option A: Automated (Recommended)
|
||||
|
||||
```bash
|
||||
# 1. Sync latest code
|
||||
make sync-dev
|
||||
|
||||
# 2. Setup Tailscale (one-time)
|
||||
make setup-tailscale
|
||||
|
||||
# 3. Setup Telegram (one-time)
|
||||
make setup-telegram
|
||||
|
||||
# Done! Your bot is live 🎉
|
||||
```
|
||||
|
||||
### Option B: Manual
|
||||
|
||||
See `SETUP_COMPLETE.md` for step-by-step instructions.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Telegram Users (Public) │
|
||||
│ Sends: /start, text, images, voice messages │
|
||||
└──────────────────┬──────────────────────────────────┘
|
||||
│
|
||||
↓ (Telegram API - Public)
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ PicoClaw Telegram Bot │
|
||||
│ - Polling mode (no webhook needed) │
|
||||
│ - Requests via HTTPS to Telegram API │
|
||||
│ - No incoming connections required │
|
||||
└──────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
↓ (Encrypted Tailscale tunnel)
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Hostinger VPS (Private via Tailscale) │
|
||||
│ ├─ Port 18790 (127.0.0.1 only - not public) │
|
||||
│ ├─ Tailscale serve proxy │
|
||||
│ ├─ Docker container (picoclaw) │
|
||||
│ ├─ LLM API connections (Anthropic/OpenAI) │
|
||||
│ └─ UFW firewall (blocks public access) │
|
||||
└──────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
↓ (HTTPS - Outbound)
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ LLM Providers (Claude, GPT-4, etc) │
|
||||
│ Web Search APIs │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
|
||||
Your Devices (MacBook, Laptop, etc)
|
||||
├─ Access via: Tailscale network
|
||||
├─ SSH: ssh picoclaw.TAILNET.ts.net
|
||||
└─ GUI: http://100.x.x.x:18790 (via Tailscale IP)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 Key Files & Their Purpose
|
||||
|
||||
| File | Purpose | Edit? |
|
||||
|------|---------|-------|
|
||||
| `Makefile` | Build & deployment targets | Scripts only |
|
||||
| `.github/workflows/deploy-hostinger.yml` | CI/CD pipeline | `make setup-telegram` |
|
||||
| `deploy/hostinger/setup-server.sh` | VPS initialization | `make setup-tailscale` |
|
||||
| `deploy/hostinger/setup-telegram.sh` | Interactive bot setup | Run it |
|
||||
| `deploy/hostinger/setup-tailscale.sh` | Interactive Tailscale setup | Run it |
|
||||
| `deploy/sync-dev.sh` | Git sync helper | `make sync-dev` |
|
||||
| `config/config.json` | App configuration | Edit on VPS |
|
||||
| `config/.env` | Environment variables | Edit on VPS or GitHub Secrets |
|
||||
| `docs/TELEGRAM_SETUP.md` | Reference guide | Read only |
|
||||
| `SETUP_COMPLETE.md` | Setup walkthrough | Read only |
|
||||
| `SYNC_GUIDE.md` | Git guide | Read only |
|
||||
| `QUICK_REFERENCE.md` | Cheat sheet | Read only |
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Verification Checklist
|
||||
|
||||
```bash
|
||||
# Check GitHub branch
|
||||
git branch -v
|
||||
# Should show: * claude/hostinger-remote-deployment-TGVof
|
||||
|
||||
# Check git history
|
||||
git log --oneline -10
|
||||
# Should show your recent commits
|
||||
|
||||
# Check secrets are configured
|
||||
gh secret list
|
||||
# Should show PICOCLAW_TELEGRAM_BOT_TOKEN, ANTHROPIC_API_KEY, etc
|
||||
|
||||
# Test locally (if you have Go installed)
|
||||
make build
|
||||
make run
|
||||
|
||||
# Verify Docker setup on VPS
|
||||
ssh root@YOUR_IP 'docker compose ps'
|
||||
|
||||
# Check Tailscale status
|
||||
ssh root@YOUR_IP 'tailscale status'
|
||||
|
||||
# Verify port binding (should be 127.0.0.1 only)
|
||||
ssh root@YOUR_IP 'netstat -tuln | grep 18790'
|
||||
# Expected: tcp 127.0.0.1:18790
|
||||
|
||||
# Check Telegram logs
|
||||
ssh root@YOUR_IP 'docker compose logs picoclaw | grep -i telegram'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Current Setup State
|
||||
|
||||
| Component | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| Git Branch | ✅ Ready | `claude/hostinger-remote-deployment-TGVof` |
|
||||
| Docker Setup | ✅ Ready | Configured for production |
|
||||
| Telegram Bot | ⏳ Pending | Run `make setup-telegram` |
|
||||
| Tailscale | ⏳ Pending | Run `make setup-tailscale` |
|
||||
| GitHub Actions | ✅ Ready | Will auto-deploy on push |
|
||||
| Firewall | ✅ Secured | Port 18790 blocked from public |
|
||||
| Documentation | ✅ Complete | 5 guides + this status file |
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Security Summary
|
||||
|
||||
| Layer | Status | Details |
|
||||
|-------|--------|---------|
|
||||
| Network | 🔒 Secure | Tailscale VPN tunnel |
|
||||
| Port 18790 | 🔒 Secure | Bound to 127.0.0.1 only |
|
||||
| Firewall | 🔒 Secure | UFW blocks public access |
|
||||
| Bot Token | 🔒 Secure | In GitHub Secrets (not in code) |
|
||||
| SSH | 🔒 Secure | Via Tailscale tunnel |
|
||||
| API Keys | 🔒 Secure | In GitHub Secrets or .env (on VPS) |
|
||||
| Logs | 📝 Available | Via SSH: `docker compose logs` |
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
### Quick Commands Reference
|
||||
```bash
|
||||
# Daily: Sync with latest
|
||||
make sync-dev
|
||||
|
||||
# One-time: Setup Tailscale
|
||||
make setup-tailscale
|
||||
|
||||
# One-time: Setup Telegram
|
||||
make setup-telegram
|
||||
|
||||
# Development: Build & run locally
|
||||
make build && make run
|
||||
|
||||
# Server: Check status
|
||||
ssh root@YOUR_IP 'docker compose ps'
|
||||
|
||||
# Server: View logs
|
||||
ssh root@YOUR_IP 'docker compose logs picoclaw'
|
||||
|
||||
# GitHub: Check deploy status
|
||||
gh run list
|
||||
```
|
||||
|
||||
### Read These When...
|
||||
|
||||
- **First time setup**: Read `SETUP_COMPLETE.md`
|
||||
- **Need quick commands**: Read `QUICK_REFERENCE.md`
|
||||
- **Working with git**: Read `SYNC_GUIDE.md`
|
||||
- **Telegram issues**: Read `docs/TELEGRAM_SETUP.md`
|
||||
- **Telegram quickstart**: Read `TELEGRAM_QUICKSTART.md`
|
||||
|
||||
---
|
||||
|
||||
## 📈 Next Steps
|
||||
|
||||
1. ✅ **Tailscale Setup** → Run `make setup-tailscale`
|
||||
2. ✅ **Telegram Setup** → Run `make setup-telegram`
|
||||
3. ✅ **Test Bot** → Find @your_bot on Telegram and send `/start`
|
||||
4. ⭐ **Monitor Deployment** → Check GitHub Actions
|
||||
5. 🎉 **You're live!**
|
||||
|
||||
---
|
||||
|
||||
## 💡 Quick Facts
|
||||
|
||||
- **Telegram Bot**: Public (anyone can chat)
|
||||
- **PicoClaw Gateway**: Private (only you via Tailscale)
|
||||
- **Deployment**: Automatic on git push
|
||||
- **Hosting**: Hostinger VPS
|
||||
- **Network**: Secured via Tailscale
|
||||
- **Firewall**: UFW blocking public access
|
||||
- **SSL/TLS**: Telegram API + Tailscale tunnel
|
||||
|
||||
---
|
||||
|
||||
**Status**: 🟢 **Production Ready**
|
||||
|
||||
All components configured. Ready to deploy!
|
||||
|
||||
---
|
||||
|
||||
*For detailed information, see the individual markdown files in this repository.*
|
||||
352
SYNC_GUIDE.md
Normal file
352
SYNC_GUIDE.md
Normal file
|
|
@ -0,0 +1,352 @@
|
|||
# 🔄 Mantendo PicoClaw Atualizado Localmente
|
||||
|
||||
Guia para sincronizar mudanças com a branch de desenvolvimento.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Quick Commands
|
||||
|
||||
### Atualizar tudo (recomendado):
|
||||
```bash
|
||||
make sync-dev
|
||||
# OR
|
||||
git fetch origin claude/hostinger-remote-deployment-TGVof
|
||||
git merge origin/claude/hostinger-remote-deployment-TGVof
|
||||
```
|
||||
|
||||
### Ver mudanças antes de aplicar:
|
||||
```bash
|
||||
git fetch origin claude/hostinger-remote-deployment-TGVof
|
||||
git diff origin/claude/hostinger-remote-deployment-TGVof
|
||||
```
|
||||
|
||||
### Resetar para a branch remota (se fez muitas mudanças locais):
|
||||
```bash
|
||||
git fetch origin claude/hostinger-remote-deployment-TGVof
|
||||
git reset --hard origin/claude/hostinger-remote-deployment-TGVof
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Entendendo o Fluxo de Trabalho
|
||||
|
||||
```
|
||||
Local (seu computador)
|
||||
↓
|
||||
├─ Branch: claude/hostinger-remote-deployment-TGVof
|
||||
│ (sua branch de trabalho)
|
||||
│
|
||||
Remote (GitHub)
|
||||
↓
|
||||
├─ Branch: claude/hostinger-remote-deployment-TGVof
|
||||
│ (repositório central)
|
||||
│
|
||||
Hostinger VPS
|
||||
↓
|
||||
├─ /opt/picoclaw (aplicação rodando)
|
||||
│ (sincronizado via GitHub Actions)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Cenários Comuns
|
||||
|
||||
### Cenário 1: Sincronizar após fazer mudanças locais
|
||||
|
||||
```bash
|
||||
# 1. Ver status
|
||||
git status
|
||||
|
||||
# 2. Fazer commit das mudanças
|
||||
git add .
|
||||
git commit -m "chore: my local changes"
|
||||
|
||||
# 3. Puxar mudanças do repositório remoto
|
||||
git pull origin claude/hostinger-remote-deployment-TGVof
|
||||
|
||||
# 4. Se tiver conflitos:
|
||||
# - Editar os arquivos com conflito
|
||||
# - Resolver manualmente
|
||||
git add <arquivo-resolvido>
|
||||
git commit -m "resolve merge conflicts"
|
||||
|
||||
# 5. Enviar suas mudanças
|
||||
git push origin claude/hostinger-remote-deployment-TGVof
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Cenário 2: Sincronizar SEM fazer mudanças
|
||||
|
||||
```bash
|
||||
# Simples: puxar tudo
|
||||
git pull origin claude/hostinger-remote-deployment-TGVof
|
||||
|
||||
# Ou de forma mais segura:
|
||||
git fetch origin claude/hostinger-remote-deployment-TGVof
|
||||
git merge origin/claude/hostinger-remote-deployment-TGVof
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Cenário 3: Voltar para versão anterior (se algo deu errado)
|
||||
|
||||
```bash
|
||||
# Ver histórico
|
||||
git log --oneline -10
|
||||
|
||||
# Voltar para um commit específico (CUIDADO: descarta mudanças recentes)
|
||||
git reset --hard <COMMIT_HASH>
|
||||
|
||||
# Ou simplesmente resetar para a versão remota
|
||||
git reset --hard origin/claude/hostinger-remote-deployment-TGVof
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Arquivos Importantes (Não Edite Diretamente)
|
||||
|
||||
Esses arquivos são gerenciados automaticamente pelo Claude Code. **Edite apenas via scripts**:
|
||||
|
||||
| Arquivo | Como Editar |
|
||||
|---------|-------------|
|
||||
| `.github/workflows/deploy-hostinger.yml` | `make setup-telegram` / `make setup-tailscale` |
|
||||
| `deploy/hostinger/setup-server.sh` | `make setup-tailscale` |
|
||||
| `deploy/hostinger/setup-telegram.sh` | `make setup-telegram` |
|
||||
| `deploy/hostinger/docker-compose.production.yml` | Manual via SSH |
|
||||
| `Makefile` | `make` targets são auto-gerenciados |
|
||||
|
||||
---
|
||||
|
||||
## 🚨 Evite Fazer Isso
|
||||
|
||||
### ❌ NÃO edite arquivos manualmente que podem ter conflitos:
|
||||
|
||||
```bash
|
||||
# RUIM: Editar workflow manualmente
|
||||
nano .github/workflows/deploy-hostinger.yml
|
||||
|
||||
# BOM: Usar os scripts
|
||||
make setup-telegram
|
||||
make setup-tailscale
|
||||
```
|
||||
|
||||
### ❌ NÃO faça force push para main:
|
||||
|
||||
```bash
|
||||
# MUITO RUIM - pode apagar trabalho de outros!
|
||||
git push --force-with-lease origin main
|
||||
|
||||
# OK para sua branch de dev
|
||||
git push --force-with-lease origin claude/hostinger-remote-deployment-TGVof
|
||||
# (só se tiver certeza)
|
||||
```
|
||||
|
||||
### ❌ NÃO commit .env ou secrets:
|
||||
|
||||
```bash
|
||||
# Se acidentalmente fez commit de secrets:
|
||||
git rm --cached .env config/.env
|
||||
git commit -m "remove secrets (they were already in GitHub Secrets anyway)"
|
||||
git push origin claude/hostinger-remote-deployment-TGVof
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Workflow Recomendado
|
||||
|
||||
### Diariamente:
|
||||
|
||||
```bash
|
||||
# Ao iniciar o dia
|
||||
git fetch origin
|
||||
git status
|
||||
|
||||
# Se houver mudanças remotas
|
||||
git pull origin claude/hostinger-remote-deployment-TGVof
|
||||
```
|
||||
|
||||
### Antes de Fazer Mudanças:
|
||||
|
||||
```bash
|
||||
# Garantir que está atualizado
|
||||
git pull origin claude/hostinger-remote-deployment-TGVof
|
||||
|
||||
# Criar sua mudança
|
||||
# ... editar arquivos ...
|
||||
|
||||
# Commitar
|
||||
git add .
|
||||
git commit -m "feat: descrição da mudança"
|
||||
|
||||
# Enviar
|
||||
git push origin claude/hostinger-remote-deployment-TGVof
|
||||
```
|
||||
|
||||
### Após Deploy no Hostinger:
|
||||
|
||||
```bash
|
||||
# Verificar se o deploy funcionou
|
||||
git log --oneline -5
|
||||
|
||||
# Ver status do deploy
|
||||
# (GitHub Actions mostra o status automaticamente)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Comandos Git Úteis
|
||||
|
||||
```bash
|
||||
# Ver qual branch está
|
||||
git branch -v
|
||||
|
||||
# Ver mudanças não commitadas
|
||||
git diff
|
||||
|
||||
# Ver histórico
|
||||
git log --oneline -10
|
||||
|
||||
# Ver diferenças com remoto
|
||||
git diff origin/claude/hostinger-remote-deployment-TGVof
|
||||
|
||||
# Limpar arquivos não rastreados
|
||||
git clean -fd
|
||||
|
||||
# Descartar mudanças em um arquivo
|
||||
git checkout -- <arquivo>
|
||||
|
||||
# Descartar todas mudanças locais
|
||||
git reset --hard HEAD
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Monitorando Deploy
|
||||
|
||||
Após fazer push, o deploy automático começa. Monitore em:
|
||||
|
||||
**GitHub Actions:**
|
||||
```
|
||||
https://github.com/agenciaspace/picoclaw/actions
|
||||
```
|
||||
|
||||
**Ou via terminal:**
|
||||
```bash
|
||||
gh run list -b claude/hostinger-remote-deployment-TGVof --limit 5
|
||||
|
||||
# Ver logs de um deploy específico
|
||||
gh run view <RUN_ID>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Resolving Merge Conflicts
|
||||
|
||||
Se tiver conflitos ao fazer pull:
|
||||
|
||||
```bash
|
||||
# 1. Ver quais arquivos têm conflito
|
||||
git status
|
||||
|
||||
# 2. Editar os arquivos com conflito
|
||||
# Procurar por:
|
||||
# <<<<<<< HEAD (sua versão local)
|
||||
# =======
|
||||
# >>>>>>> origin/... (versão remota)
|
||||
|
||||
# 3. Decidir qual versão manter ou combinar
|
||||
|
||||
# 4. Marcar como resolvido
|
||||
git add <arquivo-resolvido>
|
||||
|
||||
# 5. Completar merge
|
||||
git commit -m "resolve merge conflicts"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 Atualizar Dependências
|
||||
|
||||
```bash
|
||||
# Ver dependências desatualizadas
|
||||
make check
|
||||
|
||||
# Atualizar todas
|
||||
make update-deps
|
||||
|
||||
# Commitar mudanças
|
||||
git add go.mod go.sum
|
||||
git commit -m "chore: update dependencies"
|
||||
git push origin claude/hostinger-remote-deployment-TGVof
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Segurança
|
||||
|
||||
### NUNCA commit secrets:
|
||||
- ❌ Bot tokens de Telegram
|
||||
- ❌ API keys
|
||||
- ❌ Passwords
|
||||
- ❌ Private keys
|
||||
|
||||
### Use GitHub Secrets:
|
||||
```bash
|
||||
# Adicionar secret
|
||||
gh secret set PICOCLAW_TELEGRAM_BOT_TOKEN -b "sua_token"
|
||||
|
||||
# Ver secrets (valores não aparecem)
|
||||
gh secret list
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📱 Sincronizar em Múltiplas Máquinas
|
||||
|
||||
Se trabalhar em vários computadores:
|
||||
|
||||
```bash
|
||||
# Máquina A: Fazer mudanças e push
|
||||
git add .
|
||||
git commit -m "feat: minha mudança"
|
||||
git push origin claude/hostinger-remote-deployment-TGVof
|
||||
|
||||
# Máquina B: Puxar mudanças
|
||||
git pull origin claude/hostinger-remote-deployment-TGVof
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Checklist de Atualização
|
||||
|
||||
- [ ] `git pull origin claude/hostinger-remote-deployment-TGVof`
|
||||
- [ ] `git status` (verifica se há conflitos)
|
||||
- [ ] Testar localmente: `make build && make run`
|
||||
- [ ] Conferir arquivos: `git diff HEAD~1` (mudanças do último commit)
|
||||
- [ ] `git push origin claude/hostinger-remote-deployment-TGVof`
|
||||
- [ ] Monitorar GitHub Actions (deploy automático)
|
||||
- [ ] Testar no Hostinger após deploy
|
||||
|
||||
---
|
||||
|
||||
## ❓ FAQ
|
||||
|
||||
**P: Como saber se está desatualizado?**
|
||||
R: `git fetch` e depois `git status` mostra se há mudanças remotas.
|
||||
|
||||
**P: Posso fazer push diretamente para main?**
|
||||
R: Tecnicamente sim, mas evite. Use a branch claude/hostinger-* para segurança.
|
||||
|
||||
**P: O que fazer se acidentalmente editei arquivo importante?**
|
||||
R: `git checkout -- <arquivo>` para descartar mudanças.
|
||||
|
||||
**P: Como reverter um commit já feito?**
|
||||
R: `git revert <COMMIT_HASH>` (cria novo commit que desfaz as mudanças).
|
||||
|
||||
**P: Preciso fazer pull toda vez?**
|
||||
R: Sim, para manter sincronizado. Especialmente antes de fazer push.
|
||||
|
||||
---
|
||||
|
||||
**Dica:** Faça `git pull` regularmente para evitar conflitos grandes! 🚀
|
||||
93
TELEGRAM_QUICKSTART.md
Normal file
93
TELEGRAM_QUICKSTART.md
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
# 🤖 PicoClaw Telegram Bot - Quick Start
|
||||
|
||||
**Get your PicoClaw AI assistant on Telegram in 3 minutes!**
|
||||
|
||||
## ⚡ Quick Setup
|
||||
|
||||
### 1️⃣ Get Your Bot Token
|
||||
|
||||
1. Open Telegram → Search `@BotFather`
|
||||
2. Send `/newbot`
|
||||
3. Follow prompts (name + username ending in `_bot`)
|
||||
4. **Copy the token** 🔐
|
||||
|
||||
### 2️⃣ Add to GitHub Secrets
|
||||
|
||||
- Go to your repo: **Settings** → **Secrets and Variables** → **Actions**
|
||||
- Click **New repository secret**
|
||||
- Name: `PICOCLAW_TELEGRAM_BOT_TOKEN`
|
||||
- Value: Your token from step 1
|
||||
|
||||
### 3️⃣ Deploy
|
||||
|
||||
Push to main branch or trigger workflow:
|
||||
```bash
|
||||
git push origin main
|
||||
```
|
||||
|
||||
The bot will be live in ~2 minutes ✨
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Start Using
|
||||
|
||||
Find your bot on Telegram (search username) and send:
|
||||
```
|
||||
/start
|
||||
Hello!
|
||||
```
|
||||
|
||||
### Commands
|
||||
- `/start` - Begin
|
||||
- `/help` - Show help
|
||||
- `/show` - Agent info
|
||||
- `/list` - List agents
|
||||
|
||||
### Features
|
||||
✨ Text messages
|
||||
🎤 Voice notes (auto-transcribed)
|
||||
📸 Images
|
||||
📄 Documents
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Manual Setup (Without GitHub Actions)
|
||||
|
||||
### SSH Setup
|
||||
```bash
|
||||
ssh root@YOUR_IP
|
||||
nano /opt/picoclaw/config/.env
|
||||
```
|
||||
|
||||
Add:
|
||||
```
|
||||
PICOCLAW_CHANNELS_TELEGRAM_ENABLED=true
|
||||
PICOCLAW_CHANNELS_TELEGRAM_TOKEN=YOUR_TOKEN_HERE
|
||||
```
|
||||
|
||||
Restart:
|
||||
```bash
|
||||
docker compose -f /opt/picoclaw/docker-compose.yml restart picoclaw
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ Security
|
||||
|
||||
- ⚠️ Never commit tokens to git
|
||||
- 🔐 Use GitHub Secrets
|
||||
- 👥 Optional: Whitelist users in `allow_from` config
|
||||
|
||||
---
|
||||
|
||||
## 📚 Full Guide
|
||||
|
||||
See [docs/TELEGRAM_SETUP.md](docs/TELEGRAM_SETUP.md) for advanced options.
|
||||
|
||||
---
|
||||
|
||||
**Questions?** Check logs:
|
||||
```bash
|
||||
ssh root@YOUR_IP
|
||||
tail -f /opt/picoclaw/logs/picoclaw.log | grep -i telegram
|
||||
```
|
||||
|
|
@ -660,17 +660,20 @@ func gatewayCmd() {
|
|||
fmt.Println("✓ Device event service started")
|
||||
}
|
||||
|
||||
// Create the gateway HTTP server first so channels can register webhooks on it
|
||||
healthServer := health.NewServer(cfg.Gateway.Host, cfg.Gateway.Port)
|
||||
channelManager.RegisterWebhooks(healthServer.Mux())
|
||||
|
||||
if err := channelManager.StartAll(ctx); err != nil {
|
||||
fmt.Printf("Error starting channels: %v\n", err)
|
||||
}
|
||||
|
||||
healthServer := health.NewServer(cfg.Gateway.Host, cfg.Gateway.Port)
|
||||
go func() {
|
||||
if err := healthServer.Start(); err != nil && err != http.ErrServerClosed {
|
||||
logger.ErrorCF("health", "Health server error", map[string]interface{}{"error": err.Error()})
|
||||
}
|
||||
}()
|
||||
fmt.Printf("✓ Health endpoints available at http://%s:%d/health and /ready\n", cfg.Gateway.Host, cfg.Gateway.Port)
|
||||
fmt.Printf("✓ Gateway listening on http://%s:%d (health, webhooks)\n", cfg.Gateway.Host, cfg.Gateway.Port)
|
||||
|
||||
go agentLoop.Run(ctx)
|
||||
|
||||
|
|
|
|||
32
deploy/hostinger/.deploy.env.example
Normal file
32
deploy/hostinger/.deploy.env.example
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# ============================================================
|
||||
# PicoClaw - Deploy Configuration (Hostinger VPS)
|
||||
# ============================================================
|
||||
# Copy this file to .deploy.env and fill in your values:
|
||||
# cp deploy/hostinger/.deploy.env.example deploy/hostinger/.deploy.env
|
||||
#
|
||||
# This file is git-ignored. Never commit credentials.
|
||||
# ============================================================
|
||||
|
||||
# SSH Connection
|
||||
DEPLOY_HOST="YOUR_VPS_IP"
|
||||
DEPLOY_USER="root"
|
||||
DEPLOY_PORT="22"
|
||||
DEPLOY_PASSWORD=""
|
||||
|
||||
# SSH Key (leave empty to use password above)
|
||||
DEPLOY_SSH_KEY=""
|
||||
|
||||
# Deploy method: docker or binary
|
||||
DEPLOY_METHOD="docker"
|
||||
|
||||
# LLM Provider & API Key
|
||||
LLM_PROVIDER="anthropic"
|
||||
LLM_API_KEY=""
|
||||
|
||||
# Chat Channel & Token
|
||||
CHAT_CHANNEL="telegram"
|
||||
CHAT_TOKEN=""
|
||||
CHAT_ALLOW_FROM=""
|
||||
|
||||
# Timezone
|
||||
DEPLOY_TIMEZONE="America/Sao_Paulo"
|
||||
205
deploy/hostinger/DEPLOY.md
Normal file
205
deploy/hostinger/DEPLOY.md
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
# PicoClaw - Deploy na Hostinger VPS
|
||||
|
||||
Guia completo para deploy do PicoClaw em um servidor VPS da Hostinger.
|
||||
|
||||
## Pre-requisitos
|
||||
|
||||
- VPS Hostinger com Ubuntu 22.04+ ou Debian 12+ (KVM)
|
||||
- Acesso SSH configurado (chave SSH recomendado)
|
||||
- API keys dos provedores que deseja usar (Anthropic, OpenAI, etc.)
|
||||
|
||||
> **Nota:** Hospedagem compartilhada da Hostinger **nao** suporta PicoClaw.
|
||||
> Voce precisa de um plano VPS ou Cloud com acesso root.
|
||||
|
||||
## Metodo 1: Deploy via Docker (Recomendado)
|
||||
|
||||
### Passo 1: Configurar acesso SSH
|
||||
|
||||
```bash
|
||||
# Gerar chave SSH (se ainda nao tem)
|
||||
ssh-keygen -t ed25519 -C "picoclaw-hostinger"
|
||||
|
||||
# Copiar chave para o servidor
|
||||
ssh-copy-id -i ~/.ssh/id_ed25519 root@SEU_IP_VPS
|
||||
```
|
||||
|
||||
### Passo 2: Setup inicial do servidor
|
||||
|
||||
Execute uma unica vez para preparar o servidor:
|
||||
|
||||
```bash
|
||||
# Opcao A: Via make
|
||||
make deploy-hostinger-setup HOSTINGER_HOST=SEU_IP_VPS
|
||||
|
||||
# Opcao B: Direto via SSH
|
||||
ssh root@SEU_IP_VPS 'bash -s' < deploy/hostinger/setup-server.sh
|
||||
```
|
||||
|
||||
Isso instala: Docker, firewall (ufw), fail2ban, e cria a estrutura de diretorios.
|
||||
|
||||
### Passo 3: Configurar API keys no servidor
|
||||
|
||||
```bash
|
||||
ssh root@SEU_IP_VPS
|
||||
|
||||
# Editar chaves de API
|
||||
nano /opt/picoclaw/config/.env
|
||||
|
||||
# Editar configuracao do PicoClaw
|
||||
nano /opt/picoclaw/config/config.json
|
||||
```
|
||||
|
||||
Exemplo minimo do `.env`:
|
||||
```bash
|
||||
ANTHROPIC_API_KEY=sk-ant-sua-chave-aqui
|
||||
TELEGRAM_BOT_TOKEN=123456:ABC-seu-token
|
||||
TZ=America/Sao_Paulo
|
||||
```
|
||||
|
||||
### Passo 4: Deploy
|
||||
|
||||
```bash
|
||||
# Deploy com Docker
|
||||
make deploy-hostinger HOSTINGER_HOST=SEU_IP_VPS
|
||||
|
||||
# Ou com variaveis de ambiente
|
||||
export HOSTINGER_HOST=SEU_IP_VPS
|
||||
export HOSTINGER_USER=root
|
||||
make deploy-hostinger
|
||||
```
|
||||
|
||||
### Passo 5: Verificar
|
||||
|
||||
```bash
|
||||
# Status completo
|
||||
make deploy-hostinger-status HOSTINGER_HOST=SEU_IP_VPS
|
||||
|
||||
# Health check rapido
|
||||
curl http://SEU_IP_VPS:18790/health
|
||||
```
|
||||
|
||||
## Metodo 2: Deploy via Binario (Menor consumo)
|
||||
|
||||
Para VPS com pouca memoria (512MB-1GB), o deploy via binario usa menos recursos.
|
||||
|
||||
### Setup do servidor
|
||||
|
||||
```bash
|
||||
ssh root@SEU_IP_VPS 'bash -s -- binary' < deploy/hostinger/setup-server.sh
|
||||
```
|
||||
|
||||
### Deploy
|
||||
|
||||
```bash
|
||||
# Build local e upload (mais rapido se sua maquina e potente)
|
||||
make deploy-hostinger HOSTINGER_HOST=SEU_IP_VPS HOSTINGER_DEPLOY_METHOD=binary
|
||||
|
||||
# Ou build no servidor (nao precisa de Go local)
|
||||
# O script sincroniza o codigo e compila no VPS
|
||||
```
|
||||
|
||||
### Gerenciar servico
|
||||
|
||||
```bash
|
||||
ssh root@SEU_IP_VPS
|
||||
|
||||
# Status
|
||||
systemctl status picoclaw
|
||||
|
||||
# Logs em tempo real
|
||||
tail -f /opt/picoclaw/logs/picoclaw.log
|
||||
|
||||
# Reiniciar
|
||||
systemctl restart picoclaw
|
||||
|
||||
# Parar
|
||||
systemctl stop picoclaw
|
||||
```
|
||||
|
||||
## Comandos Make
|
||||
|
||||
| Comando | Descricao |
|
||||
|---------|-----------|
|
||||
| `make deploy-hostinger-setup` | Setup inicial do servidor |
|
||||
| `make deploy-hostinger` | Deploy/atualizar PicoClaw |
|
||||
| `make deploy-hostinger-status` | Verificar status |
|
||||
| `make deploy-hostinger-rollback` | Reverter para versao anterior |
|
||||
|
||||
### Variaveis de ambiente
|
||||
|
||||
| Variavel | Padrao | Descricao |
|
||||
|----------|--------|-----------|
|
||||
| `HOSTINGER_HOST` | (obrigatorio) | IP ou hostname do VPS |
|
||||
| `HOSTINGER_USER` | `root` | Usuario SSH |
|
||||
| `HOSTINGER_SSH_KEY` | `~/.ssh/id_rsa` | Caminho da chave SSH |
|
||||
| `HOSTINGER_SSH_PORT` | `22` | Porta SSH |
|
||||
| `HOSTINGER_DEPLOY_METHOD` | `docker` | `docker` ou `binary` |
|
||||
|
||||
## Estrutura no Servidor
|
||||
|
||||
```
|
||||
/opt/picoclaw/
|
||||
├── bin/ # Binario do PicoClaw (metodo binary)
|
||||
├── config/
|
||||
│ ├── .env # Chaves de API (chmod 600)
|
||||
│ └── config.json # Configuracao do PicoClaw
|
||||
├── workspace/ # Workspace persistente
|
||||
│ ├── memory/ # Memoria de sessoes
|
||||
│ └── skills/ # Skills customizadas
|
||||
├── logs/ # Logs (metodo binary)
|
||||
├── backups/ # Backups de versoes anteriores
|
||||
└── src/ # Codigo fonte (para build remoto)
|
||||
```
|
||||
|
||||
## Seguranca
|
||||
|
||||
- Firewall (ufw) configurado: apenas SSH (22) e Gateway (18790)
|
||||
- fail2ban ativo para protecao contra brute-force SSH
|
||||
- Servico roda com usuario dedicado `picoclaw` (sem root)
|
||||
- Hardening via systemd (ProtectSystem, NoNewPrivileges, etc.)
|
||||
- Arquivos `.env` e `config.json` com permissao 600
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Container nao inicia (Docker)
|
||||
```bash
|
||||
ssh root@SEU_IP_VPS
|
||||
docker compose -f /opt/picoclaw/docker-compose.yml logs picoclaw-gateway
|
||||
```
|
||||
|
||||
### Servico nao inicia (Binary)
|
||||
```bash
|
||||
ssh root@SEU_IP_VPS
|
||||
journalctl -u picoclaw -n 50 --no-pager
|
||||
cat /opt/picoclaw/logs/picoclaw-error.log
|
||||
```
|
||||
|
||||
### Health check falha
|
||||
```bash
|
||||
# Verificar se a porta esta aberta
|
||||
ss -tlnp | grep 18790
|
||||
|
||||
# Verificar firewall
|
||||
ufw status
|
||||
|
||||
# Testar localmente no servidor
|
||||
curl -v http://localhost:18790/health
|
||||
```
|
||||
|
||||
### Memoria insuficiente
|
||||
Se o VPS tem pouca RAM, use o metodo binary em vez de Docker:
|
||||
```bash
|
||||
make deploy-hostinger HOSTINGER_HOST=SEU_IP_VPS HOSTINGER_DEPLOY_METHOD=binary
|
||||
```
|
||||
|
||||
## Atualizacoes
|
||||
|
||||
Para atualizar o PicoClaw, basta rodar novamente:
|
||||
```bash
|
||||
make deploy-hostinger HOSTINGER_HOST=SEU_IP_VPS
|
||||
```
|
||||
|
||||
Para reverter:
|
||||
```bash
|
||||
make deploy-hostinger-rollback HOSTINGER_HOST=SEU_IP_VPS
|
||||
```
|
||||
256
deploy/hostinger/deploy.sh
Executable file
256
deploy/hostinger/deploy.sh
Executable file
|
|
@ -0,0 +1,256 @@
|
|||
#!/usr/bin/env bash
|
||||
# ============================================================
|
||||
# PicoClaw - Remote Deploy to Hostinger VPS
|
||||
# ============================================================
|
||||
# Deploys PicoClaw to a remote Hostinger VPS via SSH.
|
||||
# Supports both Docker and binary deployment methods.
|
||||
#
|
||||
# Usage:
|
||||
# ./deploy/hostinger/deploy.sh [OPTIONS]
|
||||
#
|
||||
# Options:
|
||||
# -h, --host HOST VPS IP or hostname (required, or set HOSTINGER_HOST)
|
||||
# -u, --user USER SSH user (default: root)
|
||||
# -k, --key KEY SSH key path (default: ~/.ssh/id_rsa)
|
||||
# -m, --method METHOD Deploy method: "docker" or "binary" (default: docker)
|
||||
# -p, --port PORT SSH port (default: 22)
|
||||
# --build-local Build binary locally and upload (for binary method)
|
||||
# --help Show this help
|
||||
# ============================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() { echo -e "${GREEN}[DEPLOY]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; }
|
||||
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
|
||||
|
||||
# ── Default Configuration ────────────────────────────
|
||||
HOST="${HOSTINGER_HOST:-}"
|
||||
USER="${HOSTINGER_USER:-root}"
|
||||
SSH_KEY="${HOSTINGER_SSH_KEY:-${HOME}/.ssh/id_rsa}"
|
||||
SSH_PORT="${HOSTINGER_SSH_PORT:-22}"
|
||||
METHOD="${HOSTINGER_DEPLOY_METHOD:-docker}"
|
||||
BUILD_LOCAL=false
|
||||
REMOTE_DIR="/opt/picoclaw"
|
||||
|
||||
# ── Parse Arguments ──────────────────────────────────
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-h|--host) HOST="$2"; shift 2 ;;
|
||||
-u|--user) USER="$2"; shift 2 ;;
|
||||
-k|--key) SSH_KEY="$2"; shift 2 ;;
|
||||
-m|--method) METHOD="$2"; shift 2 ;;
|
||||
-p|--port) SSH_PORT="$2"; shift 2 ;;
|
||||
--build-local) BUILD_LOCAL=true; shift ;;
|
||||
--help)
|
||||
head -20 "$0" | tail -15
|
||||
exit 0
|
||||
;;
|
||||
*) error "Unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ── Validate ─────────────────────────────────────────
|
||||
[ -z "${HOST}" ] && error "Host required. Use -h/--host or set HOSTINGER_HOST env var."
|
||||
[ ! -f "${SSH_KEY}" ] && error "SSH key not found: ${SSH_KEY}"
|
||||
|
||||
SSH_OPTS="-o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 -p ${SSH_PORT} -i ${SSH_KEY}"
|
||||
SCP_OPTS="-o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 -P ${SSH_PORT} -i ${SSH_KEY}"
|
||||
SSH_CMD="ssh ${SSH_OPTS} ${USER}@${HOST}"
|
||||
SCP_CMD="scp ${SCP_OPTS}"
|
||||
|
||||
# ── Verify Connection ────────────────────────────────
|
||||
log "Verifying SSH connection to ${USER}@${HOST}:${SSH_PORT}..."
|
||||
${SSH_CMD} "echo 'Connection OK'" || error "Cannot connect to ${HOST}. Check SSH settings."
|
||||
|
||||
# ── Get project root ─────────────────────────────────
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "${PROJECT_ROOT}"
|
||||
|
||||
log "Deploying PicoClaw to ${HOST} via ${METHOD} method"
|
||||
echo ""
|
||||
|
||||
# ── Deploy via Docker ────────────────────────────────
|
||||
deploy_docker() {
|
||||
log "=== Docker Deployment ==="
|
||||
|
||||
# 1. Sync project files to server
|
||||
log "Syncing project files..."
|
||||
rsync -avz --delete \
|
||||
-e "ssh ${SSH_OPTS}" \
|
||||
--exclude '.git' \
|
||||
--exclude 'build/' \
|
||||
--exclude '.env' \
|
||||
--exclude 'config/config.json' \
|
||||
--exclude '*.test' \
|
||||
"${PROJECT_ROOT}/" "${USER}@${HOST}:${REMOTE_DIR}/src/"
|
||||
|
||||
# 2. Copy docker-compose production file
|
||||
log "Copying Docker Compose production config..."
|
||||
${SCP_CMD} "${PROJECT_ROOT}/deploy/hostinger/docker-compose.production.yml" \
|
||||
"${USER}@${HOST}:${REMOTE_DIR}/docker-compose.yml"
|
||||
|
||||
# 3. Build and restart on server
|
||||
log "Building and starting containers on server..."
|
||||
${SSH_CMD} <<'REMOTEOF'
|
||||
set -e
|
||||
cd /opt/picoclaw
|
||||
|
||||
# Build the image
|
||||
echo "[REMOTE] Building Docker image..."
|
||||
docker compose build --no-cache picoclaw-gateway
|
||||
|
||||
# Stop existing container gracefully
|
||||
echo "[REMOTE] Stopping existing container..."
|
||||
docker compose down --timeout 30 2>/dev/null || true
|
||||
|
||||
# Start new container
|
||||
echo "[REMOTE] Starting PicoClaw gateway..."
|
||||
docker compose up -d picoclaw-gateway
|
||||
|
||||
# Wait and verify
|
||||
sleep 5
|
||||
if docker compose ps picoclaw-gateway | grep -q "Up"; then
|
||||
echo "[REMOTE] PicoClaw is running!"
|
||||
docker compose ps
|
||||
else
|
||||
echo "[REMOTE] ERROR: Container failed to start"
|
||||
docker compose logs --tail=50 picoclaw-gateway
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Health check
|
||||
echo "[REMOTE] Checking health..."
|
||||
sleep 3
|
||||
if curl -sf http://localhost:18790/health > /dev/null 2>&1; then
|
||||
echo "[REMOTE] Health check PASSED"
|
||||
else
|
||||
echo "[REMOTE] Health check failed (may still be starting up)"
|
||||
fi
|
||||
|
||||
# Cleanup old images
|
||||
echo "[REMOTE] Cleaning up old Docker images..."
|
||||
docker image prune -f
|
||||
REMOTEOF
|
||||
|
||||
log "Docker deployment complete!"
|
||||
}
|
||||
|
||||
# ── Deploy via Binary ────────────────────────────────
|
||||
deploy_binary() {
|
||||
log "=== Binary Deployment ==="
|
||||
|
||||
if [ "${BUILD_LOCAL}" = true ]; then
|
||||
# Build locally for linux/amd64
|
||||
log "Building binary locally for linux/amd64..."
|
||||
cd "${PROJECT_ROOT}"
|
||||
make generate
|
||||
GOOS=linux GOARCH=amd64 go build -v \
|
||||
-ldflags "-X main.version=$(git describe --tags --always --dirty 2>/dev/null || echo dev) \
|
||||
-X main.gitCommit=$(git rev-parse --short=8 HEAD 2>/dev/null || echo dev) \
|
||||
-X main.buildTime=$(date +%FT%T%z)" \
|
||||
-o build/picoclaw-linux-amd64 ./cmd/picoclaw
|
||||
BINARY_PATH="build/picoclaw-linux-amd64"
|
||||
log "Binary built: ${BINARY_PATH}"
|
||||
else
|
||||
# Build on server
|
||||
log "Syncing source code to server..."
|
||||
rsync -avz --delete \
|
||||
-e "ssh ${SSH_OPTS}" \
|
||||
--exclude '.git' \
|
||||
--exclude 'build/' \
|
||||
--exclude '.env' \
|
||||
--exclude 'config/config.json' \
|
||||
"${PROJECT_ROOT}/" "${USER}@${HOST}:${REMOTE_DIR}/src/"
|
||||
|
||||
log "Building on server..."
|
||||
${SSH_CMD} <<'REMOTEOF'
|
||||
set -e
|
||||
cd /opt/picoclaw/src
|
||||
export PATH=$PATH:/usr/local/go/bin
|
||||
make build
|
||||
cp build/picoclaw /opt/picoclaw/bin/picoclaw
|
||||
chmod +x /opt/picoclaw/bin/picoclaw
|
||||
echo "[REMOTE] Build complete: $(/opt/picoclaw/bin/picoclaw version 2>/dev/null || echo 'built')"
|
||||
REMOTEOF
|
||||
fi
|
||||
|
||||
if [ "${BUILD_LOCAL}" = true ]; then
|
||||
# Upload binary
|
||||
log "Uploading binary to server..."
|
||||
${SCP_CMD} "${BINARY_PATH}" "${USER}@${HOST}:${REMOTE_DIR}/bin/picoclaw"
|
||||
${SSH_CMD} "chmod +x ${REMOTE_DIR}/bin/picoclaw"
|
||||
|
||||
# Initialize workspace if needed
|
||||
${SSH_CMD} <<REMOTEOF
|
||||
set -e
|
||||
if [ ! -d "${REMOTE_DIR}/workspace/memory" ]; then
|
||||
echo "[REMOTE] Initializing workspace..."
|
||||
sudo -u picoclaw ${REMOTE_DIR}/bin/picoclaw onboard
|
||||
fi
|
||||
REMOTEOF
|
||||
fi
|
||||
|
||||
# Restart service
|
||||
log "Restarting PicoClaw service..."
|
||||
${SSH_CMD} <<'REMOTEOF'
|
||||
set -e
|
||||
echo "[REMOTE] Restarting picoclaw service..."
|
||||
systemctl restart picoclaw
|
||||
|
||||
sleep 3
|
||||
if systemctl is-active --quiet picoclaw; then
|
||||
echo "[REMOTE] PicoClaw is running!"
|
||||
systemctl status picoclaw --no-pager
|
||||
else
|
||||
echo "[REMOTE] ERROR: Service failed to start"
|
||||
journalctl -u picoclaw --no-pager -n 30
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Health check
|
||||
sleep 3
|
||||
if curl -sf http://localhost:18790/health > /dev/null 2>&1; then
|
||||
echo "[REMOTE] Health check PASSED"
|
||||
else
|
||||
echo "[REMOTE] Health check failed (may still be starting up)"
|
||||
tail -20 /opt/picoclaw/logs/picoclaw.log 2>/dev/null || true
|
||||
fi
|
||||
REMOTEOF
|
||||
|
||||
log "Binary deployment complete!"
|
||||
}
|
||||
|
||||
# ── Execute Deploy ───────────────────────────────────
|
||||
DEPLOY_START=$(date +%s)
|
||||
|
||||
case "${METHOD}" in
|
||||
docker) deploy_docker ;;
|
||||
binary) deploy_binary ;;
|
||||
*) error "Unknown method: ${METHOD}. Use 'docker' or 'binary'." ;;
|
||||
esac
|
||||
|
||||
DEPLOY_END=$(date +%s)
|
||||
DEPLOY_DURATION=$((DEPLOY_END - DEPLOY_START))
|
||||
|
||||
echo ""
|
||||
log "=========================================="
|
||||
log " Deployment successful!"
|
||||
log " Duration: ${DEPLOY_DURATION}s"
|
||||
log " Host: ${HOST}"
|
||||
log " Method: ${METHOD}"
|
||||
log "=========================================="
|
||||
echo ""
|
||||
info "Useful commands:"
|
||||
echo " Check status: ${SSH_CMD} 'systemctl status picoclaw'"
|
||||
echo " View logs: ${SSH_CMD} 'tail -f /opt/picoclaw/logs/picoclaw.log'"
|
||||
echo " Health check: curl http://${HOST}:18790/health"
|
||||
echo ""
|
||||
48
deploy/hostinger/docker-compose.production.yml
Normal file
48
deploy/hostinger/docker-compose.production.yml
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# ============================================================
|
||||
# PicoClaw - Production Docker Compose for Hostinger VPS
|
||||
# ============================================================
|
||||
# Usage:
|
||||
# docker compose up -d picoclaw-gateway
|
||||
# docker compose logs -f picoclaw-gateway
|
||||
# docker compose down
|
||||
# ============================================================
|
||||
|
||||
services:
|
||||
picoclaw-gateway:
|
||||
build:
|
||||
context: ./src
|
||||
dockerfile: Dockerfile
|
||||
container_name: picoclaw-gateway
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:18790:18790"
|
||||
volumes:
|
||||
# Configuration (read-only)
|
||||
- /opt/picoclaw/config/config.json:/root/.picoclaw/config.json:ro
|
||||
# Persistent workspace (sessions, memory, logs)
|
||||
- picoclaw-workspace:/root/.picoclaw/workspace
|
||||
env_file:
|
||||
- /opt/picoclaw/config/.env
|
||||
environment:
|
||||
- TZ=${TZ:-America/Sao_Paulo}
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "--spider", "http://localhost:18790/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "5"
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 128M
|
||||
reservations:
|
||||
memory: 32M
|
||||
|
||||
volumes:
|
||||
picoclaw-workspace:
|
||||
driver: local
|
||||
734
deploy/hostinger/full-deploy.sh
Executable file
734
deploy/hostinger/full-deploy.sh
Executable file
|
|
@ -0,0 +1,734 @@
|
|||
#!/usr/bin/env bash
|
||||
# ============================================================
|
||||
# PicoClaw - Full Hostinger VPS Deploy (All-in-One)
|
||||
# ============================================================
|
||||
# This script does EVERYTHING via SSH in a single run:
|
||||
# 1. Server setup (packages, Docker, firewall, fail2ban)
|
||||
# 2. User & directory creation
|
||||
# 3. Configuration files (prompts for API keys)
|
||||
# 4. Source code sync & Docker build
|
||||
# 5. Start service & health check verification
|
||||
#
|
||||
# Usage:
|
||||
# ./deploy/hostinger/full-deploy.sh -h YOUR_VPS_IP [OPTIONS]
|
||||
#
|
||||
# Examples:
|
||||
# # Interactive (prompts for API keys):
|
||||
# ./deploy/hostinger/full-deploy.sh -h 149.28.10.50
|
||||
#
|
||||
# # Non-interactive with env vars:
|
||||
# LLM_PROVIDER=anthropic \
|
||||
# LLM_API_KEY=sk-ant-xxx \
|
||||
# CHAT_CHANNEL=telegram \
|
||||
# CHAT_TOKEN=123456:ABC \
|
||||
# ./deploy/hostinger/full-deploy.sh -h 149.28.10.50
|
||||
#
|
||||
# Options:
|
||||
# -h, --host HOST VPS IP or hostname (required)
|
||||
# -u, --user USER SSH user (default: root)
|
||||
# -k, --key KEY SSH key path (default: ~/.ssh/id_rsa)
|
||||
# -p, --port PORT SSH port (default: 22)
|
||||
# -m, --method METHOD "docker" or "binary" (default: docker)
|
||||
# --skip-setup Skip server provisioning (if already done)
|
||||
# --skip-config Skip config prompts (use existing server config)
|
||||
# --yes Skip confirmation prompts
|
||||
# --help Show this help
|
||||
# ============================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Colors ───────────────────────────────────────────
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() { echo -e "${GREEN}>>>${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; }
|
||||
info() { echo -e "${BLUE}[i]${NC} $*"; }
|
||||
step() { echo -e "\n${CYAN}${BOLD}── $1 ──${NC}"; }
|
||||
success() { echo -e "${GREEN}${BOLD}$*${NC}"; }
|
||||
|
||||
# ── Default Configuration ────────────────────────────
|
||||
HOST=""
|
||||
SSH_USER="root"
|
||||
SSH_KEY="${HOME}/.ssh/id_rsa"
|
||||
SSH_PORT="22"
|
||||
SSH_PASSWORD=""
|
||||
METHOD="docker"
|
||||
SKIP_SETUP=false
|
||||
SKIP_CONFIG=false
|
||||
AUTO_YES=false
|
||||
REMOTE_DIR="/opt/picoclaw"
|
||||
|
||||
# Pre-set via env (for non-interactive deploys)
|
||||
LLM_PROVIDER="${LLM_PROVIDER:-}"
|
||||
LLM_API_KEY="${LLM_API_KEY:-}"
|
||||
CHAT_CHANNEL="${CHAT_CHANNEL:-}"
|
||||
CHAT_TOKEN="${CHAT_TOKEN:-}"
|
||||
CHAT_ALLOW_FROM="${CHAT_ALLOW_FROM:-}"
|
||||
DEPLOY_TIMEZONE="${DEPLOY_TIMEZONE:-America/Sao_Paulo}"
|
||||
|
||||
# ── Load .deploy.env if exists ──────────────────────
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
if [ -f "${SCRIPT_DIR}/.deploy.env" ]; then
|
||||
# shellcheck disable=SC1091
|
||||
source "${SCRIPT_DIR}/.deploy.env"
|
||||
# Map DEPLOY_* vars to internal vars
|
||||
HOST="${DEPLOY_HOST:-${HOST}}"
|
||||
SSH_USER="${DEPLOY_USER:-${SSH_USER}}"
|
||||
SSH_PORT="${DEPLOY_PORT:-${SSH_PORT}}"
|
||||
SSH_PASSWORD="${DEPLOY_PASSWORD:-${SSH_PASSWORD}}"
|
||||
METHOD="${DEPLOY_METHOD:-${METHOD}}"
|
||||
if [ -n "${DEPLOY_SSH_KEY:-}" ]; then
|
||||
SSH_KEY="${DEPLOY_SSH_KEY}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Parse Arguments ──────────────────────────────────
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-h|--host) HOST="$2"; shift 2 ;;
|
||||
-u|--user) SSH_USER="$2"; shift 2 ;;
|
||||
-k|--key) SSH_KEY="$2"; shift 2 ;;
|
||||
-p|--port) SSH_PORT="$2"; shift 2 ;;
|
||||
-m|--method) METHOD="$2"; shift 2 ;;
|
||||
--skip-setup) SKIP_SETUP=true; shift ;;
|
||||
--skip-config) SKIP_CONFIG=true; shift ;;
|
||||
--yes) AUTO_YES=true; shift ;;
|
||||
--help)
|
||||
sed -n '2,/^# =====/p' "$0" | head -n -1 | sed 's/^# //' | sed 's/^#//'
|
||||
exit 0
|
||||
;;
|
||||
*) error "Unknown option: $1. Use --help for usage." ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ── Validate ─────────────────────────────────────────
|
||||
[ -z "${HOST}" ] && error "Host is required. Usage: $0 -h YOUR_VPS_IP"
|
||||
|
||||
SSHPASS_CMD=""
|
||||
if [ -n "${SSH_PASSWORD}" ]; then
|
||||
# Use sshpass for password-based authentication
|
||||
if ! command -v sshpass &>/dev/null; then
|
||||
warn "sshpass not installed. Installing..."
|
||||
if command -v apt-get &>/dev/null; then
|
||||
sudo apt-get install -y -qq sshpass 2>/dev/null
|
||||
elif command -v brew &>/dev/null; then
|
||||
brew install sshpass 2>/dev/null || brew install hudochenkov/sshpass/sshpass 2>/dev/null
|
||||
fi
|
||||
command -v sshpass &>/dev/null || error "Could not install sshpass. Install it manually: apt-get install sshpass"
|
||||
fi
|
||||
export SSHPASS="${SSH_PASSWORD}"
|
||||
SSHPASS_CMD="sshpass -e"
|
||||
SSH_OPTS="-o StrictHostKeyChecking=accept-new -o ConnectTimeout=15 -p ${SSH_PORT}"
|
||||
SCP_OPTS="-o StrictHostKeyChecking=accept-new -o ConnectTimeout=15 -P ${SSH_PORT}"
|
||||
info "Using password from .deploy.env"
|
||||
elif [ -f "${SSH_KEY}" ]; then
|
||||
SSH_OPTS="-o StrictHostKeyChecking=accept-new -o ConnectTimeout=15 -p ${SSH_PORT} -i ${SSH_KEY}"
|
||||
SCP_OPTS="-o StrictHostKeyChecking=accept-new -o ConnectTimeout=15 -P ${SSH_PORT} -i ${SSH_KEY}"
|
||||
else
|
||||
warn "SSH key not found at ${SSH_KEY} and no password configured."
|
||||
info "Will attempt interactive password SSH. For automation, create deploy/hostinger/.deploy.env"
|
||||
SSH_OPTS="-o StrictHostKeyChecking=accept-new -o ConnectTimeout=15 -p ${SSH_PORT}"
|
||||
SCP_OPTS="-o StrictHostKeyChecking=accept-new -o ConnectTimeout=15 -P ${SSH_PORT}"
|
||||
fi
|
||||
|
||||
SSH_CMD="${SSHPASS_CMD} ssh ${SSH_OPTS} ${SSH_USER}@${HOST}"
|
||||
SCP_CMD="${SSHPASS_CMD} scp ${SCP_OPTS}"
|
||||
|
||||
# ── Get project root ─────────────────────────────────
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
|
||||
# ── Banner ───────────────────────────────────────────
|
||||
echo ""
|
||||
echo -e "${CYAN}${BOLD}"
|
||||
echo " ╔═══════════════════════════════════════════╗"
|
||||
echo " ║ PicoClaw - Hostinger Full Deploy ║"
|
||||
echo " ╚═══════════════════════════════════════════╝"
|
||||
echo -e "${NC}"
|
||||
echo " Host: ${HOST}"
|
||||
echo " User: ${SSH_USER}"
|
||||
echo " Method: ${METHOD}"
|
||||
if [ -n "${SSH_PASSWORD}" ]; then
|
||||
echo " Auth: password (from .deploy.env)"
|
||||
elif [ -f "${SSH_KEY}" ]; then
|
||||
echo " Auth: SSH key (${SSH_KEY})"
|
||||
else
|
||||
echo " Auth: interactive password"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
if [ "${AUTO_YES}" = false ]; then
|
||||
read -rp " Continue with deployment? [Y/n] " confirm
|
||||
case "${confirm}" in
|
||||
[nN]*) echo "Aborted."; exit 0 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
DEPLOY_START=$(date +%s)
|
||||
|
||||
# ════════════════════════════════════════════════════
|
||||
# PHASE 1: Verify SSH Connection
|
||||
# ════════════════════════════════════════════════════
|
||||
step "Phase 1/5: Checking SSH connection"
|
||||
|
||||
if ${SSH_CMD} "echo 'ok'" &>/dev/null; then
|
||||
log "SSH connection to ${SSH_USER}@${HOST}:${SSH_PORT} OK"
|
||||
else
|
||||
error "Cannot connect via SSH to ${SSH_USER}@${HOST}:${SSH_PORT}
|
||||
|
||||
Troubleshooting:
|
||||
- Verify the VPS IP in Hostinger hPanel
|
||||
- Enable SSH access in hPanel > Advanced > SSH Access
|
||||
- Check your SSH key: ssh-keygen -t ed25519 && ssh-copy-id ${SSH_USER}@${HOST}
|
||||
- Try with password: $0 -h ${HOST} (without -k flag)"
|
||||
fi
|
||||
|
||||
# Detect remote OS
|
||||
REMOTE_OS=$(${SSH_CMD} "cat /etc/os-release 2>/dev/null | head -3 || echo 'unknown'" 2>/dev/null)
|
||||
info "Remote OS: $(echo "${REMOTE_OS}" | grep PRETTY_NAME | cut -d= -f2 | tr -d '"' || echo 'detected')"
|
||||
|
||||
# ════════════════════════════════════════════════════
|
||||
# PHASE 2: Server Provisioning
|
||||
# ════════════════════════════════════════════════════
|
||||
if [ "${SKIP_SETUP}" = false ]; then
|
||||
step "Phase 2/5: Server provisioning"
|
||||
log "Installing packages, Docker, firewall, fail2ban..."
|
||||
|
||||
${SSH_CMD} "bash -s -- ${METHOD}" <<'SETUP_SCRIPT'
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
METHOD="${1:-docker}"
|
||||
|
||||
echo "[REMOTE] Updating system packages..."
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
if command -v apt-get &>/dev/null; then
|
||||
apt-get update -qq
|
||||
apt-get upgrade -y -qq
|
||||
apt-get install -y -qq curl wget git ufw fail2ban unzip jq rsync
|
||||
elif command -v yum &>/dev/null; then
|
||||
yum update -y -q
|
||||
yum install -y -q curl wget git firewalld fail2ban unzip jq rsync
|
||||
elif command -v dnf &>/dev/null; then
|
||||
dnf update -y -q
|
||||
dnf install -y -q curl wget git firewalld fail2ban unzip jq rsync
|
||||
fi
|
||||
|
||||
# Create dedicated user
|
||||
if ! id picoclaw &>/dev/null; then
|
||||
echo "[REMOTE] Creating picoclaw user..."
|
||||
useradd --system --create-home --home-dir /opt/picoclaw --shell /bin/bash picoclaw
|
||||
fi
|
||||
|
||||
# Directory structure
|
||||
echo "[REMOTE] Creating directories..."
|
||||
mkdir -p /opt/picoclaw/{bin,config,workspace,logs,backups,src}
|
||||
chown -R picoclaw:picoclaw /opt/picoclaw
|
||||
|
||||
# Docker
|
||||
if [ "${METHOD}" = "docker" ]; then
|
||||
if ! command -v docker &>/dev/null; then
|
||||
echo "[REMOTE] Installing Docker..."
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
systemctl enable docker
|
||||
systemctl start docker
|
||||
usermod -aG docker picoclaw
|
||||
fi
|
||||
echo "[REMOTE] Docker: $(docker --version 2>/dev/null)"
|
||||
|
||||
if ! docker compose version &>/dev/null; then
|
||||
echo "[REMOTE] Installing Docker Compose..."
|
||||
apt-get install -y -qq docker-compose-plugin 2>/dev/null || true
|
||||
fi
|
||||
echo "[REMOTE] Docker Compose: $(docker compose version 2>/dev/null || echo 'not available')"
|
||||
else
|
||||
# Binary method: install Go
|
||||
if ! command -v go &>/dev/null; then
|
||||
echo "[REMOTE] Installing Go..."
|
||||
GO_VERSION="1.23.4"
|
||||
ARCH=$(uname -m)
|
||||
case "${ARCH}" in
|
||||
x86_64) GO_ARCH="amd64" ;;
|
||||
aarch64) GO_ARCH="arm64" ;;
|
||||
*) GO_ARCH="${ARCH}" ;;
|
||||
esac
|
||||
curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${GO_ARCH}.tar.gz" -o /tmp/go.tar.gz
|
||||
rm -rf /usr/local/go
|
||||
tar -C /usr/local -xzf /tmp/go.tar.gz
|
||||
rm /tmp/go.tar.gz
|
||||
echo 'export PATH=$PATH:/usr/local/go/bin' > /etc/profile.d/go.sh
|
||||
export PATH=$PATH:/usr/local/go/bin
|
||||
fi
|
||||
echo "[REMOTE] Go: $(go version 2>/dev/null || echo 'installed')"
|
||||
|
||||
if ! command -v make &>/dev/null; then
|
||||
apt-get install -y -qq make 2>/dev/null || yum install -y -q make 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Install systemd service
|
||||
cat > /etc/systemd/system/picoclaw.service <<'SVCEOF'
|
||||
[Unit]
|
||||
Description=PicoClaw AI Assistant Gateway
|
||||
Documentation=https://github.com/agenciaspace/picoclaw
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=5
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=picoclaw
|
||||
Group=picoclaw
|
||||
WorkingDirectory=/opt/picoclaw
|
||||
ExecStart=/opt/picoclaw/bin/picoclaw gateway
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
TimeoutStopSec=30
|
||||
|
||||
StandardOutput=append:/opt/picoclaw/logs/picoclaw.log
|
||||
StandardError=append:/opt/picoclaw/logs/picoclaw-error.log
|
||||
|
||||
EnvironmentFile=-/opt/picoclaw/config/.env
|
||||
Environment=HOME=/opt/picoclaw
|
||||
Environment=PICOCLAW_CONFIG=/opt/picoclaw/config/config.json
|
||||
|
||||
NoNewPrivileges=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
ReadWritePaths=/opt/picoclaw
|
||||
PrivateTmp=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectControlGroups=yes
|
||||
RestrictSUIDSGID=yes
|
||||
RestrictNamespaces=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
SVCEOF
|
||||
systemctl daemon-reload
|
||||
systemctl enable picoclaw
|
||||
fi
|
||||
|
||||
# Firewall
|
||||
echo "[REMOTE] Configuring firewall..."
|
||||
if command -v ufw &>/dev/null; then
|
||||
ufw --force reset >/dev/null 2>&1
|
||||
ufw default deny incoming >/dev/null 2>&1
|
||||
ufw default allow outgoing >/dev/null 2>&1
|
||||
ufw allow ssh >/dev/null 2>&1
|
||||
ufw allow 18790/tcp >/dev/null 2>&1
|
||||
ufw --force enable >/dev/null 2>&1
|
||||
echo "[REMOTE] UFW: enabled (SSH + 18790)"
|
||||
elif command -v firewall-cmd &>/dev/null; then
|
||||
systemctl enable firewalld >/dev/null 2>&1
|
||||
systemctl start firewalld >/dev/null 2>&1
|
||||
firewall-cmd --permanent --add-service=ssh >/dev/null 2>&1
|
||||
firewall-cmd --permanent --add-port=18790/tcp >/dev/null 2>&1
|
||||
firewall-cmd --reload >/dev/null 2>&1
|
||||
echo "[REMOTE] firewalld: enabled (SSH + 18790)"
|
||||
fi
|
||||
|
||||
# fail2ban
|
||||
systemctl enable fail2ban >/dev/null 2>&1
|
||||
systemctl start fail2ban >/dev/null 2>&1
|
||||
echo "[REMOTE] fail2ban: active"
|
||||
|
||||
# Logrotate
|
||||
cat > /etc/logrotate.d/picoclaw <<'LOGEOF'
|
||||
/opt/picoclaw/logs/*.log {
|
||||
daily
|
||||
missingok
|
||||
rotate 14
|
||||
compress
|
||||
delaycompress
|
||||
notifempty
|
||||
create 0640 picoclaw picoclaw
|
||||
}
|
||||
LOGEOF
|
||||
|
||||
echo "[REMOTE] Server provisioning complete!"
|
||||
SETUP_SCRIPT
|
||||
|
||||
log "Server provisioning done"
|
||||
else
|
||||
step "Phase 2/5: Server provisioning (SKIPPED)"
|
||||
fi
|
||||
|
||||
# ════════════════════════════════════════════════════
|
||||
# PHASE 3: Configuration
|
||||
# ════════════════════════════════════════════════════
|
||||
if [ "${SKIP_CONFIG}" = false ]; then
|
||||
step "Phase 3/5: Configuration"
|
||||
|
||||
# Collect config interactively if not provided via env
|
||||
if [ -z "${LLM_PROVIDER}" ]; then
|
||||
echo ""
|
||||
echo " Which LLM provider will you use?"
|
||||
echo " 1) anthropic (Claude)"
|
||||
echo " 2) openai (GPT)"
|
||||
echo " 3) openrouter (Multiple models)"
|
||||
echo " 4) gemini (Google)"
|
||||
echo " 5) zhipu (GLM)"
|
||||
echo " 6) groq (Fast inference)"
|
||||
echo " 7) ollama (Local/self-hosted)"
|
||||
echo ""
|
||||
read -rp " Provider [1-7]: " provider_choice
|
||||
case "${provider_choice}" in
|
||||
1) LLM_PROVIDER="anthropic" ;;
|
||||
2) LLM_PROVIDER="openai" ;;
|
||||
3) LLM_PROVIDER="openrouter" ;;
|
||||
4) LLM_PROVIDER="gemini" ;;
|
||||
5) LLM_PROVIDER="zhipu" ;;
|
||||
6) LLM_PROVIDER="groq" ;;
|
||||
7) LLM_PROVIDER="ollama" ;;
|
||||
*) LLM_PROVIDER="anthropic" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [ -z "${LLM_API_KEY}" ] && [ "${LLM_PROVIDER}" != "ollama" ]; then
|
||||
read -rp " ${LLM_PROVIDER} API key: " LLM_API_KEY
|
||||
[ -z "${LLM_API_KEY}" ] && warn "No API key provided. You can set it later on the server."
|
||||
fi
|
||||
|
||||
if [ -z "${CHAT_CHANNEL}" ]; then
|
||||
echo ""
|
||||
echo " Which chat channel? (optional, press Enter to skip)"
|
||||
echo " 1) telegram"
|
||||
echo " 2) discord"
|
||||
echo " 3) slack"
|
||||
echo " 4) whatsapp"
|
||||
echo " 5) line"
|
||||
echo " 6) none (gateway API only)"
|
||||
echo ""
|
||||
read -rp " Channel [1-6]: " channel_choice
|
||||
case "${channel_choice}" in
|
||||
1) CHAT_CHANNEL="telegram" ;;
|
||||
2) CHAT_CHANNEL="discord" ;;
|
||||
3) CHAT_CHANNEL="slack" ;;
|
||||
4) CHAT_CHANNEL="whatsapp" ;;
|
||||
5) CHAT_CHANNEL="line" ;;
|
||||
*) CHAT_CHANNEL="" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [ -n "${CHAT_CHANNEL}" ] && [ -z "${CHAT_TOKEN}" ]; then
|
||||
read -rp " ${CHAT_CHANNEL} bot token: " CHAT_TOKEN
|
||||
fi
|
||||
|
||||
if [ -n "${CHAT_CHANNEL}" ] && [ -z "${CHAT_ALLOW_FROM}" ]; then
|
||||
read -rp " Allowed user IDs (comma-separated, or * for all): " CHAT_ALLOW_FROM
|
||||
fi
|
||||
|
||||
# Determine model based on provider
|
||||
case "${LLM_PROVIDER}" in
|
||||
anthropic) DEFAULT_MODEL="claude-sonnet-4-20250514" ;;
|
||||
openai) DEFAULT_MODEL="gpt-4o" ;;
|
||||
openrouter) DEFAULT_MODEL="anthropic/claude-sonnet-4" ;;
|
||||
gemini) DEFAULT_MODEL="gemini-2.0-flash" ;;
|
||||
zhipu) DEFAULT_MODEL="glm-4" ;;
|
||||
groq) DEFAULT_MODEL="llama-3.3-70b-versatile" ;;
|
||||
ollama) DEFAULT_MODEL="llama3.2" ;;
|
||||
*) DEFAULT_MODEL="claude-sonnet-4-20250514" ;;
|
||||
esac
|
||||
|
||||
# Build env key name
|
||||
case "${LLM_PROVIDER}" in
|
||||
anthropic) ENV_KEY_NAME="ANTHROPIC_API_KEY" ;;
|
||||
openai) ENV_KEY_NAME="OPENAI_API_KEY" ;;
|
||||
openrouter) ENV_KEY_NAME="OPENROUTER_API_KEY" ;;
|
||||
gemini) ENV_KEY_NAME="GEMINI_API_KEY" ;;
|
||||
zhipu) ENV_KEY_NAME="ZHIPU_API_KEY" ;;
|
||||
groq) ENV_KEY_NAME="GROQ_API_KEY" ;;
|
||||
ollama) ENV_KEY_NAME="" ;;
|
||||
*) ENV_KEY_NAME="${LLM_PROVIDER^^}_API_KEY" ;;
|
||||
esac
|
||||
|
||||
# Build chat token env name
|
||||
case "${CHAT_CHANNEL}" in
|
||||
telegram) CHAT_ENV_KEY="TELEGRAM_BOT_TOKEN" ;;
|
||||
discord) CHAT_ENV_KEY="DISCORD_BOT_TOKEN" ;;
|
||||
slack) CHAT_ENV_KEY="SLACK_BOT_TOKEN" ;;
|
||||
*) CHAT_ENV_KEY="" ;;
|
||||
esac
|
||||
|
||||
# Format allow_from as JSON array
|
||||
if [ -n "${CHAT_ALLOW_FROM}" ] && [ "${CHAT_ALLOW_FROM}" != "*" ]; then
|
||||
ALLOW_FROM_JSON=$(echo "${CHAT_ALLOW_FROM}" | tr ',' '\n' | sed 's/^ *//;s/ *$//' | awk '{printf "\"%s\",", $0}' | sed 's/,$//')
|
||||
ALLOW_FROM_JSON="[${ALLOW_FROM_JSON}]"
|
||||
else
|
||||
ALLOW_FROM_JSON="[]"
|
||||
fi
|
||||
|
||||
log "Writing configuration to server..."
|
||||
|
||||
# Write .env file
|
||||
${SSH_CMD} "cat > /opt/picoclaw/config/.env" <<ENVEOF
|
||||
# PicoClaw Production Environment
|
||||
# Generated: $(date -Iseconds)
|
||||
|
||||
# LLM Provider
|
||||
${ENV_KEY_NAME:+${ENV_KEY_NAME}=${LLM_API_KEY}}
|
||||
|
||||
# Chat Channel
|
||||
${CHAT_ENV_KEY:+${CHAT_ENV_KEY}=${CHAT_TOKEN}}
|
||||
|
||||
# Timezone
|
||||
TZ=${DEPLOY_TIMEZONE}
|
||||
ENVEOF
|
||||
|
||||
# Write config.json
|
||||
CHANNEL_ENABLED="false"
|
||||
[ -n "${CHAT_CHANNEL}" ] && CHANNEL_ENABLED="true"
|
||||
|
||||
${SSH_CMD} "cat > /opt/picoclaw/config/config.json" <<CFGEOF
|
||||
{
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"workspace": "/opt/picoclaw/workspace",
|
||||
"restrict_to_workspace": true,
|
||||
"model": "${DEFAULT_MODEL}",
|
||||
"max_tokens": 8192,
|
||||
"temperature": 0.7,
|
||||
"max_tool_iterations": 20
|
||||
}
|
||||
},
|
||||
"channels": {
|
||||
"telegram": {
|
||||
"enabled": $([ "${CHAT_CHANNEL}" = "telegram" ] && echo "true" || echo "false"),
|
||||
"token": "$([ "${CHAT_CHANNEL}" = "telegram" ] && echo "${CHAT_TOKEN}" || echo "")",
|
||||
"allow_from": $([ "${CHAT_CHANNEL}" = "telegram" ] && echo "${ALLOW_FROM_JSON}" || echo "[]")
|
||||
},
|
||||
"discord": {
|
||||
"enabled": $([ "${CHAT_CHANNEL}" = "discord" ] && echo "true" || echo "false"),
|
||||
"token": "$([ "${CHAT_CHANNEL}" = "discord" ] && echo "${CHAT_TOKEN}" || echo "")",
|
||||
"allow_from": $([ "${CHAT_CHANNEL}" = "discord" ] && echo "${ALLOW_FROM_JSON}" || echo "[]")
|
||||
},
|
||||
"slack": {
|
||||
"enabled": $([ "${CHAT_CHANNEL}" = "slack" ] && echo "true" || echo "false"),
|
||||
"bot_token": "$([ "${CHAT_CHANNEL}" = "slack" ] && echo "${CHAT_TOKEN}" || echo "")",
|
||||
"app_token": "",
|
||||
"allow_from": $([ "${CHAT_CHANNEL}" = "slack" ] && echo "${ALLOW_FROM_JSON}" || echo "[]")
|
||||
}
|
||||
},
|
||||
"providers": {
|
||||
"${LLM_PROVIDER}": {
|
||||
"api_key": "${LLM_API_KEY}",
|
||||
"api_base": ""
|
||||
}
|
||||
},
|
||||
"heartbeat": {
|
||||
"enabled": true,
|
||||
"interval": 30
|
||||
},
|
||||
"gateway": {
|
||||
"host": "0.0.0.0",
|
||||
"port": 18790
|
||||
}
|
||||
}
|
||||
CFGEOF
|
||||
|
||||
# Secure config files
|
||||
${SSH_CMD} "chown picoclaw:picoclaw /opt/picoclaw/config/.env /opt/picoclaw/config/config.json && chmod 600 /opt/picoclaw/config/.env /opt/picoclaw/config/config.json"
|
||||
|
||||
log "Configuration written"
|
||||
info "Provider: ${LLM_PROVIDER} (model: ${DEFAULT_MODEL})"
|
||||
[ -n "${CHAT_CHANNEL}" ] && info "Channel: ${CHAT_CHANNEL} (enabled)" || info "Channel: none (gateway API only)"
|
||||
else
|
||||
step "Phase 3/5: Configuration (SKIPPED - using existing)"
|
||||
fi
|
||||
|
||||
# ════════════════════════════════════════════════════
|
||||
# PHASE 4: Build & Deploy
|
||||
# ════════════════════════════════════════════════════
|
||||
step "Phase 4/5: Build & Deploy (${METHOD})"
|
||||
|
||||
cd "${PROJECT_ROOT}"
|
||||
|
||||
if [ "${METHOD}" = "docker" ]; then
|
||||
# ── Docker Deploy ────────────────────────────────
|
||||
log "Syncing project files to server..."
|
||||
rsync -az --delete \
|
||||
-e "${SSHPASS_CMD} ssh ${SSH_OPTS}" \
|
||||
--exclude '.git' \
|
||||
--exclude 'build/' \
|
||||
--exclude '.env' \
|
||||
--exclude 'node_modules/' \
|
||||
"${PROJECT_ROOT}/" "${SSH_USER}@${HOST}:${REMOTE_DIR}/src/"
|
||||
|
||||
log "Copying Docker Compose config..."
|
||||
${SCP_CMD} "${PROJECT_ROOT}/deploy/hostinger/docker-compose.production.yml" \
|
||||
"${SSH_USER}@${HOST}:${REMOTE_DIR}/docker-compose.yml"
|
||||
|
||||
log "Building Docker image & starting container..."
|
||||
${SSH_CMD} <<'DOCKER_DEPLOY'
|
||||
set -e
|
||||
cd /opt/picoclaw
|
||||
|
||||
echo "[REMOTE] Building Docker image (this may take a few minutes)..."
|
||||
docker compose build picoclaw-gateway
|
||||
|
||||
echo "[REMOTE] Stopping existing container..."
|
||||
docker compose down --timeout 30 2>/dev/null || true
|
||||
|
||||
echo "[REMOTE] Starting PicoClaw gateway..."
|
||||
docker compose up -d picoclaw-gateway
|
||||
|
||||
echo "[REMOTE] Waiting for container to start..."
|
||||
sleep 5
|
||||
|
||||
if docker compose ps picoclaw-gateway 2>/dev/null | grep -q "Up\|running"; then
|
||||
echo "[REMOTE] Container is running!"
|
||||
docker compose ps
|
||||
else
|
||||
echo "[REMOTE] Container status:"
|
||||
docker compose ps
|
||||
echo ""
|
||||
echo "[REMOTE] Container logs:"
|
||||
docker compose logs --tail=30 picoclaw-gateway
|
||||
fi
|
||||
|
||||
docker image prune -f >/dev/null 2>&1
|
||||
echo "[REMOTE] Docker deploy complete"
|
||||
DOCKER_DEPLOY
|
||||
|
||||
else
|
||||
# ── Binary Deploy ────────────────────────────────
|
||||
log "Syncing source code to server..."
|
||||
rsync -az --delete \
|
||||
-e "${SSHPASS_CMD} ssh ${SSH_OPTS}" \
|
||||
--exclude '.git' \
|
||||
--exclude 'build/' \
|
||||
--exclude '.env' \
|
||||
"${PROJECT_ROOT}/" "${SSH_USER}@${HOST}:${REMOTE_DIR}/src/"
|
||||
|
||||
log "Building and installing binary on server..."
|
||||
${SSH_CMD} <<'BINARY_DEPLOY'
|
||||
set -e
|
||||
cd /opt/picoclaw/src
|
||||
export PATH=$PATH:/usr/local/go/bin
|
||||
|
||||
echo "[REMOTE] Building PicoClaw..."
|
||||
make build
|
||||
|
||||
# Backup current binary
|
||||
if [ -f /opt/picoclaw/bin/picoclaw ]; then
|
||||
cp /opt/picoclaw/bin/picoclaw "/opt/picoclaw/backups/picoclaw-$(date +%Y%m%d%H%M%S).bak"
|
||||
fi
|
||||
|
||||
cp build/picoclaw /opt/picoclaw/bin/picoclaw
|
||||
chmod +x /opt/picoclaw/bin/picoclaw
|
||||
|
||||
echo "[REMOTE] Restarting picoclaw service..."
|
||||
systemctl restart picoclaw
|
||||
|
||||
sleep 3
|
||||
if systemctl is-active --quiet picoclaw; then
|
||||
echo "[REMOTE] Service is running"
|
||||
systemctl status picoclaw --no-pager -l
|
||||
else
|
||||
echo "[REMOTE] Service status:"
|
||||
systemctl status picoclaw --no-pager -l || true
|
||||
echo "[REMOTE] Recent logs:"
|
||||
journalctl -u picoclaw --no-pager -n 20 || true
|
||||
fi
|
||||
|
||||
echo "[REMOTE] Binary deploy complete"
|
||||
BINARY_DEPLOY
|
||||
fi
|
||||
|
||||
log "Build & deploy done"
|
||||
|
||||
# ════════════════════════════════════════════════════
|
||||
# PHASE 5: Verification
|
||||
# ════════════════════════════════════════════════════
|
||||
step "Phase 5/5: Verification"
|
||||
|
||||
log "Running health checks..."
|
||||
|
||||
HEALTH_OK=false
|
||||
for i in 1 2 3 4 5; do
|
||||
sleep 3
|
||||
if ${SSH_CMD} "curl -sf http://localhost:18790/health" &>/dev/null; then
|
||||
HEALTH_OK=true
|
||||
break
|
||||
fi
|
||||
info "Waiting for gateway to start... (attempt ${i}/5)"
|
||||
done
|
||||
|
||||
# Get final status
|
||||
${SSH_CMD} <<'VERIFY_SCRIPT'
|
||||
echo ""
|
||||
echo "── Server ────────────────────────────────────"
|
||||
printf " %-12s %s\n" "Hostname:" "$(hostname)"
|
||||
printf " %-12s %s\n" "Memory:" "$(free -h | awk '/Mem:/ {print $3 "/" $2}')"
|
||||
printf " %-12s %s\n" "Disk:" "$(df -h / | awk 'NR==2 {print $3 "/" $2 " (" $5 ")"}')"
|
||||
echo ""
|
||||
|
||||
echo "── PicoClaw ──────────────────────────────────"
|
||||
if docker compose -f /opt/picoclaw/docker-compose.yml ps 2>/dev/null | grep -q picoclaw; then
|
||||
printf " %-12s %s\n" "Method:" "Docker"
|
||||
STATUS=$(docker compose -f /opt/picoclaw/docker-compose.yml ps --format '{{.Status}}' picoclaw-gateway 2>/dev/null)
|
||||
printf " %-12s %s\n" "Container:" "${STATUS}"
|
||||
elif systemctl is-active --quiet picoclaw 2>/dev/null; then
|
||||
printf " %-12s %s\n" "Method:" "Binary (systemd)"
|
||||
printf " %-12s %s\n" "Status:" "running"
|
||||
else
|
||||
printf " %-12s %s\n" "Status:" "unknown"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "── Health Check ──────────────────────────────"
|
||||
if curl -sf http://localhost:18790/health >/dev/null 2>&1; then
|
||||
printf " %-12s %s\n" "Gateway:" "HEALTHY (port 18790)"
|
||||
else
|
||||
printf " %-12s %s\n" "Gateway:" "not responding yet (may still be starting)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "── Firewall ──────────────────────────────────"
|
||||
if command -v ufw &>/dev/null; then
|
||||
ufw status | grep -E "^(Status|18790|22)" 2>/dev/null || true
|
||||
fi
|
||||
VERIFY_SCRIPT
|
||||
|
||||
# ════════════════════════════════════════════════════
|
||||
# Summary
|
||||
# ════════════════════════════════════════════════════
|
||||
DEPLOY_END=$(date +%s)
|
||||
DEPLOY_DURATION=$((DEPLOY_END - DEPLOY_START))
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}${BOLD}"
|
||||
echo " ╔═══════════════════════════════════════════╗"
|
||||
if [ "${HEALTH_OK}" = true ]; then
|
||||
echo " ║ Deploy completed successfully! ║"
|
||||
else
|
||||
echo " ║ Deploy finished (check status) ║"
|
||||
fi
|
||||
echo " ╚═══════════════════════════════════════════╝"
|
||||
echo -e "${NC}"
|
||||
echo " Host: ${HOST}"
|
||||
echo " Method: ${METHOD}"
|
||||
echo " Duration: ${DEPLOY_DURATION}s"
|
||||
echo " Gateway: http://${HOST}:18790"
|
||||
if [ "${HEALTH_OK}" = true ]; then
|
||||
echo -e " Health: ${GREEN}HEALTHY${NC}"
|
||||
else
|
||||
echo -e " Health: ${YELLOW}Starting up (check in a few seconds)${NC}"
|
||||
fi
|
||||
echo ""
|
||||
echo " Quick commands:"
|
||||
echo " Status: ssh ${SSH_USER}@${HOST} 'docker compose -f /opt/picoclaw/docker-compose.yml ps'"
|
||||
echo " Logs: ssh ${SSH_USER}@${HOST} 'docker compose -f /opt/picoclaw/docker-compose.yml logs -f'"
|
||||
echo " Health: curl http://${HOST}:18790/health"
|
||||
echo " Redeploy: $0 -h ${HOST} --skip-setup --skip-config"
|
||||
echo ""
|
||||
51
deploy/hostinger/picoclaw.service
Normal file
51
deploy/hostinger/picoclaw.service
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# ============================================================
|
||||
# PicoClaw - systemd Service File
|
||||
# ============================================================
|
||||
# Install: sudo cp picoclaw.service /etc/systemd/system/
|
||||
# sudo systemctl daemon-reload
|
||||
# sudo systemctl enable picoclaw
|
||||
# sudo systemctl start picoclaw
|
||||
# ============================================================
|
||||
|
||||
[Unit]
|
||||
Description=PicoClaw AI Assistant Gateway
|
||||
Documentation=https://github.com/agenciaspace/picoclaw
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=5
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=picoclaw
|
||||
Group=picoclaw
|
||||
WorkingDirectory=/opt/picoclaw
|
||||
ExecStart=/opt/picoclaw/bin/picoclaw gateway
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
TimeoutStopSec=30
|
||||
|
||||
# Logging
|
||||
StandardOutput=append:/opt/picoclaw/logs/picoclaw.log
|
||||
StandardError=append:/opt/picoclaw/logs/picoclaw-error.log
|
||||
|
||||
# Environment
|
||||
EnvironmentFile=-/opt/picoclaw/config/.env
|
||||
Environment=HOME=/opt/picoclaw
|
||||
Environment=PICOCLAW_CONFIG=/opt/picoclaw/config/config.json
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
ReadWritePaths=/opt/picoclaw
|
||||
PrivateTmp=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectControlGroups=yes
|
||||
RestrictSUIDSGID=yes
|
||||
RestrictNamespaces=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
109
deploy/hostinger/rollback.sh
Executable file
109
deploy/hostinger/rollback.sh
Executable file
|
|
@ -0,0 +1,109 @@
|
|||
#!/usr/bin/env bash
|
||||
# ============================================================
|
||||
# PicoClaw - Rollback to Previous Version
|
||||
# ============================================================
|
||||
# Usage:
|
||||
# ./deploy/hostinger/rollback.sh -h YOUR_VPS_IP
|
||||
# ============================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() { echo -e "${GREEN}[ROLLBACK]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; }
|
||||
|
||||
# Configuration
|
||||
HOST="${HOSTINGER_HOST:-}"
|
||||
USER="${HOSTINGER_USER:-root}"
|
||||
SSH_KEY="${HOSTINGER_SSH_KEY:-${HOME}/.ssh/id_rsa}"
|
||||
SSH_PORT="${HOSTINGER_SSH_PORT:-22}"
|
||||
METHOD="${HOSTINGER_DEPLOY_METHOD:-docker}"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-h|--host) HOST="$2"; shift 2 ;;
|
||||
-u|--user) USER="$2"; shift 2 ;;
|
||||
-k|--key) SSH_KEY="$2"; shift 2 ;;
|
||||
-m|--method) METHOD="$2"; shift 2 ;;
|
||||
-p|--port) SSH_PORT="$2"; shift 2 ;;
|
||||
*) error "Unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[ -z "${HOST}" ] && error "Host required. Use -h/--host or set HOSTINGER_HOST env var."
|
||||
|
||||
SSH_OPTS="-o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 -p ${SSH_PORT} -i ${SSH_KEY}"
|
||||
SSH_CMD="ssh ${SSH_OPTS} ${USER}@${HOST}"
|
||||
|
||||
log "Rolling back PicoClaw on ${HOST}..."
|
||||
|
||||
if [ "${METHOD}" = "docker" ]; then
|
||||
${SSH_CMD} <<'REMOTEOF'
|
||||
set -e
|
||||
cd /opt/picoclaw
|
||||
|
||||
# List available images
|
||||
echo "Available images:"
|
||||
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.CreatedAt}}" | grep picoclaw || true
|
||||
|
||||
# Docker rollback: restart with previous image
|
||||
echo "[REMOTE] Stopping current container..."
|
||||
docker compose down --timeout 30
|
||||
|
||||
echo "[REMOTE] Starting previous version..."
|
||||
# The previous image should still be cached
|
||||
docker compose up -d picoclaw-gateway
|
||||
|
||||
sleep 5
|
||||
if docker compose ps picoclaw-gateway | grep -q "Up"; then
|
||||
echo "[REMOTE] Rollback successful!"
|
||||
docker compose ps
|
||||
else
|
||||
echo "[REMOTE] Rollback failed!"
|
||||
docker compose logs --tail=20 picoclaw-gateway
|
||||
exit 1
|
||||
fi
|
||||
REMOTEOF
|
||||
else
|
||||
${SSH_CMD} <<'REMOTEOF'
|
||||
set -e
|
||||
BACKUP_DIR="/opt/picoclaw/backups"
|
||||
|
||||
# Find latest backup
|
||||
LATEST_BACKUP=$(ls -t ${BACKUP_DIR}/picoclaw-*.bak 2>/dev/null | head -1)
|
||||
|
||||
if [ -z "${LATEST_BACKUP}" ]; then
|
||||
echo "[REMOTE] No backup found to rollback to!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[REMOTE] Rolling back to: ${LATEST_BACKUP}"
|
||||
|
||||
# Stop service
|
||||
systemctl stop picoclaw
|
||||
|
||||
# Restore binary
|
||||
cp "${LATEST_BACKUP}" /opt/picoclaw/bin/picoclaw
|
||||
chmod +x /opt/picoclaw/bin/picoclaw
|
||||
|
||||
# Start service
|
||||
systemctl start picoclaw
|
||||
|
||||
sleep 3
|
||||
if systemctl is-active --quiet picoclaw; then
|
||||
echo "[REMOTE] Rollback successful!"
|
||||
systemctl status picoclaw --no-pager
|
||||
else
|
||||
echo "[REMOTE] Rollback failed!"
|
||||
journalctl -u picoclaw --no-pager -n 20
|
||||
exit 1
|
||||
fi
|
||||
REMOTEOF
|
||||
fi
|
||||
|
||||
log "Rollback complete!"
|
||||
328
deploy/hostinger/setup-server.sh
Executable file
328
deploy/hostinger/setup-server.sh
Executable file
|
|
@ -0,0 +1,328 @@
|
|||
#!/usr/bin/env bash
|
||||
# ============================================================
|
||||
# PicoClaw - Hostinger VPS Initial Setup
|
||||
# ============================================================
|
||||
# Run this script ONCE on a fresh Hostinger VPS to prepare it
|
||||
# for PicoClaw deployment.
|
||||
#
|
||||
# Usage:
|
||||
# ssh root@YOUR_VPS_IP 'bash -s' < deploy/hostinger/setup-server.sh
|
||||
#
|
||||
# Or copy and run directly on the server:
|
||||
# scp deploy/hostinger/setup-server.sh root@YOUR_VPS_IP:/tmp/
|
||||
# ssh root@YOUR_VPS_IP 'bash /tmp/setup-server.sh'
|
||||
# ============================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() { echo -e "${GREEN}[SETUP]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; }
|
||||
|
||||
# ── Configuration ──────────────────────────────────────
|
||||
PICOCLAW_USER="picoclaw"
|
||||
PICOCLAW_HOME="/opt/picoclaw"
|
||||
DEPLOY_METHOD="${1:-docker}" # "docker" or "binary"
|
||||
|
||||
log "PicoClaw Hostinger VPS Setup"
|
||||
log "Deploy method: ${DEPLOY_METHOD}"
|
||||
echo ""
|
||||
|
||||
# ── 1. System Update ──────────────────────────────────
|
||||
log "Updating system packages..."
|
||||
if command -v apt-get &>/dev/null; then
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get upgrade -y -qq
|
||||
apt-get install -y -qq curl wget git ufw fail2ban unzip jq
|
||||
elif command -v yum &>/dev/null; then
|
||||
yum update -y -q
|
||||
yum install -y -q curl wget git firewalld fail2ban unzip jq
|
||||
elif command -v dnf &>/dev/null; then
|
||||
dnf update -y -q
|
||||
dnf install -y -q curl wget git firewalld fail2ban unzip jq
|
||||
else
|
||||
error "Unsupported package manager. This script supports apt, yum, and dnf."
|
||||
fi
|
||||
|
||||
# ── 2. Create dedicated user ─────────────────────────
|
||||
if ! id "${PICOCLAW_USER}" &>/dev/null; then
|
||||
log "Creating dedicated user: ${PICOCLAW_USER}"
|
||||
useradd --system --create-home --home-dir "${PICOCLAW_HOME}" \
|
||||
--shell /bin/bash "${PICOCLAW_USER}"
|
||||
else
|
||||
log "User ${PICOCLAW_USER} already exists"
|
||||
fi
|
||||
|
||||
# ── 3. Create directory structure ─────────────────────
|
||||
log "Creating directory structure..."
|
||||
mkdir -p "${PICOCLAW_HOME}"/{bin,config,workspace,logs,backups}
|
||||
chown -R "${PICOCLAW_USER}:${PICOCLAW_USER}" "${PICOCLAW_HOME}"
|
||||
|
||||
# ── 4. Install Docker (if docker method) ─────────────
|
||||
if [ "${DEPLOY_METHOD}" = "docker" ]; then
|
||||
if ! command -v docker &>/dev/null; then
|
||||
log "Installing Docker..."
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
systemctl enable docker
|
||||
systemctl start docker
|
||||
usermod -aG docker "${PICOCLAW_USER}"
|
||||
log "Docker installed successfully"
|
||||
else
|
||||
log "Docker already installed: $(docker --version)"
|
||||
fi
|
||||
|
||||
# Install Docker Compose plugin if not present
|
||||
if ! docker compose version &>/dev/null; then
|
||||
log "Installing Docker Compose plugin..."
|
||||
apt-get install -y -qq docker-compose-plugin 2>/dev/null || \
|
||||
yum install -y -q docker-compose-plugin 2>/dev/null || \
|
||||
dnf install -y -q docker-compose-plugin 2>/dev/null || {
|
||||
COMPOSE_VERSION=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | jq -r .tag_name)
|
||||
curl -fsSL "https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-$(uname -s)-$(uname -m)" \
|
||||
-o /usr/local/bin/docker-compose
|
||||
chmod +x /usr/local/bin/docker-compose
|
||||
}
|
||||
fi
|
||||
log "Docker Compose ready: $(docker compose version 2>/dev/null || docker-compose --version 2>/dev/null)"
|
||||
fi
|
||||
|
||||
# ── 5. Install Go (if binary method) ─────────────────
|
||||
if [ "${DEPLOY_METHOD}" = "binary" ]; then
|
||||
if ! command -v go &>/dev/null; then
|
||||
log "Installing Go..."
|
||||
GO_VERSION="1.23.4"
|
||||
ARCH=$(uname -m)
|
||||
case "${ARCH}" in
|
||||
x86_64) GO_ARCH="amd64" ;;
|
||||
aarch64) GO_ARCH="arm64" ;;
|
||||
*) GO_ARCH="${ARCH}" ;;
|
||||
esac
|
||||
curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${GO_ARCH}.tar.gz" -o /tmp/go.tar.gz
|
||||
rm -rf /usr/local/go
|
||||
tar -C /usr/local -xzf /tmp/go.tar.gz
|
||||
rm /tmp/go.tar.gz
|
||||
echo 'export PATH=$PATH:/usr/local/go/bin' > /etc/profile.d/go.sh
|
||||
export PATH=$PATH:/usr/local/go/bin
|
||||
log "Go installed: $(go version)"
|
||||
else
|
||||
log "Go already installed: $(go version)"
|
||||
fi
|
||||
|
||||
# Install make if not present
|
||||
if ! command -v make &>/dev/null; then
|
||||
apt-get install -y -qq make 2>/dev/null || \
|
||||
yum install -y -q make 2>/dev/null || \
|
||||
dnf install -y -q make 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── 6. Configure Firewall ────────────────────────────
|
||||
log "Configuring firewall..."
|
||||
if command -v ufw &>/dev/null; then
|
||||
ufw --force reset
|
||||
ufw default deny incoming
|
||||
ufw default allow outgoing
|
||||
ufw allow ssh
|
||||
# Port 18790 is NOT opened publicly - accessible only via Tailscale
|
||||
ufw --force enable
|
||||
log "UFW firewall configured (port 18790 is tailscale-only)"
|
||||
elif command -v firewall-cmd &>/dev/null; then
|
||||
systemctl enable firewalld
|
||||
systemctl start firewalld
|
||||
firewall-cmd --permanent --add-service=ssh
|
||||
# Port 18790 is NOT opened publicly - accessible only via Tailscale
|
||||
firewall-cmd --reload
|
||||
log "firewalld configured (port 18790 is tailscale-only)"
|
||||
fi
|
||||
|
||||
# ── 6b. Install and configure Tailscale ──────────────
|
||||
log "Installing Tailscale..."
|
||||
if ! command -v tailscale &>/dev/null; then
|
||||
curl -fsSL https://tailscale.com/install.sh | sh
|
||||
log "Tailscale installed"
|
||||
else
|
||||
log "Tailscale already installed: $(tailscale version 2>/dev/null | head -1)"
|
||||
fi
|
||||
|
||||
if [ -n "${TAILSCALE_AUTH_KEY:-}" ]; then
|
||||
log "Authenticating Tailscale with auth key..."
|
||||
tailscale up --authkey="${TAILSCALE_AUTH_KEY}" --hostname="picoclaw" --ssh
|
||||
log "Tailscale authenticated. Configuring serve..."
|
||||
tailscale serve --bg http://localhost:18790
|
||||
log "Tailscale serve active: https://picoclaw.TAILNET.ts.net -> localhost:18790"
|
||||
else
|
||||
warn "TAILSCALE_AUTH_KEY not set. Run manually after setup:"
|
||||
warn " tailscale up --hostname=picoclaw --ssh"
|
||||
warn " tailscale serve --bg http://localhost:18790"
|
||||
fi
|
||||
|
||||
# ── 7. Configure fail2ban ────────────────────────────
|
||||
log "Configuring fail2ban..."
|
||||
systemctl enable fail2ban
|
||||
systemctl start fail2ban
|
||||
|
||||
# ── 8. Create environment template ───────────────────
|
||||
if [ ! -f "${PICOCLAW_HOME}/config/.env" ]; then
|
||||
log "Creating environment template..."
|
||||
cat > "${PICOCLAW_HOME}/config/.env" <<'ENVEOF'
|
||||
# ============================================================
|
||||
# PicoClaw Production Environment
|
||||
# ============================================================
|
||||
# Edit this file with your actual API keys and tokens.
|
||||
# NEVER commit this file to version control.
|
||||
# ============================================================
|
||||
|
||||
# ── LLM Provider (uncomment one) ──────────────────────
|
||||
# ANTHROPIC_API_KEY=sk-ant-xxx
|
||||
# OPENAI_API_KEY=sk-xxx
|
||||
# OPENROUTER_API_KEY=sk-or-v1-xxx
|
||||
# GEMINI_API_KEY=xxx
|
||||
|
||||
# ── Telegram Bot ─────────────────────────────────────
|
||||
# Get token from @BotFather on Telegram
|
||||
PICOCLAW_CHANNELS_TELEGRAM_ENABLED=true
|
||||
PICOCLAW_CHANNELS_TELEGRAM_TOKEN=
|
||||
|
||||
# ── Other Chat Channels ──────────────────────────────
|
||||
# DISCORD_BOT_TOKEN=xxx
|
||||
|
||||
# ── Web Search (optional) ────────────────────────────
|
||||
# BRAVE_SEARCH_API_KEY=BSA...
|
||||
|
||||
# ── Timezone ─────────────────────────────────────────
|
||||
TZ=America/Sao_Paulo
|
||||
ENVEOF
|
||||
chown "${PICOCLAW_USER}:${PICOCLAW_USER}" "${PICOCLAW_HOME}/config/.env"
|
||||
chmod 600 "${PICOCLAW_HOME}/config/.env"
|
||||
fi
|
||||
|
||||
# ── 9. Create config.json template ───────────────────
|
||||
if [ ! -f "${PICOCLAW_HOME}/config/config.json" ]; then
|
||||
log "Creating config.json template..."
|
||||
cat > "${PICOCLAW_HOME}/config/config.json" <<'JSONEOF'
|
||||
{
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"workspace": "/opt/picoclaw/workspace",
|
||||
"restrict_to_workspace": true,
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
"max_tokens": 8192,
|
||||
"temperature": 0.7,
|
||||
"max_tool_iterations": 20
|
||||
}
|
||||
},
|
||||
"channels": {
|
||||
"telegram": {
|
||||
"enabled": true,
|
||||
"token": "",
|
||||
"proxy": "",
|
||||
"allow_from": []
|
||||
}
|
||||
},
|
||||
"providers": {
|
||||
"anthropic": {
|
||||
"api_key": "",
|
||||
"api_base": ""
|
||||
}
|
||||
},
|
||||
"heartbeat": {
|
||||
"enabled": true,
|
||||
"interval": 30
|
||||
},
|
||||
"gateway": {
|
||||
"host": "0.0.0.0",
|
||||
"port": 18790
|
||||
}
|
||||
}
|
||||
JSONEOF
|
||||
chown "${PICOCLAW_USER}:${PICOCLAW_USER}" "${PICOCLAW_HOME}/config/config.json"
|
||||
chmod 600 "${PICOCLAW_HOME}/config/config.json"
|
||||
fi
|
||||
|
||||
# ── 10. Install systemd service (for binary method) ──
|
||||
if [ "${DEPLOY_METHOD}" = "binary" ]; then
|
||||
log "Installing systemd service..."
|
||||
cat > /etc/systemd/system/picoclaw.service <<SVCEOF
|
||||
[Unit]
|
||||
Description=PicoClaw AI Assistant Gateway
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=5
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=${PICOCLAW_USER}
|
||||
Group=${PICOCLAW_USER}
|
||||
WorkingDirectory=${PICOCLAW_HOME}
|
||||
ExecStart=${PICOCLAW_HOME}/bin/picoclaw gateway
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
StandardOutput=append:${PICOCLAW_HOME}/logs/picoclaw.log
|
||||
StandardError=append:${PICOCLAW_HOME}/logs/picoclaw-error.log
|
||||
|
||||
# Environment
|
||||
EnvironmentFile=-${PICOCLAW_HOME}/config/.env
|
||||
Environment=HOME=${PICOCLAW_HOME}
|
||||
Environment=PICOCLAW_CONFIG=${PICOCLAW_HOME}/config/config.json
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
ReadWritePaths=${PICOCLAW_HOME}
|
||||
PrivateTmp=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectKernelTunables=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
SVCEOF
|
||||
systemctl daemon-reload
|
||||
systemctl enable picoclaw
|
||||
log "systemd service installed and enabled"
|
||||
fi
|
||||
|
||||
# ── 11. Create logrotate config ──────────────────────
|
||||
cat > /etc/logrotate.d/picoclaw <<LOGEOF
|
||||
${PICOCLAW_HOME}/logs/*.log {
|
||||
daily
|
||||
missingok
|
||||
rotate 14
|
||||
compress
|
||||
delaycompress
|
||||
notifempty
|
||||
create 0640 ${PICOCLAW_USER} ${PICOCLAW_USER}
|
||||
postrotate
|
||||
systemctl reload picoclaw 2>/dev/null || true
|
||||
endscript
|
||||
}
|
||||
LOGEOF
|
||||
|
||||
# ── Done ─────────────────────────────────────────────
|
||||
echo ""
|
||||
log "=========================================="
|
||||
log " Server setup complete!"
|
||||
log "=========================================="
|
||||
echo ""
|
||||
log "Next steps:"
|
||||
echo " 1. Edit API keys: nano ${PICOCLAW_HOME}/config/.env"
|
||||
echo " 2. Edit config: nano ${PICOCLAW_HOME}/config/config.json"
|
||||
if [ "${DEPLOY_METHOD}" = "docker" ]; then
|
||||
echo " 3. Deploy: Run 'make deploy-hostinger' from your local machine"
|
||||
else
|
||||
echo " 3. Deploy: Run 'make deploy-hostinger' from your local machine"
|
||||
echo " 4. Start service: systemctl start picoclaw"
|
||||
echo " 5. Check status: systemctl status picoclaw"
|
||||
echo " 6. View logs: tail -f ${PICOCLAW_HOME}/logs/picoclaw.log"
|
||||
fi
|
||||
echo ""
|
||||
log "Firewall ports open: SSH (22) only. Port 18790 accessible via Tailscale only."
|
||||
echo ""
|
||||
195
deploy/hostinger/setup-tailscale.sh
Executable file
195
deploy/hostinger/setup-tailscale.sh
Executable file
|
|
@ -0,0 +1,195 @@
|
|||
#!/usr/bin/env bash
|
||||
# ============================================================
|
||||
# PicoClaw - Tailscale Setup Script
|
||||
# ============================================================
|
||||
# Guides you through Tailscale setup and configuration
|
||||
#
|
||||
# Usage:
|
||||
# bash deploy/hostinger/setup-tailscale.sh
|
||||
# OR
|
||||
# make setup-tailscale
|
||||
# ============================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() { echo -e "${GREEN}✓${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}⚠${NC} $*"; }
|
||||
error() { echo -e "${RED}✗${NC} $*"; exit 1; }
|
||||
info() { echo -e "${BLUE}ℹ${NC} $*"; }
|
||||
header() { echo -e "\n${BLUE}══════════════════════════════════════${NC}\n$*\n${BLUE}══════════════════════════════════════${NC}\n"; }
|
||||
|
||||
# ── Configuration ──────────────────────────────────────
|
||||
SSH_HOST=""
|
||||
SSH_USER="root"
|
||||
SSH_PORT="22"
|
||||
|
||||
header "🔐 PicoClaw Tailscale Setup"
|
||||
|
||||
# ── Step 1: Collect SSH Details ────────────────────────
|
||||
step_ssh_details() {
|
||||
header "Step 1️⃣ SSH Connection Details"
|
||||
|
||||
info "Enter your Hostinger VPS details"
|
||||
read -p "Server IP or hostname: " SSH_HOST
|
||||
read -p "SSH user (default: root): " SSH_USER_INPUT
|
||||
SSH_USER="${SSH_USER_INPUT:-root}"
|
||||
read -p "SSH port (default: 22): " SSH_PORT_INPUT
|
||||
SSH_PORT="${SSH_PORT_INPUT:-22}"
|
||||
|
||||
log "SSH Details: ${GREEN}${SSH_USER}@${SSH_HOST}:${SSH_PORT}${NC}"
|
||||
}
|
||||
|
||||
# ── Step 2: Install Tailscale ──────────────────────────
|
||||
step_install_tailscale() {
|
||||
header "Step 2️⃣ Install Tailscale on Server"
|
||||
|
||||
info "Installing Tailscale..."
|
||||
|
||||
ssh -p "$SSH_PORT" "${SSH_USER}@${SSH_HOST}" <<'EOF'
|
||||
if command -v tailscale &>/dev/null; then
|
||||
echo "✓ Tailscale already installed"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Installing Tailscale..."
|
||||
curl -fsSL https://tailscale.com/install.sh | sh
|
||||
|
||||
if command -v tailscale &>/dev/null; then
|
||||
echo "✓ Tailscale installed successfully"
|
||||
else
|
||||
echo "✗ Failed to install Tailscale"
|
||||
exit 1
|
||||
fi
|
||||
EOF
|
||||
|
||||
log "Tailscale installed"
|
||||
}
|
||||
|
||||
# ── Step 3: Authenticate with Tailscale ───────────────
|
||||
step_authenticate() {
|
||||
header "Step 3️⃣ Authenticate with Tailscale"
|
||||
|
||||
info "Opening Tailscale authentication..."
|
||||
info "A URL will appear below. Open it in your browser and authorize."
|
||||
echo ""
|
||||
|
||||
ssh -p "$SSH_PORT" "${SSH_USER}@${SSH_HOST}" <<'EOF'
|
||||
echo "Starting Tailscale authentication..."
|
||||
echo "Click the link below or open it in your browser:"
|
||||
echo ""
|
||||
|
||||
tailscale up --hostname=picoclaw --ssh
|
||||
|
||||
echo ""
|
||||
echo "✓ Tailscale authentication complete"
|
||||
tailscale ip -4
|
||||
EOF
|
||||
|
||||
log "Tailscale authenticated"
|
||||
}
|
||||
|
||||
# ── Step 4: Configure Tailscale Serve ──────────────────
|
||||
step_configure_serve() {
|
||||
header "Step 4️⃣ Configure Tailscale Serve"
|
||||
|
||||
info "Configuring Tailscale to expose PicoClaw on tailnet..."
|
||||
|
||||
ssh -p "$SSH_PORT" "${SSH_USER}@${SSH_HOST}" <<'EOF'
|
||||
tailscale serve --bg http://localhost:18790
|
||||
|
||||
echo "✓ Tailscale serve configured"
|
||||
echo ""
|
||||
echo "Your PicoClaw is now accessible at:"
|
||||
tailscale ip -4 | while read ip; do
|
||||
echo " http://$ip:18790"
|
||||
done
|
||||
echo " https://picoclaw.$(tailscale status --json | grep -o '"Self":{"ID":"[^"]*' | grep -o '"[^"]*$' | tr -d '"' | sed 's/.*\.//' | head -1).ts.net"
|
||||
EOF
|
||||
|
||||
log "Tailscale serve active"
|
||||
}
|
||||
|
||||
# ── Step 5: Verify Access ──────────────────────────────
|
||||
step_verify_access() {
|
||||
header "Step 5️⃣ Verify Access"
|
||||
|
||||
info "Testing Tailscale connection..."
|
||||
|
||||
TAILNET_IP=$(ssh -p "$SSH_PORT" "${SSH_USER}@${SSH_HOST}" "tailscale ip -4" 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$TAILNET_IP" ]; then
|
||||
warn "Could not get Tailscale IP"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "Tailscale IP: ${BLUE}${TAILNET_IP}${NC}"
|
||||
|
||||
# Try to curl the health endpoint
|
||||
if curl -sf "http://${TAILNET_IP}:18790/health" >/dev/null 2>&1; then
|
||||
log "✨ PicoClaw is accessible via Tailscale!"
|
||||
echo ""
|
||||
info "Access PicoClaw at: ${BLUE}http://${TAILNET_IP}:18790${NC}"
|
||||
else
|
||||
warn "Could not connect to PicoClaw (might be still starting)"
|
||||
echo ""
|
||||
info "Try manually: ${BLUE}curl http://${TAILNET_IP}:18790/health${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Step 6: Show Next Steps ────────────────────────────
|
||||
step_next_steps() {
|
||||
header "✨ Tailscale Setup Complete!"
|
||||
|
||||
echo ""
|
||||
echo " ${GREEN}Your PicoClaw VPS is now secured with Tailscale${NC}"
|
||||
echo ""
|
||||
echo " ${BLUE}Port 18790 is ${GREEN}NOT${BLUE} accessible from the internet${NC}"
|
||||
echo " ${BLUE}Only accessible via your Tailnet${NC}"
|
||||
echo ""
|
||||
echo " ${YELLOW}Next steps:${NC}"
|
||||
echo " 1. Get your Tailscale IP:"
|
||||
echo " ${BLUE}ssh ${SSH_USER}@${SSH_HOST} -p ${SSH_PORT} 'tailscale ip -4'${NC}"
|
||||
echo ""
|
||||
echo " 2. Access PicoClaw:"
|
||||
echo " ${BLUE}http://<TAILSCALE_IP>:18790${NC}"
|
||||
echo ""
|
||||
echo " 3. SSH via Tailscale:"
|
||||
echo " ${BLUE}tailscale list${NC} (to see devices)"
|
||||
echo " ${BLUE}ssh picoclaw.${USER}.ts.net${NC}"
|
||||
echo ""
|
||||
echo " 4. Set up Telegram bot:"
|
||||
echo " ${BLUE}make setup-telegram${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ── Main Execution ────────────────────────────────────
|
||||
main() {
|
||||
info "This script will set up Tailscale to secure your PicoClaw"
|
||||
echo ""
|
||||
echo " What it does:"
|
||||
echo " • Installs Tailscale on your Hostinger VPS"
|
||||
echo " • Authenticates with your Tailnet"
|
||||
echo " • Exposes PicoClaw only on Tailscale"
|
||||
echo " • Blocks public internet access to port 18790"
|
||||
echo ""
|
||||
|
||||
read -p "Continue? (y/n): " proceed
|
||||
[ "$proceed" != "y" ] && error "Aborted by user"
|
||||
|
||||
step_ssh_details
|
||||
step_install_tailscale
|
||||
step_authenticate
|
||||
step_configure_serve
|
||||
step_verify_access
|
||||
step_next_steps
|
||||
}
|
||||
|
||||
# ── Run ────────────────────────────────────────────────
|
||||
main
|
||||
257
deploy/hostinger/setup-telegram.sh
Executable file
257
deploy/hostinger/setup-telegram.sh
Executable file
|
|
@ -0,0 +1,257 @@
|
|||
#!/usr/bin/env bash
|
||||
# ============================================================
|
||||
# PicoClaw - Telegram Bot Setup Script
|
||||
# ============================================================
|
||||
# Interactive setup for Telegram bot integration
|
||||
#
|
||||
# Usage:
|
||||
# bash deploy/hostinger/setup-telegram.sh
|
||||
# ============================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() { echo -e "${GREEN}✓${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}⚠${NC} $*"; }
|
||||
error() { echo -e "${RED}✗${NC} $*"; exit 1; }
|
||||
info() { echo -e "${BLUE}ℹ${NC} $*"; }
|
||||
header() { echo -e "\n${BLUE}══════════════════════════════════════${NC}\n$*\n${BLUE}══════════════════════════════════════${NC}\n"; }
|
||||
|
||||
# ── Configuration ──────────────────────────────────────
|
||||
PICOCLAW_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
TELEGRAM_TOKEN=""
|
||||
GITHUB_REPO=""
|
||||
GITHUB_TOKEN=""
|
||||
SSH_HOST=""
|
||||
|
||||
header "🤖 PicoClaw Telegram Bot Setup"
|
||||
|
||||
# ── Step 1: Create Bot with BotFather ──────────────────
|
||||
step_create_bot() {
|
||||
header "Step 1️⃣ Create Telegram Bot with @BotFather"
|
||||
|
||||
echo "Follow these steps in Telegram:"
|
||||
echo ""
|
||||
echo " 1. Open Telegram and search for: ${BLUE}@BotFather${NC}"
|
||||
echo " 2. Send: ${GREEN}/start${NC}"
|
||||
echo " 3. Send: ${GREEN}/newbot${NC}"
|
||||
echo " 4. Give it a ${BLUE}Name${NC} (e.g., 'PicoClaw AI')"
|
||||
echo " 5. Give it a ${BLUE}Username${NC} (e.g., 'picoclaw_bot')"
|
||||
echo " ${YELLOW}⚠ Must be unique and end with _bot${NC}"
|
||||
echo " 6. ${GREEN}Copy the token${NC} provided by BotFather"
|
||||
echo ""
|
||||
|
||||
read -p "Paste your bot token here: " TELEGRAM_TOKEN
|
||||
|
||||
if [ -z "$TELEGRAM_TOKEN" ]; then
|
||||
error "Bot token cannot be empty!"
|
||||
fi
|
||||
|
||||
# Basic validation: should be numbers:letters format
|
||||
if [[ ! $TELEGRAM_TOKEN =~ ^[0-9]+:[A-Za-z0-9_-]+$ ]]; then
|
||||
warn "Token format looks unusual. Continue? (y/n)"
|
||||
read -p "" confirm
|
||||
[ "$confirm" != "y" ] && error "Aborted"
|
||||
fi
|
||||
|
||||
log "Bot token saved: ${TELEGRAM_TOKEN:0:20}..."
|
||||
}
|
||||
|
||||
# ── Step 2: Test Bot ───────────────────────────────────
|
||||
step_test_bot() {
|
||||
header "Step 2️⃣ Test Your Bot Token"
|
||||
|
||||
info "Validating token with Telegram API..."
|
||||
|
||||
RESPONSE=$(curl -s "https://api.telegram.org/bot${TELEGRAM_TOKEN}/getMe")
|
||||
|
||||
if echo "$RESPONSE" | grep -q '"ok":true'; then
|
||||
BOT_USERNAME=$(echo "$RESPONSE" | grep -o '"username":"[^"]*' | cut -d'"' -f4)
|
||||
BOT_NAME=$(echo "$RESPONSE" | grep -o '"first_name":"[^"]*' | cut -d'"' -f4)
|
||||
|
||||
log "Bot token is valid! ✨"
|
||||
log "Bot Name: ${BLUE}$BOT_NAME${NC}"
|
||||
log "Bot Username: ${BLUE}@$BOT_USERNAME${NC}"
|
||||
echo ""
|
||||
info "Find your bot on Telegram: ${GREEN}@$BOT_USERNAME${NC}"
|
||||
else
|
||||
error "Invalid bot token! Please check and try again."
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Step 3: GitHub Setup ───────────────────────────────
|
||||
step_github_setup() {
|
||||
header "Step 3️⃣ Configure GitHub Secrets"
|
||||
|
||||
info "Checking for 'gh' CLI..."
|
||||
if ! command -v gh &>/dev/null; then
|
||||
warn "GitHub CLI not installed. Please add the secret manually:"
|
||||
echo ""
|
||||
echo " 1. Go to: ${BLUE}https://github.com/YOUR_USER/YOUR_REPO/settings/secrets/actions${NC}"
|
||||
echo " 2. Click ${GREEN}New repository secret${NC}"
|
||||
echo " 3. Name: ${GREEN}PICOCLAW_TELEGRAM_BOT_TOKEN${NC}"
|
||||
echo " 4. Value: ${GREEN}${TELEGRAM_TOKEN:0:30}...${NC}"
|
||||
echo ""
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Get repo info
|
||||
if [ -z "$GITHUB_REPO" ]; then
|
||||
info "Detecting GitHub repository..."
|
||||
GITHUB_REPO=$(cd "$PICOCLAW_DIR" && git config --get remote.origin.url | sed 's/.*:\(.*\)\.git/\1/')
|
||||
fi
|
||||
|
||||
if [ -z "$GITHUB_REPO" ]; then
|
||||
warn "Could not detect GitHub repo. Please enter manually:"
|
||||
read -p "GitHub repo (user/repo): " GITHUB_REPO
|
||||
fi
|
||||
|
||||
info "Repository: ${BLUE}$GITHUB_REPO${NC}"
|
||||
|
||||
# Try to set secret with gh CLI
|
||||
if gh secret set PICOCLAW_TELEGRAM_BOT_TOKEN --body "$TELEGRAM_TOKEN" -R "$GITHUB_REPO" 2>/dev/null; then
|
||||
log "GitHub secret configured! 🔐"
|
||||
else
|
||||
warn "Could not set GitHub secret via CLI"
|
||||
echo ""
|
||||
echo "Set it manually:"
|
||||
echo " ${BLUE}gh secret set PICOCLAW_TELEGRAM_BOT_TOKEN -b '$TELEGRAM_TOKEN' -R '$GITHUB_REPO'${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Step 4: Configure Locally (for testing) ───────────
|
||||
step_configure_local() {
|
||||
header "Step 4️⃣ Configure Locally (Optional - for testing)"
|
||||
|
||||
read -p "Configure locally for testing? (y/n): " configure_local
|
||||
|
||||
if [ "$configure_local" != "y" ]; then
|
||||
info "Skipping local configuration"
|
||||
return 0
|
||||
fi
|
||||
|
||||
ENV_FILE="${PICOCLAW_DIR}/config/.env"
|
||||
CONFIG_FILE="${PICOCLAW_DIR}/config/config.json"
|
||||
|
||||
info "Updating .env file..."
|
||||
if grep -q "PICOCLAW_CHANNELS_TELEGRAM_TOKEN" "$ENV_FILE" 2>/dev/null; then
|
||||
sed -i "s|^PICOCLAW_CHANNELS_TELEGRAM_TOKEN=.*|PICOCLAW_CHANNELS_TELEGRAM_TOKEN=$TELEGRAM_TOKEN|" "$ENV_FILE"
|
||||
else
|
||||
echo "PICOCLAW_CHANNELS_TELEGRAM_TOKEN=$TELEGRAM_TOKEN" >> "$ENV_FILE"
|
||||
fi
|
||||
|
||||
if grep -q "PICOCLAW_CHANNELS_TELEGRAM_ENABLED" "$ENV_FILE" 2>/dev/null; then
|
||||
sed -i 's/^PICOCLAW_CHANNELS_TELEGRAM_ENABLED=.*/PICOCLAW_CHANNELS_TELEGRAM_ENABLED=true/' "$ENV_FILE"
|
||||
else
|
||||
echo "PICOCLAW_CHANNELS_TELEGRAM_ENABLED=true" >> "$ENV_FILE"
|
||||
fi
|
||||
|
||||
log ".env file updated"
|
||||
|
||||
# Update config.json
|
||||
if command -v jq &>/dev/null && [ -f "$CONFIG_FILE" ]; then
|
||||
info "Updating config.json..."
|
||||
jq '.channels.telegram.enabled = true | .channels.telegram.token = ""' "$CONFIG_FILE" > "${CONFIG_FILE}.tmp"
|
||||
mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE"
|
||||
log "config.json updated"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Step 5: Deploy ───────────────────────────────────
|
||||
step_deploy() {
|
||||
header "Step 5️⃣ Deploy to Hostinger"
|
||||
|
||||
read -p "Ready to deploy? (y/n): " deploy_ready
|
||||
|
||||
if [ "$deploy_ready" != "y" ]; then
|
||||
info "Skipping deployment"
|
||||
return 0
|
||||
fi
|
||||
|
||||
info "Pushing code changes..."
|
||||
cd "$PICOCLAW_DIR"
|
||||
|
||||
if git diff --quiet; then
|
||||
info "No local changes to commit"
|
||||
else
|
||||
warn "Local changes detected"
|
||||
git status
|
||||
read -p "Commit and push? (y/n): " commit_ready
|
||||
if [ "$commit_ready" = "y" ]; then
|
||||
git add .
|
||||
git commit -m "chore: configure telegram bot integration"
|
||||
git push origin claude/hostinger-remote-deployment-TGVof
|
||||
fi
|
||||
fi
|
||||
|
||||
info "GitHub Actions deployment triggered..."
|
||||
info "Check status at: ${BLUE}https://github.com/$GITHUB_REPO/actions${NC}"
|
||||
}
|
||||
|
||||
# ── Step 6: Verification ───────────────────────────────
|
||||
step_verify() {
|
||||
header "Step 6️⃣ Verify Installation"
|
||||
|
||||
info "Your Telegram bot is now live!"
|
||||
echo ""
|
||||
echo " ${GREEN}Find your bot on Telegram and send: /start${NC}"
|
||||
echo ""
|
||||
|
||||
read -p "Check logs on server? (y/n): " check_logs
|
||||
|
||||
if [ "$check_logs" != "y" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
info "Enter SSH details:"
|
||||
read -p "Server IP or hostname: " SSH_HOST
|
||||
read -p "SSH user (default: root): " SSH_USER
|
||||
SSH_USER="${SSH_USER:-root}"
|
||||
|
||||
info "Connecting to server..."
|
||||
ssh -t "${SSH_USER}@${SSH_HOST}" \
|
||||
'tail -50 /opt/picoclaw/logs/picoclaw.log | grep -i telegram'
|
||||
}
|
||||
|
||||
# ── Main Execution ────────────────────────────────────
|
||||
main() {
|
||||
info "This script will:"
|
||||
echo " 1. Create a Telegram bot with @BotFather"
|
||||
echo " 2. Validate the bot token"
|
||||
echo " 3. Configure GitHub Secrets for CI/CD"
|
||||
echo " 4. (Optional) Configure locally for testing"
|
||||
echo " 5. Deploy to your Hostinger VPS"
|
||||
echo " 6. Verify the installation"
|
||||
echo ""
|
||||
|
||||
read -p "Continue? (y/n): " proceed
|
||||
[ "$proceed" != "y" ] && error "Aborted by user"
|
||||
|
||||
step_create_bot
|
||||
step_test_bot
|
||||
step_github_setup
|
||||
step_configure_local
|
||||
step_deploy
|
||||
step_verify
|
||||
|
||||
header "✨ Setup Complete!"
|
||||
echo ""
|
||||
echo " ${GREEN}Your PicoClaw Telegram bot is ready!${NC}"
|
||||
echo ""
|
||||
echo " Next steps:"
|
||||
echo " 1. Open Telegram and find your bot"
|
||||
echo " 2. Send /start"
|
||||
echo " 3. Start chatting!"
|
||||
echo ""
|
||||
echo " For troubleshooting, see: ${BLUE}docs/TELEGRAM_SETUP.md${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ── Run ────────────────────────────────────────────────
|
||||
main
|
||||
98
deploy/hostinger/status.sh
Executable file
98
deploy/hostinger/status.sh
Executable file
|
|
@ -0,0 +1,98 @@
|
|||
#!/usr/bin/env bash
|
||||
# ============================================================
|
||||
# PicoClaw - Check Remote Server Status
|
||||
# ============================================================
|
||||
# Usage:
|
||||
# ./deploy/hostinger/status.sh -h YOUR_VPS_IP
|
||||
# ============================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() { echo -e "${GREEN}[STATUS]${NC} $*"; }
|
||||
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
|
||||
|
||||
HOST="${HOSTINGER_HOST:-}"
|
||||
USER="${HOSTINGER_USER:-root}"
|
||||
SSH_KEY="${HOSTINGER_SSH_KEY:-${HOME}/.ssh/id_rsa}"
|
||||
SSH_PORT="${HOSTINGER_SSH_PORT:-22}"
|
||||
METHOD="${HOSTINGER_DEPLOY_METHOD:-docker}"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-h|--host) HOST="$2"; shift 2 ;;
|
||||
-u|--user) USER="$2"; shift 2 ;;
|
||||
-k|--key) SSH_KEY="$2"; shift 2 ;;
|
||||
-m|--method) METHOD="$2"; shift 2 ;;
|
||||
-p|--port) SSH_PORT="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[ -z "${HOST}" ] && { echo "Usage: $0 -h HOST"; exit 1; }
|
||||
|
||||
SSH_OPTS="-o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 -p ${SSH_PORT} -i ${SSH_KEY}"
|
||||
SSH_CMD="ssh ${SSH_OPTS} ${USER}@${HOST}"
|
||||
|
||||
log "Checking PicoClaw status on ${HOST}..."
|
||||
echo ""
|
||||
|
||||
${SSH_CMD} <<REMOTEOF
|
||||
echo "── System ──────────────────────────────────"
|
||||
echo "Hostname: \$(hostname)"
|
||||
echo "Uptime: \$(uptime -p)"
|
||||
echo "Memory: \$(free -h | awk '/Mem:/ {print \$3 "/" \$2}')"
|
||||
echo "Disk: \$(df -h / | awk 'NR==2 {print \$3 "/" \$2 " (" \$5 " used)"}')"
|
||||
echo "Load: \$(cat /proc/loadavg | awk '{print \$1, \$2, \$3}')"
|
||||
echo ""
|
||||
|
||||
echo "── PicoClaw ────────────────────────────────"
|
||||
if [ "${METHOD}" = "docker" ]; then
|
||||
if command -v docker &>/dev/null; then
|
||||
echo "Method: Docker"
|
||||
docker compose -f /opt/picoclaw/docker-compose.yml ps 2>/dev/null || echo "Container: not running"
|
||||
echo ""
|
||||
echo "── Recent Logs ─────────────────────────────"
|
||||
docker compose -f /opt/picoclaw/docker-compose.yml logs --tail=10 picoclaw-gateway 2>/dev/null || true
|
||||
else
|
||||
echo "Docker not installed"
|
||||
fi
|
||||
else
|
||||
echo "Method: Binary (systemd)"
|
||||
if systemctl is-active --quiet picoclaw 2>/dev/null; then
|
||||
echo "Status: RUNNING"
|
||||
version=\$(/opt/picoclaw/bin/picoclaw version 2>/dev/null || echo "unknown")
|
||||
echo "Version: \${version}"
|
||||
else
|
||||
echo "Status: STOPPED"
|
||||
fi
|
||||
systemctl status picoclaw --no-pager -l 2>/dev/null || true
|
||||
echo ""
|
||||
echo "── Recent Logs ─────────────────────────────"
|
||||
tail -10 /opt/picoclaw/logs/picoclaw.log 2>/dev/null || echo "No logs found"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "── Health Check ────────────────────────────"
|
||||
if curl -sf http://localhost:18790/health > /dev/null 2>&1; then
|
||||
echo "Gateway: HEALTHY (port 18790)"
|
||||
else
|
||||
echo "Gateway: UNREACHABLE"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "── Firewall ────────────────────────────────"
|
||||
if command -v ufw &>/dev/null; then
|
||||
ufw status 2>/dev/null | head -10
|
||||
elif command -v firewall-cmd &>/dev/null; then
|
||||
firewall-cmd --list-all 2>/dev/null | head -10
|
||||
fi
|
||||
REMOTEOF
|
||||
|
||||
echo ""
|
||||
log "Status check complete"
|
||||
141
deploy/sync-dev.sh
Executable file
141
deploy/sync-dev.sh
Executable file
|
|
@ -0,0 +1,141 @@
|
|||
#!/usr/bin/env bash
|
||||
# ============================================================
|
||||
# Sync Development Branch Script
|
||||
# ============================================================
|
||||
# Automatically sync with the development branch
|
||||
#
|
||||
# Usage:
|
||||
# bash deploy/sync-dev.sh
|
||||
# OR
|
||||
# make sync-dev
|
||||
# ============================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() { echo -e "${GREEN}✓${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}⚠${NC} $*"; }
|
||||
error() { echo -e "${RED}✗${NC} $*"; exit 1; }
|
||||
info() { echo -e "${BLUE}ℹ${NC} $*"; }
|
||||
|
||||
# ── Get current directory ──────────────────────────────
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PICOCLAW_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
cd "$PICOCLAW_DIR"
|
||||
|
||||
# ── Configuration ──────────────────────────────────────
|
||||
BRANCH="claude/hostinger-remote-deployment-TGVof"
|
||||
REMOTE="origin"
|
||||
|
||||
# ── Functions ──────────────────────────────────────────
|
||||
check_git() {
|
||||
if ! command -v git &>/dev/null; then
|
||||
error "git is not installed"
|
||||
fi
|
||||
}
|
||||
|
||||
check_unstaged() {
|
||||
if ! git diff-index --quiet HEAD --; then
|
||||
warn "You have unstaged changes:"
|
||||
git status --short
|
||||
echo ""
|
||||
read -p "Continue anyway? (y/n): " continue
|
||||
[ "$continue" != "y" ] && error "Aborted"
|
||||
fi
|
||||
}
|
||||
|
||||
fetch_latest() {
|
||||
info "Fetching latest changes from $REMOTE/$BRANCH..."
|
||||
if ! git fetch "$REMOTE" "$BRANCH" 2>/dev/null; then
|
||||
error "Failed to fetch from $REMOTE"
|
||||
fi
|
||||
log "Fetched latest changes"
|
||||
}
|
||||
|
||||
show_diff() {
|
||||
info "Checking for differences..."
|
||||
DIFF_COUNT=$(git diff --stat origin/"$BRANCH" | tail -1 | awk '{print $1}')
|
||||
|
||||
if [ -z "$DIFF_COUNT" ] || [ "$DIFF_COUNT" = "0" ]; then
|
||||
log "Already up to date!"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
info "Changes to merge:"
|
||||
git diff --stat origin/"$BRANCH"
|
||||
echo ""
|
||||
|
||||
read -p "View full diff? (y/n): " show_full
|
||||
if [ "$show_full" = "y" ]; then
|
||||
git diff origin/"$BRANCH" | head -100
|
||||
echo "... (showing first 100 lines)"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
merge_changes() {
|
||||
info "Merging changes from $REMOTE/$BRANCH..."
|
||||
|
||||
if git merge origin/"$BRANCH" --no-edit 2>&1 | grep -q "Merge made"; then
|
||||
log "Changes merged successfully"
|
||||
return 0
|
||||
elif git merge origin/"$BRANCH" --no-edit 2>&1 | grep -q "Already up to date"; then
|
||||
log "Already up to date"
|
||||
return 1
|
||||
else
|
||||
warn "Merge completed (but may have conflicts)"
|
||||
git status
|
||||
return 2
|
||||
fi
|
||||
}
|
||||
|
||||
check_conflicts() {
|
||||
if git diff --name-only --diff-filter=U | grep -q .; then
|
||||
warn "Merge conflicts detected!"
|
||||
echo ""
|
||||
echo "Conflicting files:"
|
||||
git diff --name-only --diff-filter=U
|
||||
echo ""
|
||||
error "Please resolve conflicts manually and run: git add . && git commit"
|
||||
fi
|
||||
}
|
||||
|
||||
show_status() {
|
||||
echo ""
|
||||
info "Sync complete!"
|
||||
echo ""
|
||||
git log --oneline -5
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ── Main ───────────────────────────────────────────────
|
||||
main() {
|
||||
echo ""
|
||||
info "🔄 Syncing with $BRANCH"
|
||||
echo ""
|
||||
|
||||
check_git
|
||||
check_unstaged
|
||||
fetch_latest
|
||||
|
||||
if show_diff; then
|
||||
read -p "Merge changes? (y/n): " merge_ok
|
||||
[ "$merge_ok" != "y" ] && error "Aborted"
|
||||
merge_changes
|
||||
check_conflicts
|
||||
fi
|
||||
|
||||
show_status
|
||||
log "Done!"
|
||||
}
|
||||
|
||||
# ── Run ────────────────────────────────────────────────
|
||||
main
|
||||
205
docs/TELEGRAM_SETUP.md
Normal file
205
docs/TELEGRAM_SETUP.md
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
# PicoClaw Telegram Bot Setup Guide
|
||||
|
||||
Complete step-by-step guide to set up PicoClaw as a Telegram bot.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Telegram account
|
||||
- Running PicoClaw instance (deployed)
|
||||
- GitHub secrets configured (for automated deployment)
|
||||
|
||||
## Step 1: Create a Telegram Bot with BotFather
|
||||
|
||||
1. Open Telegram and search for **@BotFather**
|
||||
2. Start the chat and send `/start`
|
||||
3. Send `/newbot` to create a new bot
|
||||
4. Follow the prompts:
|
||||
- **Name**: Give your bot a name (e.g., "PicoClaw AI")
|
||||
- **Username**: Must be unique and end with `_bot` (e.g., `picoclaw_bot`)
|
||||
5. **Copy the token** provided (looks like `123456789:ABCdefGHIjklMNOpqrSTUvwxYZ`)
|
||||
- ⚠️ **Keep this token secret!** Anyone with this token can control your bot.
|
||||
|
||||
## Step 2: Configure PicoClaw
|
||||
|
||||
### Option A: Using Environment Variables (Production)
|
||||
|
||||
Add to your `.env` file or GitHub Secrets:
|
||||
|
||||
```bash
|
||||
PICOCLAW_CHANNELS_TELEGRAM_ENABLED=true
|
||||
PICOCLAW_CHANNELS_TELEGRAM_TOKEN=YOUR_BOT_TOKEN_HERE
|
||||
```
|
||||
|
||||
### Option B: Using config.json (Development)
|
||||
|
||||
Edit `config/config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"telegram": {
|
||||
"enabled": true,
|
||||
"token": "YOUR_BOT_TOKEN_HERE",
|
||||
"proxy": "",
|
||||
"allow_from": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Optional: User Whitelist
|
||||
|
||||
To restrict access to specific users, add their Telegram user IDs:
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"telegram": {
|
||||
"enabled": true,
|
||||
"token": "YOUR_BOT_TOKEN_HERE",
|
||||
"allow_from": ["123456789", "987654321"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To find your Telegram user ID:
|
||||
1. Send any message to your bot
|
||||
2. Check the logs: `cat /opt/picoclaw/logs/picoclaw.log | grep "user_id"`
|
||||
3. Your user ID will be in the output
|
||||
|
||||
## Step 3: Deploy to Hostinger
|
||||
|
||||
### For GitHub Actions Deployment
|
||||
|
||||
1. Add the bot token to your GitHub repository secrets:
|
||||
- Go to **Settings** → **Secrets and Variables** → **Actions**
|
||||
- Click **New repository secret**
|
||||
- **Name**: `PICOCLAW_TELEGRAM_BOT_TOKEN`
|
||||
- **Value**: Your BotFather token
|
||||
|
||||
2. Update `.github/workflows/deploy-hostinger.yml` to use the secret:
|
||||
```yaml
|
||||
env:
|
||||
PICOCLAW_CHANNELS_TELEGRAM_ENABLED: "true"
|
||||
PICOCLAW_CHANNELS_TELEGRAM_TOKEN: ${{ secrets.PICOCLAW_TELEGRAM_BOT_TOKEN }}
|
||||
```
|
||||
|
||||
### For Manual Deployment
|
||||
|
||||
SSH into your server:
|
||||
```bash
|
||||
ssh root@YOUR_HOSTINGER_IP
|
||||
nano /opt/picoclaw/config/.env
|
||||
```
|
||||
|
||||
Add:
|
||||
```
|
||||
PICOCLAW_CHANNELS_TELEGRAM_ENABLED=true
|
||||
PICOCLAW_CHANNELS_TELEGRAM_TOKEN=YOUR_BOT_TOKEN
|
||||
```
|
||||
|
||||
Restart the container:
|
||||
```bash
|
||||
cd /opt/picoclaw && docker compose -f docker-compose.production.yml restart picoclaw
|
||||
```
|
||||
|
||||
## Step 4: Start Using Your Bot
|
||||
|
||||
1. Find your bot on Telegram (search by username: @your_bot_username)
|
||||
2. Send `/start` to initialize
|
||||
3. Start chatting!
|
||||
|
||||
### Available Commands
|
||||
|
||||
- `/start` - Initialize the bot
|
||||
- `/help` - Show available commands
|
||||
- `/show` - Show current agent info
|
||||
- `/list` - List available agents
|
||||
|
||||
### Features
|
||||
|
||||
✅ **Text Messages** - Ask questions, get AI responses
|
||||
✅ **Voice Messages** - Send voice notes (auto-transcribed)
|
||||
✅ **Images** - Send photos for analysis
|
||||
✅ **Documents** - Share files for processing
|
||||
✅ **Multi-agent** - Switch between different AI agents
|
||||
✅ **Thinking Indicator** - See when the AI is processing
|
||||
|
||||
## Step 5: Verify It's Working
|
||||
|
||||
Check the logs to confirm the bot is running:
|
||||
|
||||
```bash
|
||||
ssh root@YOUR_HOSTINGER_IP
|
||||
tail -f /opt/picoclaw/logs/picoclaw.log | grep -i telegram
|
||||
```
|
||||
|
||||
You should see:
|
||||
```
|
||||
[INFO] [telegram] Starting Telegram bot (polling mode)...
|
||||
[INFO] [telegram] Telegram bot connected
|
||||
```
|
||||
|
||||
Send a test message to your bot and check:
|
||||
```bash
|
||||
tail -f /opt/picoclaw/logs/picoclaw.log | grep -i "Received message"
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Bot doesn't respond
|
||||
|
||||
1. Check if Telegram is enabled:
|
||||
```bash
|
||||
docker exec picoclaw cat /opt/picoclaw/config/.env | grep TELEGRAM
|
||||
```
|
||||
|
||||
2. Verify the token is correct (no spaces, exact copy from BotFather)
|
||||
|
||||
3. Check logs for errors:
|
||||
```bash
|
||||
docker exec picoclaw tail -100 /opt/picoclaw/logs/picoclaw.log | grep -i telegram
|
||||
```
|
||||
|
||||
### "Failed to create telegram bot"
|
||||
|
||||
- Token is invalid or expired
|
||||
- Token is incomplete (missing characters)
|
||||
- Try creating a new bot with BotFather
|
||||
|
||||
### "Message rejected by allowlist"
|
||||
|
||||
- Your Telegram user ID is not in the whitelist
|
||||
- Check your actual ID by removing `allow_from` temporarily
|
||||
- Add your ID to the whitelist
|
||||
|
||||
## Advanced: Using Proxy
|
||||
|
||||
If you're in a region with restricted access to Telegram, configure a proxy:
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"telegram": {
|
||||
"enabled": true,
|
||||
"token": "YOUR_BOT_TOKEN_HERE",
|
||||
"proxy": "socks5://user:pass@proxy-host:1080",
|
||||
"allow_from": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Security Notes
|
||||
|
||||
⚠️ **Never commit your bot token to version control**
|
||||
⚠️ **Use GitHub Secrets for CI/CD deployments**
|
||||
⚠️ **Consider using user whitelists for sensitive deployments**
|
||||
⚠️ **Regularly rotate tokens if compromised**
|
||||
|
||||
## Getting Help
|
||||
|
||||
- Telegram Bot API Docs: https://core.telegram.org/bots
|
||||
- BotFather Commands: https://core.telegram.org/bots#botfather
|
||||
- PicoClaw Issues: https://github.com/sipeed/picoclaw/issues
|
||||
|
|
@ -44,6 +44,7 @@ type LINEChannel struct {
|
|||
*BaseChannel
|
||||
config config.LINEConfig
|
||||
httpServer *http.Server
|
||||
useSharedMux bool // true when webhook is registered on the gateway mux
|
||||
botUserID string // Bot's user ID
|
||||
botBasicID string // Bot's basic ID (e.g. @216ru...)
|
||||
botDisplayName string // Bot's display name for text-based mention detection
|
||||
|
|
@ -67,6 +68,20 @@ func NewLINEChannel(cfg config.LINEConfig, messageBus *bus.MessageBus) (*LINECha
|
|||
}, nil
|
||||
}
|
||||
|
||||
// RegisterWebhook registers the LINE webhook handler on a shared HTTP mux
|
||||
// (typically the gateway mux) so no separate server is needed.
|
||||
func (c *LINEChannel) RegisterWebhook(mux *http.ServeMux) {
|
||||
path := c.config.WebhookPath
|
||||
if path == "" {
|
||||
path = "/webhook/line"
|
||||
}
|
||||
mux.HandleFunc(path, c.webhookHandler)
|
||||
c.useSharedMux = true
|
||||
logger.InfoCF("line", "LINE webhook registered on gateway", map[string]interface{}{
|
||||
"path": path,
|
||||
})
|
||||
}
|
||||
|
||||
// Start launches the HTTP webhook server.
|
||||
func (c *LINEChannel) Start(ctx context.Context) error {
|
||||
logger.InfoC("line", "Starting LINE channel (Webhook Mode)")
|
||||
|
|
@ -86,6 +101,13 @@ func (c *LINEChannel) Start(ctx context.Context) error {
|
|||
})
|
||||
}
|
||||
|
||||
// If webhook was already registered on the shared gateway mux, skip creating a standalone server
|
||||
if c.useSharedMux {
|
||||
c.setRunning(true)
|
||||
logger.InfoC("line", "LINE channel started (shared gateway webhook)")
|
||||
return nil
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
path := c.config.WebhookPath
|
||||
if path == "" {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ package channels
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/bus"
|
||||
|
|
@ -314,6 +315,19 @@ func (m *Manager) GetEnabledChannels() []string {
|
|||
return names
|
||||
}
|
||||
|
||||
// RegisterWebhooks registers webhook routes for channels that support HTTP webhooks
|
||||
// on the shared gateway mux, so they share the same port as the health server.
|
||||
func (m *Manager) RegisterWebhooks(mux *http.ServeMux) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
if line, ok := m.channels["line"]; ok {
|
||||
if lc, ok := line.(*LINEChannel); ok {
|
||||
lc.RegisterWebhook(mux)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) RegisterChannel(name string, channel Channel) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
|
||||
type Server struct {
|
||||
server *http.Server
|
||||
mux *http.ServeMux
|
||||
mu sync.RWMutex
|
||||
ready bool
|
||||
checks map[string]Check
|
||||
|
|
@ -30,14 +31,23 @@ type StatusResponse struct {
|
|||
Checks map[string]Check `json:"checks,omitempty"`
|
||||
}
|
||||
|
||||
type GatewayInfo struct {
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
Uptime string `json:"uptime"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
func NewServer(host string, port int) *Server {
|
||||
mux := http.NewServeMux()
|
||||
s := &Server{
|
||||
mux: mux,
|
||||
ready: false,
|
||||
checks: make(map[string]Check),
|
||||
startTime: time.Now(),
|
||||
}
|
||||
|
||||
mux.HandleFunc("/", s.rootHandler)
|
||||
mux.HandleFunc("/health", s.healthHandler)
|
||||
mux.HandleFunc("/ready", s.readyHandler)
|
||||
|
||||
|
|
@ -52,6 +62,11 @@ func NewServer(host string, port int) *Server {
|
|||
return s
|
||||
}
|
||||
|
||||
// Mux returns the HTTP mux so channels can register webhook routes on the gateway port.
|
||||
func (s *Server) Mux() *http.ServeMux {
|
||||
return s.mux
|
||||
}
|
||||
|
||||
func (s *Server) Start() error {
|
||||
s.mu.Lock()
|
||||
s.ready = true
|
||||
|
|
@ -103,6 +118,25 @@ func (s *Server) RegisterCheck(name string, checkFn func() (bool, string)) {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *Server) rootHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
uptime := time.Since(s.startTime)
|
||||
resp := GatewayInfo{
|
||||
Name: "PicoClaw Gateway",
|
||||
Status: "running",
|
||||
Uptime: uptime.String(),
|
||||
Version: "1.0.0",
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue