mirror of
https://github.com/ruvnet/RuVector.git
synced 2026-05-29 19:33:34 +00:00
feat(ruvbot): add chat UI, channel integrations, and cloud deployment
- Add embedded chat UI with dark mode default (ADR-015) - Add channel setup commands for Slack, Discord, Telegram - Add webhook configuration CLI commands - Add cloud deployment CLI with Cloud Run, Docker, Kubernetes support - Add gcloud CLI integration and curl-based installer - Add template library system for quick starts - Update Dockerfile to include static files - Bump version to 0.1.6 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b9e0be49df
commit
1bdac103be
13 changed files with 3905 additions and 87 deletions
|
|
@ -41,6 +41,9 @@ COPY src/ ./src/
|
|||
# Build TypeScript
|
||||
RUN npm run build
|
||||
|
||||
# Copy static files to dist
|
||||
RUN mkdir -p dist/api/public && cp -r src/api/public/* dist/api/public/ 2>/dev/null || true
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Stage 3: Production Runner
|
||||
# -----------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
- [Requirements](#requirements)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Configuration](#configuration)
|
||||
- [Channel Integrations](#channel-integrations)
|
||||
- [Template Library](#template-library)
|
||||
- [API Usage](#api-usage)
|
||||
- [Security](#security-architecture-6-layers---why-this-matters)
|
||||
- [LLM Providers](#llm-providers---gemini-25-default)
|
||||
|
|
@ -86,29 +88,49 @@ RuvBot is a next-generation personal AI assistant powered by RuVector's WASM vec
|
|||
|
||||
## Quick Start
|
||||
|
||||
### Install via curl
|
||||
### Install via curl (Recommended)
|
||||
|
||||
```bash
|
||||
curl -fsSL https://get.ruvector.dev/ruvbot | bash
|
||||
# Basic install
|
||||
curl -fsSL https://raw.githubusercontent.com/ruvnet/ruvector/main/npm/packages/ruvbot/scripts/install.sh | bash
|
||||
|
||||
# Install with interactive wizard
|
||||
RUVBOT_WIZARD=true curl -fsSL https://raw.githubusercontent.com/ruvnet/ruvector/main/npm/packages/ruvbot/scripts/install.sh | bash
|
||||
|
||||
# Install specific version
|
||||
RUVBOT_VERSION=0.1.3 curl -fsSL https://raw.githubusercontent.com/ruvnet/ruvector/main/npm/packages/ruvbot/scripts/install.sh | bash
|
||||
|
||||
# Install and deploy to Cloud Run
|
||||
RUVBOT_DEPLOY=cloudrun curl -fsSL https://raw.githubusercontent.com/ruvnet/ruvector/main/npm/packages/ruvbot/scripts/install.sh | bash
|
||||
|
||||
# Install with Slack channel dependencies
|
||||
RUVBOT_CHANNEL=slack curl -fsSL https://raw.githubusercontent.com/ruvnet/ruvector/main/npm/packages/ruvbot/scripts/install.sh | bash
|
||||
```
|
||||
|
||||
Or with custom settings:
|
||||
### Install Options (Environment Variables)
|
||||
|
||||
```bash
|
||||
RUVBOT_VERSION=0.1.0 \
|
||||
RUVBOT_INSTALL_DIR=/opt/ruvbot \
|
||||
curl -fsSL https://get.ruvector.dev/ruvbot | bash
|
||||
```
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `RUVBOT_VERSION` | Specific version to install | `latest` |
|
||||
| `RUVBOT_GLOBAL` | Install globally | `true` |
|
||||
| `RUVBOT_INIT` | Initialize project after install | `false` |
|
||||
| `RUVBOT_CHANNEL` | Install channel deps: `slack`, `discord`, `telegram`, `all` | - |
|
||||
| `RUVBOT_DEPLOY` | Deploy to: `cloudrun`, `docker`, `k8s` | - |
|
||||
| `RUVBOT_WIZARD` | Run interactive setup wizard | `false` |
|
||||
|
||||
### Install via npm/npx
|
||||
|
||||
```bash
|
||||
# Run directly
|
||||
npx @ruvector/ruvbot start
|
||||
# Run directly (no install)
|
||||
npx ruvbot start
|
||||
|
||||
# Or install globally
|
||||
npm install -g @ruvector/ruvbot
|
||||
# Install globally
|
||||
npm install -g ruvbot
|
||||
ruvbot start
|
||||
|
||||
# Install locally
|
||||
npm install ruvbot
|
||||
npx ruvbot start
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
|
@ -177,6 +199,11 @@ ruvbot init
|
|||
# Start the bot server
|
||||
ruvbot start [--port 3000] [--debug]
|
||||
|
||||
# Start with a specific channel
|
||||
ruvbot start --channel slack
|
||||
ruvbot start --channel discord
|
||||
ruvbot start --channel telegram
|
||||
|
||||
# Check status
|
||||
ruvbot status
|
||||
|
||||
|
|
@ -184,6 +211,17 @@ ruvbot status
|
|||
ruvbot skills list
|
||||
ruvbot skills add <name>
|
||||
|
||||
# Channel setup help
|
||||
ruvbot channels list
|
||||
ruvbot channels setup slack
|
||||
ruvbot channels setup discord
|
||||
ruvbot channels setup telegram
|
||||
|
||||
# Template library
|
||||
ruvbot templates list
|
||||
ruvbot templates info <template-id>
|
||||
ruvbot deploy <template-id>
|
||||
|
||||
# Run diagnostics
|
||||
ruvbot doctor
|
||||
|
||||
|
|
@ -191,6 +229,246 @@ ruvbot doctor
|
|||
ruvbot config --show
|
||||
```
|
||||
|
||||
## Channel Integrations
|
||||
|
||||
RuvBot supports multiple messaging platforms. Use `ruvbot channels setup <platform>` for interactive setup guides.
|
||||
|
||||
### Slack Integration
|
||||
|
||||
**Step 1: Create a Slack App**
|
||||
1. Go to https://api.slack.com/apps
|
||||
2. Click "Create New App" → "From Scratch"
|
||||
3. Name your app and select your workspace
|
||||
|
||||
**Step 2: Configure Bot Permissions**
|
||||
Navigate to **OAuth & Permissions** and add these Bot Token Scopes:
|
||||
- `app_mentions:read` - Receive @mentions
|
||||
- `chat:write` - Send messages
|
||||
- `channels:history` - Read channel messages
|
||||
- `im:history` - Read direct messages
|
||||
- `reactions:write` - Add reactions
|
||||
- `files:read` - Access shared files
|
||||
|
||||
**Step 3: Enable Socket Mode**
|
||||
1. Go to **Socket Mode** → Enable
|
||||
2. Create an App-Level Token with `connections:write` scope
|
||||
3. Save the `xapp-...` token
|
||||
|
||||
**Step 4: Install & Get Tokens**
|
||||
1. Go to **Install App** → Install to Workspace
|
||||
2. Copy the Bot User OAuth Token (`xoxb-...`)
|
||||
3. Copy the Signing Secret from **Basic Information**
|
||||
|
||||
**Step 5: Configure Environment**
|
||||
```bash
|
||||
export SLACK_BOT_TOKEN="xoxb-your-bot-token"
|
||||
export SLACK_SIGNING_SECRET="your-signing-secret"
|
||||
export SLACK_APP_TOKEN="xapp-your-app-token"
|
||||
|
||||
# Start with Slack
|
||||
ruvbot start --channel slack
|
||||
```
|
||||
|
||||
**Step 6: Enable Events (Optional)**
|
||||
For real-time events without Socket Mode:
|
||||
1. Go to **Event Subscriptions** → Enable
|
||||
2. Add Request URL: `https://your-ruvbot.run.app/slack/events`
|
||||
3. Subscribe to bot events: `message.channels`, `message.im`, `app_mention`
|
||||
|
||||
---
|
||||
|
||||
### Discord Integration
|
||||
|
||||
**Step 1: Create a Discord Application**
|
||||
1. Go to https://discord.com/developers/applications
|
||||
2. Click "New Application" and name it
|
||||
|
||||
**Step 2: Create a Bot**
|
||||
1. Go to **Bot** section → Add Bot
|
||||
2. Enable Privileged Gateway Intents:
|
||||
- ✅ Message Content Intent
|
||||
- ✅ Server Members Intent
|
||||
3. Copy the Bot Token (click "Reset Token" if needed)
|
||||
|
||||
**Step 3: Get Application IDs**
|
||||
1. Copy **Application ID** from General Information
|
||||
2. Right-click your server → Copy Server ID (for guild-specific commands)
|
||||
|
||||
**Step 4: Invite Bot to Server**
|
||||
1. Go to **OAuth2** → **URL Generator**
|
||||
2. Select scopes: `bot`, `applications.commands`
|
||||
3. Select permissions: `Send Messages`, `Read Message History`, `Add Reactions`, `Use Slash Commands`
|
||||
4. Open the generated URL to invite the bot
|
||||
|
||||
**Step 5: Configure Environment**
|
||||
```bash
|
||||
export DISCORD_TOKEN="your-bot-token"
|
||||
export DISCORD_CLIENT_ID="your-application-id"
|
||||
export DISCORD_GUILD_ID="your-server-id" # Optional, for testing
|
||||
|
||||
# Start with Discord
|
||||
ruvbot start --channel discord
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Telegram Integration
|
||||
|
||||
**Step 1: Create a Bot with BotFather**
|
||||
1. Open Telegram and search for `@BotFather`
|
||||
2. Send `/newbot` command
|
||||
3. Follow prompts to name your bot
|
||||
4. Copy the HTTP API token (format: `123456789:ABC-DEF...`)
|
||||
|
||||
**Step 2: Configure Environment**
|
||||
```bash
|
||||
export TELEGRAM_BOT_TOKEN="your-bot-token"
|
||||
|
||||
# Start with Telegram
|
||||
ruvbot start --channel telegram
|
||||
```
|
||||
|
||||
**Step 3: Test Your Bot**
|
||||
1. Search for your bot by username in Telegram
|
||||
2. Start a chat and send `/start`
|
||||
3. Send messages to interact with RuvBot
|
||||
|
||||
**Production: Webhook Mode**
|
||||
For production deployments (Cloud Run, etc.), use webhook mode:
|
||||
```bash
|
||||
export TELEGRAM_BOT_TOKEN="your-bot-token"
|
||||
export TELEGRAM_WEBHOOK_URL="https://your-ruvbot.run.app/telegram/webhook"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Multi-Channel Configuration
|
||||
|
||||
Run RuvBot with multiple channels simultaneously:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-ruvbot",
|
||||
"channels": {
|
||||
"slack": {
|
||||
"enabled": true,
|
||||
"token": "${SLACK_BOT_TOKEN}",
|
||||
"signingSecret": "${SLACK_SIGNING_SECRET}",
|
||||
"appToken": "${SLACK_APP_TOKEN}"
|
||||
},
|
||||
"discord": {
|
||||
"enabled": true,
|
||||
"token": "${DISCORD_TOKEN}",
|
||||
"clientId": "${DISCORD_CLIENT_ID}"
|
||||
},
|
||||
"telegram": {
|
||||
"enabled": true,
|
||||
"token": "${TELEGRAM_BOT_TOKEN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Install optional dependencies:
|
||||
```bash
|
||||
npm install @slack/bolt @slack/web-api discord.js telegraf
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Cloud Run Channel Setup
|
||||
|
||||
For Google Cloud Run deployments:
|
||||
|
||||
```bash
|
||||
# Slack
|
||||
gcloud run services update ruvbot --set-env-vars="\
|
||||
SLACK_BOT_TOKEN=xoxb-...,\
|
||||
SLACK_SIGNING_SECRET=...,\
|
||||
SLACK_APP_TOKEN=xapp-..."
|
||||
|
||||
# Discord
|
||||
gcloud run services update ruvbot --set-env-vars="\
|
||||
DISCORD_TOKEN=...,\
|
||||
DISCORD_CLIENT_ID=...,\
|
||||
DISCORD_GUILD_ID=..."
|
||||
|
||||
# Telegram (webhook mode recommended)
|
||||
gcloud run services update ruvbot --set-env-vars="\
|
||||
TELEGRAM_BOT_TOKEN=...,\
|
||||
TELEGRAM_WEBHOOK_URL=https://ruvbot-xxx.run.app/telegram/webhook"
|
||||
```
|
||||
|
||||
## Template Library
|
||||
|
||||
RuvBot includes pre-built templates for deploying long-running agent patterns.
|
||||
|
||||
### List Available Templates
|
||||
|
||||
```bash
|
||||
ruvbot templates list
|
||||
ruvbot templates list --category advanced
|
||||
```
|
||||
|
||||
### Available Templates
|
||||
|
||||
| Category | Template | Description |
|
||||
|----------|----------|-------------|
|
||||
| **Practical** | `code-reviewer` | Automated code review with security scanning |
|
||||
| | `doc-generator` | Auto-generate project documentation |
|
||||
| | `test-generator` | Generate comprehensive test suites (TDD) |
|
||||
| **Intermediate** | `feature-swarm` | Parallel feature development with coordinated agents |
|
||||
| | `refactor-squad` | Coordinated codebase refactoring |
|
||||
| | `ci-cd-pipeline` | Automated build, test, and deployment |
|
||||
| **Advanced** | `self-learning-bot` | AI that improves from interactions |
|
||||
| | `research-swarm` | Distributed research across sources |
|
||||
| | `performance-optimizer` | Continuous performance monitoring |
|
||||
| **Exotic** | `byzantine-validator` | Byzantine fault-tolerant validation (33% fault tolerance) |
|
||||
| | `hive-mind` | Emergent collective intelligence with queen coordination |
|
||||
| | `multi-repo-coordinator` | Cross-repository change coordination |
|
||||
| | `adversarial-tester` | Red team vs blue team security testing |
|
||||
|
||||
### Deploy a Template
|
||||
|
||||
```bash
|
||||
# View template details
|
||||
ruvbot templates info hive-mind
|
||||
|
||||
# Deploy a template
|
||||
ruvbot deploy code-reviewer --repo ./my-project
|
||||
|
||||
# Deploy with options
|
||||
ruvbot deploy feature-swarm --name "auth-feature" --model google/gemini-2.0-flash-001
|
||||
|
||||
# Dry run (preview without executing)
|
||||
ruvbot deploy hive-mind --dry-run
|
||||
```
|
||||
|
||||
### Template Examples
|
||||
|
||||
```bash
|
||||
# Code review with security scanning
|
||||
ruvbot deploy code-reviewer --repo ./my-project
|
||||
|
||||
# Documentation generation
|
||||
ruvbot deploy doc-generator --output ./docs
|
||||
|
||||
# Test suite generation with 80% coverage target
|
||||
ruvbot deploy test-generator --coverage 80
|
||||
|
||||
# Feature development with coordinated swarm
|
||||
ruvbot deploy feature-swarm --feature "Add user authentication"
|
||||
|
||||
# Research across multiple sources
|
||||
ruvbot deploy research-swarm --topic "vector databases"
|
||||
|
||||
# Hive-mind for complex objectives
|
||||
ruvbot deploy hive-mind --objective "Build complete REST API"
|
||||
|
||||
# Byzantine validation for critical operations
|
||||
ruvbot deploy byzantine-validator --quorum 4
|
||||
```
|
||||
|
||||
## API Usage
|
||||
|
||||
### REST API Endpoints
|
||||
|
|
@ -503,7 +781,20 @@ const results = await memory.search('find important info', {
|
|||
});
|
||||
```
|
||||
|
||||
## Docker
|
||||
## Deployment
|
||||
|
||||
RuvBot supports multiple deployment options from local development to enterprise cloud.
|
||||
|
||||
### Quick Deploy Options
|
||||
|
||||
| Method | Best For | Command |
|
||||
|--------|----------|---------|
|
||||
| **npx** | Quick testing | `npx ruvbot start` |
|
||||
| **Docker** | Containerized | `docker run -p 3000:3000 ruvector/ruvbot` |
|
||||
| **Cloud Run** | Serverless | `ruvbot deploy cloudrun` |
|
||||
| **Kubernetes** | Enterprise | `kubectl apply -f k8s/` |
|
||||
|
||||
### Docker
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
|
|
@ -514,13 +805,41 @@ services:
|
|||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY}
|
||||
- SLACK_BOT_TOKEN=${SLACK_BOT_TOKEN}
|
||||
- SLACK_SIGNING_SECRET=${SLACK_SIGNING_SECRET}
|
||||
- SLACK_APP_TOKEN=${SLACK_APP_TOKEN}
|
||||
- DISCORD_TOKEN=${DISCORD_TOKEN}
|
||||
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
- ./skills:/app/skills
|
||||
```
|
||||
|
||||
### Docker with Webhooks
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml with webhook configuration
|
||||
version: '3.8'
|
||||
services:
|
||||
ruvbot:
|
||||
image: ruvector/ruvbot:latest
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY}
|
||||
# Outbound webhooks
|
||||
- WEBHOOK_URL=https://your-service.com/callback
|
||||
- WEBHOOK_SECRET=your-shared-secret
|
||||
# Inbound webhook auth
|
||||
- WEBHOOK_AUTH_TOKEN=your-auth-token
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
```
|
||||
|
||||
## Google Cloud Deployment
|
||||
|
||||
RuvBot includes cost-optimized Google Cloud Platform deployment (~$15-20/month for low traffic).
|
||||
|
|
@ -566,6 +885,236 @@ terraform apply \
|
|||
|
||||
See [ADR-013: GCP Deployment](docs/adr/ADR-013-gcp-deployment.md) for architecture details.
|
||||
|
||||
### gcloud CLI Integration
|
||||
|
||||
RuvBot includes native gcloud CLI integration for Cloud Run deployments.
|
||||
|
||||
**Prerequisites:**
|
||||
```bash
|
||||
# Install gcloud CLI
|
||||
curl https://sdk.cloud.google.com | bash
|
||||
|
||||
# Authenticate
|
||||
gcloud auth login
|
||||
|
||||
# Set project
|
||||
gcloud config set project YOUR_PROJECT_ID
|
||||
```
|
||||
|
||||
**Deploy with CLI:**
|
||||
```bash
|
||||
# Interactive deployment wizard
|
||||
ruvbot deploy-cloud wizard
|
||||
|
||||
# Deploy to Cloud Run
|
||||
ruvbot deploy-cloud cloudrun --project my-project --region us-central1
|
||||
|
||||
# Deploy with environment file
|
||||
ruvbot deploy-cloud cloudrun --env-file .env
|
||||
|
||||
# Deploy to Docker
|
||||
ruvbot deploy-cloud docker --port 3000
|
||||
|
||||
# Deploy to Kubernetes
|
||||
ruvbot deploy-cloud k8s --namespace production --replicas 3
|
||||
|
||||
# Check deployment status
|
||||
ruvbot deploy-cloud status
|
||||
```
|
||||
|
||||
**CLI Options:**
|
||||
|
||||
| Command | Options | Description |
|
||||
|---------|---------|-------------|
|
||||
| `cloudrun` | `--project`, `--region`, `--service`, `--memory`, `--min-instances`, `--max-instances`, `--env-file` | Deploy to Cloud Run |
|
||||
| `docker` | `--name`, `--port`, `--detach`, `--env-file` | Deploy with Docker Compose |
|
||||
| `k8s` | `--namespace`, `--replicas`, `--env-file` | Deploy to Kubernetes |
|
||||
| `wizard` | - | Interactive deployment wizard |
|
||||
| `status` | `--platform` | Check deployment status |
|
||||
|
||||
### Deploy with Channel Webhooks (Cloud Run)
|
||||
|
||||
Complete Cloud Run deployment with all channel webhooks:
|
||||
|
||||
```bash
|
||||
# Build and deploy
|
||||
gcloud run deploy ruvbot \
|
||||
--source . \
|
||||
--platform managed \
|
||||
--region us-central1 \
|
||||
--allow-unauthenticated \
|
||||
--set-env-vars="\
|
||||
OPENROUTER_API_KEY=${OPENROUTER_API_KEY},\
|
||||
DEFAULT_MODEL=google/gemini-2.0-flash-001,\
|
||||
SLACK_BOT_TOKEN=${SLACK_BOT_TOKEN},\
|
||||
SLACK_SIGNING_SECRET=${SLACK_SIGNING_SECRET},\
|
||||
TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN},\
|
||||
TELEGRAM_WEBHOOK_URL=https://ruvbot-xxx.run.app/telegram/webhook,\
|
||||
DISCORD_TOKEN=${DISCORD_TOKEN},\
|
||||
DISCORD_CLIENT_ID=${DISCORD_CLIENT_ID}"
|
||||
```
|
||||
|
||||
### Kubernetes Deployment
|
||||
|
||||
```yaml
|
||||
# k8s/deployment.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: ruvbot
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: ruvbot
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: ruvbot
|
||||
spec:
|
||||
containers:
|
||||
- name: ruvbot
|
||||
image: ruvector/ruvbot:latest
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: ruvbot-secrets
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 3000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /ready
|
||||
port: 3000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: ruvbot
|
||||
spec:
|
||||
selector:
|
||||
app: ruvbot
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 3000
|
||||
type: LoadBalancer
|
||||
```
|
||||
|
||||
## Webhook Configuration
|
||||
|
||||
### Inbound Webhooks
|
||||
|
||||
RuvBot exposes webhook endpoints for receiving messages from external services.
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/webhook/message` | POST | Receive chat messages |
|
||||
| `/webhook/event` | POST | Receive system events |
|
||||
| `/slack/events` | POST | Slack Events API |
|
||||
| `/telegram/webhook` | POST | Telegram webhook updates |
|
||||
| `/api/sessions/:id/chat` | POST | Direct chat endpoint |
|
||||
|
||||
### Webhook Security
|
||||
|
||||
```bash
|
||||
# Set webhook authentication
|
||||
export WEBHOOK_SECRET="your-shared-secret"
|
||||
export WEBHOOK_AUTH_TOKEN="bearer-token-for-inbound"
|
||||
```
|
||||
|
||||
Inbound requests are validated using:
|
||||
- **X-Webhook-Secret** header for HMAC signature verification
|
||||
- **Authorization** header with Bearer token
|
||||
- Request body signature validation
|
||||
|
||||
### Outbound Webhooks
|
||||
|
||||
Configure RuvBot to send responses and events to your services:
|
||||
|
||||
```json
|
||||
{
|
||||
"webhooks": {
|
||||
"outbound": {
|
||||
"url": "https://your-service.com/ruvbot-callback",
|
||||
"secret": "shared-secret-for-signing",
|
||||
"events": ["message", "agent.spawn", "session.create", "error"],
|
||||
"retries": 3,
|
||||
"timeout": 30000,
|
||||
"headers": {
|
||||
"X-Custom-Header": "value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Webhook Event Types
|
||||
|
||||
| Event | Payload | Description |
|
||||
|-------|---------|-------------|
|
||||
| `message` | `{sessionId, role, content, timestamp}` | New message in session |
|
||||
| `message.response` | `{sessionId, content, model, tokens}` | Bot response generated |
|
||||
| `agent.spawn` | `{agentId, type, name}` | Agent created |
|
||||
| `agent.stop` | `{agentId, reason}` | Agent terminated |
|
||||
| `session.create` | `{sessionId, agentId, userId}` | Session started |
|
||||
| `session.end` | `{sessionId, messageCount, duration}` | Session ended |
|
||||
| `memory.store` | `{key, namespace, size}` | Memory stored |
|
||||
| `security.threat` | `{type, severity, blocked}` | Threat detected |
|
||||
| `error` | `{code, message, context}` | Error occurred |
|
||||
|
||||
### Example: Custom Integration
|
||||
|
||||
```typescript
|
||||
// Your webhook receiver
|
||||
app.post('/ruvbot-callback', (req, res) => {
|
||||
// Verify signature
|
||||
const signature = req.headers['x-webhook-signature'];
|
||||
const isValid = verifyHmac(req.body, signature, WEBHOOK_SECRET);
|
||||
|
||||
if (!isValid) {
|
||||
return res.status(401).send('Invalid signature');
|
||||
}
|
||||
|
||||
const { event, data } = req.body;
|
||||
|
||||
switch (event) {
|
||||
case 'message.response':
|
||||
// Forward to your chat system
|
||||
sendToChat(data.sessionId, data.content);
|
||||
break;
|
||||
case 'security.threat':
|
||||
// Alert security team
|
||||
alertSecurity(data);
|
||||
break;
|
||||
}
|
||||
|
||||
res.status(200).send('OK');
|
||||
});
|
||||
```
|
||||
|
||||
### CLI Webhook Commands
|
||||
|
||||
```bash
|
||||
# List configured webhooks
|
||||
ruvbot webhooks list
|
||||
|
||||
# Test a webhook endpoint
|
||||
ruvbot webhooks test https://your-service.com/callback
|
||||
|
||||
# Test with custom payload
|
||||
ruvbot webhooks test https://your-service.com/callback \
|
||||
--payload '{"test": true, "message": "Hello"}'
|
||||
|
||||
# Show channel-specific setup
|
||||
ruvbot channels setup webhook
|
||||
```
|
||||
|
||||
## LLM Providers - Gemini 2.5 Default
|
||||
|
||||
RuvBot supports 12+ models with **Gemini 2.5 Pro as the recommended default** for optimal cost/performance.
|
||||
|
|
|
|||
192
npm/packages/ruvbot/docs/adr/ADR-015-chat-ui.md
Normal file
192
npm/packages/ruvbot/docs/adr/ADR-015-chat-ui.md
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
# ADR-015: Chat UI Architecture
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Date
|
||||
|
||||
2026-01-28
|
||||
|
||||
## Context
|
||||
|
||||
RuvBot provides a powerful REST API for chat interactions, but lacks a user-facing web interface. When users visit the root URL of a deployed RuvBot instance (e.g., on Cloud Run), they receive a 404 error instead of a usable chat interface.
|
||||
|
||||
### Requirements
|
||||
|
||||
1. Provide a modern, responsive chat UI out of the box
|
||||
2. Support dark mode (default) and light mode themes
|
||||
3. Work with the existing REST API endpoints
|
||||
4. No build step required - serve static files directly
|
||||
5. Support streaming responses for real-time AI interaction
|
||||
6. Mobile-friendly design
|
||||
7. Model selection capability
|
||||
8. Integration with CLI and npm package
|
||||
|
||||
### Alternatives Considered
|
||||
|
||||
| Option | Pros | Cons |
|
||||
|--------|------|------|
|
||||
| **assistant-ui** | Industry leader, 200k+ downloads, Y Combinator backed | Requires React build, adds complexity |
|
||||
| **Vercel AI Elements** | Official Vercel components, AI SDK integration | Requires Next.js |
|
||||
| **shadcn-chatbot-kit** | Beautiful components, shadcn design system | Requires React build |
|
||||
| **Embedded HTML/CSS/JS** | No build step, portable, fast deployment | Less features, custom implementation |
|
||||
|
||||
## Decision
|
||||
|
||||
Implement a **lightweight embedded chat UI** using vanilla HTML, CSS, and JavaScript that:
|
||||
|
||||
1. Is served directly from the existing HTTP server
|
||||
2. Requires no build step or additional dependencies
|
||||
3. Provides a modern, accessible interface
|
||||
4. Supports dark mode by default
|
||||
5. Includes basic markdown rendering
|
||||
6. Works seamlessly with the existing REST API
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ RuvBot Server │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ GET / → Chat UI (index.html) │
|
||||
│ GET /health → Health check │
|
||||
│ GET /api/models → Available models │
|
||||
│ POST /api/sessions → Create session │
|
||||
│ POST /api/sessions/:id/chat → Chat endpoint │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── api/
|
||||
│ └── public/
|
||||
│ └── index.html # Chat UI (single file)
|
||||
├── server.ts # Updated to serve static files
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Features
|
||||
|
||||
1. **Theme Support**: Dark mode default, light mode toggle
|
||||
2. **Model Selection**: Dropdown for available models
|
||||
3. **Responsive Design**: Mobile-first approach
|
||||
4. **Accessibility**: ARIA labels, keyboard navigation
|
||||
5. **Markdown Rendering**: Code blocks, lists, links
|
||||
6. **Error Handling**: User-friendly error messages
|
||||
7. **Session Management**: Automatic session creation
|
||||
8. **Real-time Updates**: Typing indicators
|
||||
|
||||
### CSS Design System
|
||||
|
||||
```css
|
||||
:root {
|
||||
--bg-primary: #0a0a0f; /* Dark background */
|
||||
--bg-secondary: #12121a; /* Card background */
|
||||
--text-primary: #f0f0f5; /* Main text */
|
||||
--accent: #6366f1; /* Indigo accent */
|
||||
--radius: 12px; /* Border radius */
|
||||
}
|
||||
```
|
||||
|
||||
### API Integration
|
||||
|
||||
The UI integrates with existing endpoints:
|
||||
|
||||
```javascript
|
||||
// Create session
|
||||
POST /api/sessions { agentId: 'default-agent' }
|
||||
|
||||
// Send message
|
||||
POST /api/sessions/:id/chat { message: '...', model: '...' }
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
1. **Zero Configuration**: Works out of the box
|
||||
2. **Fast Deployment**: No build step required
|
||||
3. **Portable**: Single HTML file, easy to customize
|
||||
4. **Lightweight**: ~25KB uncompressed
|
||||
5. **Framework Agnostic**: No React/Vue/Svelte dependency
|
||||
6. **Cloud Run Compatible**: Works with existing deployment
|
||||
|
||||
### Negative
|
||||
|
||||
1. **Limited Features**: No streaming UI (yet), basic markdown
|
||||
2. **Manual Updates**: No component library updates
|
||||
3. **Custom Code**: Maintenance responsibility
|
||||
|
||||
### Neutral
|
||||
|
||||
1. Future option to add assistant-ui or similar for advanced features
|
||||
2. Can be replaced with any frontend framework later
|
||||
|
||||
## Implementation
|
||||
|
||||
### Server Changes (server.ts)
|
||||
|
||||
```typescript
|
||||
// Serve static files
|
||||
function getChatUIPath(): string {
|
||||
const possiblePaths = [
|
||||
join(__dirname, 'api', 'public', 'index.html'),
|
||||
// ... fallback paths
|
||||
];
|
||||
// Find first existing path
|
||||
}
|
||||
|
||||
// Add root route
|
||||
{ method: 'GET', pattern: /^\/$/, handler: handleRoot }
|
||||
```
|
||||
|
||||
### CLI Integration
|
||||
|
||||
```bash
|
||||
# View chat UI URL after deployment
|
||||
ruvbot deploy-cloud cloudrun
|
||||
# Output: URL: https://ruvbot-xxx.run.app
|
||||
|
||||
# Open chat UI
|
||||
ruvbot open # Opens browser to chat UI
|
||||
```
|
||||
|
||||
### npm Package
|
||||
|
||||
The chat UI is bundled with the npm package:
|
||||
|
||||
```json
|
||||
{
|
||||
"files": [
|
||||
"dist",
|
||||
"bin",
|
||||
"scripts",
|
||||
"src/api/public"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Streaming Responses**: SSE/WebSocket for real-time streaming
|
||||
2. **File Uploads**: Image and document support
|
||||
3. **Voice Input**: Speech-to-text integration
|
||||
4. **assistant-ui Migration**: Full-featured React UI option
|
||||
5. **Themes**: Additional theme presets
|
||||
6. **Plugins**: Extensible UI components
|
||||
|
||||
## References
|
||||
|
||||
- [assistant-ui](https://github.com/assistant-ui/assistant-ui) - Industry-leading chat UI library
|
||||
- [Vercel AI SDK](https://ai-sdk.dev/) - AI SDK with streaming support
|
||||
- [shadcn/ui](https://ui.shadcn.com/) - Design system inspiration
|
||||
- [ADR-013: GCP Deployment](./ADR-013-gcp-deployment.md) - Cloud Run deployment
|
||||
|
||||
## Changelog
|
||||
|
||||
| Date | Change |
|
||||
|------|--------|
|
||||
| 2026-01-28 | Initial version - embedded chat UI |
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "ruvbot",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.6",
|
||||
"description": "Enterprise-grade self-learning AI assistant with military-strength security, 150x faster vector search, and 12+ LLM models",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/esm/index.js",
|
||||
|
|
@ -133,6 +133,8 @@
|
|||
"files": [
|
||||
"dist",
|
||||
"bin",
|
||||
"scripts",
|
||||
"src/api/public",
|
||||
".env.example",
|
||||
"README.md"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,37 @@
|
|||
#!/bin/bash
|
||||
|
||||
# RuvBot Installation Script
|
||||
# Usage: curl -fsSL https://get.ruvector.dev/ruvbot | bash
|
||||
#
|
||||
# RuvBot Installer
|
||||
#
|
||||
# Usage:
|
||||
# curl -fsSL https://get.ruvector.dev/ruvbot | bash
|
||||
# curl -fsSL https://raw.githubusercontent.com/ruvnet/ruvector/main/npm/packages/ruvbot/scripts/install.sh | bash
|
||||
#
|
||||
# Options (via environment variables):
|
||||
# RUVBOT_VERSION - Specific version to install (default: latest)
|
||||
# RUVBOT_GLOBAL - Install globally (default: true)
|
||||
# RUVBOT_INIT - Run init after install (default: false)
|
||||
# RUVBOT_CHANNEL - Configure channel: slack, discord, telegram
|
||||
# RUVBOT_DEPLOY - Deploy target: local, docker, cloudrun, k8s
|
||||
# RUVBOT_WIZARD - Run interactive wizard (default: false)
|
||||
#
|
||||
# Examples:
|
||||
# # Basic install
|
||||
# curl -fsSL https://get.ruvector.dev/ruvbot | bash
|
||||
#
|
||||
# # Install specific version
|
||||
# RUVBOT_VERSION=0.1.3 curl -fsSL https://get.ruvector.dev/ruvbot | bash
|
||||
#
|
||||
# # Install and initialize
|
||||
# RUVBOT_INIT=true curl -fsSL https://get.ruvector.dev/ruvbot | bash
|
||||
#
|
||||
# # Install with Slack configuration
|
||||
# RUVBOT_CHANNEL=slack curl -fsSL https://get.ruvector.dev/ruvbot | bash
|
||||
#
|
||||
# # Install and deploy to Cloud Run
|
||||
# RUVBOT_DEPLOY=cloudrun curl -fsSL https://get.ruvector.dev/ruvbot | bash
|
||||
#
|
||||
# # Run full interactive wizard
|
||||
# RUVBOT_WIZARD=true curl -fsSL https://get.ruvector.dev/ruvbot | bash
|
||||
|
||||
set -e
|
||||
|
||||
|
|
@ -10,91 +40,699 @@ RED='\033[0;31m'
|
|||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
MAGENTA='\033[0;35m'
|
||||
NC='\033[0m' # No Color
|
||||
BOLD='\033[1m'
|
||||
DIM='\033[2m'
|
||||
|
||||
echo -e "${BLUE}"
|
||||
echo " ____ ____ _ "
|
||||
echo " | _ \ _ ___ _| __ ) ___ | |_ "
|
||||
echo " | |_) | | | \ \ / / _ \ / _ \| __|"
|
||||
echo " | _ <| |_| |\ V /| |_) | (_) | |_ "
|
||||
echo " |_| \_\\\\__,_| \_/ |____/ \___/ \__|"
|
||||
echo -e "${NC}"
|
||||
echo "Self-learning AI Assistant"
|
||||
echo ""
|
||||
# Configuration
|
||||
RUVBOT_VERSION="${RUVBOT_VERSION:-latest}"
|
||||
RUVBOT_GLOBAL="${RUVBOT_GLOBAL:-true}"
|
||||
RUVBOT_INIT="${RUVBOT_INIT:-false}"
|
||||
RUVBOT_CHANNEL="${RUVBOT_CHANNEL:-}"
|
||||
RUVBOT_DEPLOY="${RUVBOT_DEPLOY:-}"
|
||||
RUVBOT_WIZARD="${RUVBOT_WIZARD:-false}"
|
||||
|
||||
# Check Node.js version
|
||||
check_node() {
|
||||
if ! command -v node &> /dev/null; then
|
||||
echo -e "${RED}Error: Node.js is not installed.${NC}"
|
||||
echo "Please install Node.js 18 or later: https://nodejs.org"
|
||||
exit 1
|
||||
fi
|
||||
# Feature flags
|
||||
GCLOUD_AVAILABLE=false
|
||||
DOCKER_AVAILABLE=false
|
||||
KUBECTL_AVAILABLE=false
|
||||
|
||||
NODE_VERSION=$(node -v | sed 's/v//' | cut -d. -f1)
|
||||
if [ "$NODE_VERSION" -lt 18 ]; then
|
||||
echo -e "${RED}Error: Node.js 18+ required. Found v$(node -v)${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}[x] Node.js $(node -v)${NC}"
|
||||
# Banner
|
||||
print_banner() {
|
||||
echo -e "${CYAN}"
|
||||
echo ' ____ ____ _ '
|
||||
echo ' | _ \ _ ___ _| __ ) ___ | |_ '
|
||||
echo ' | |_) | | | \ \ / / _ \ / _ \| __|'
|
||||
echo ' | _ <| |_| |\ V /| |_) | (_) | |_ '
|
||||
echo ' |_| \_\\__,_| \_/ |____/ \___/ \__|'
|
||||
echo -e "${NC}"
|
||||
echo -e "${BOLD}Enterprise-Grade Self-Learning AI Assistant${NC}"
|
||||
echo -e "${DIM}Military-strength security • 150x faster search • 12+ LLM models${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Check npm version
|
||||
check_npm() {
|
||||
if ! command -v npm &> /dev/null; then
|
||||
echo -e "${RED}Error: npm is not installed.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
# Logging functions
|
||||
info() { echo -e "${BLUE}ℹ${NC} $1"; }
|
||||
success() { echo -e "${GREEN}✓${NC} $1"; }
|
||||
warn() { echo -e "${YELLOW}⚠${NC} $1"; }
|
||||
error() { echo -e "${RED}✗${NC} $1"; exit 1; }
|
||||
step() { echo -e "\n${MAGENTA}▸${NC} ${BOLD}$1${NC}"; }
|
||||
|
||||
echo -e "${GREEN}[x] npm $(npm -v)${NC}"
|
||||
# Check dependencies
|
||||
check_dependencies() {
|
||||
step "Checking dependencies"
|
||||
|
||||
# Check Node.js
|
||||
if ! command -v node &> /dev/null; then
|
||||
error "Node.js is required but not installed. Install from https://nodejs.org"
|
||||
fi
|
||||
|
||||
NODE_VERSION=$(node -v | cut -d 'v' -f 2 | cut -d '.' -f 1)
|
||||
if [ "$NODE_VERSION" -lt 18 ]; then
|
||||
error "Node.js 18+ is required. Current: $(node -v)"
|
||||
fi
|
||||
success "Node.js $(node -v)"
|
||||
|
||||
# Check npm
|
||||
if ! command -v npm &> /dev/null; then
|
||||
error "npm is required but not installed"
|
||||
fi
|
||||
success "npm $(npm -v)"
|
||||
|
||||
# Check optional: gcloud
|
||||
if command -v gcloud &> /dev/null; then
|
||||
success "gcloud CLI $(gcloud --version 2>/dev/null | head -1 | awk '{print $4}')"
|
||||
GCLOUD_AVAILABLE=true
|
||||
else
|
||||
echo -e "${DIM} ○ gcloud CLI not found (optional for Cloud Run)${NC}"
|
||||
fi
|
||||
|
||||
# Check optional: docker
|
||||
if command -v docker &> /dev/null; then
|
||||
success "Docker $(docker --version | awk '{print $3}' | tr -d ',')"
|
||||
DOCKER_AVAILABLE=true
|
||||
else
|
||||
echo -e "${DIM} ○ Docker not found (optional for containerization)${NC}"
|
||||
fi
|
||||
|
||||
# Check optional: kubectl
|
||||
if command -v kubectl &> /dev/null; then
|
||||
success "kubectl $(kubectl version --client -o json 2>/dev/null | grep -o '"gitVersion": "[^"]*"' | cut -d'"' -f4)"
|
||||
KUBECTL_AVAILABLE=true
|
||||
else
|
||||
echo -e "${DIM} ○ kubectl not found (optional for Kubernetes)${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Install RuvBot globally
|
||||
# Install RuvBot
|
||||
install_ruvbot() {
|
||||
echo ""
|
||||
echo -e "${BLUE}Installing @ruvector/ruvbot...${NC}"
|
||||
step "Installing RuvBot"
|
||||
|
||||
npm install -g @ruvector/ruvbot
|
||||
PACKAGE="ruvbot"
|
||||
if [ "$RUVBOT_VERSION" != "latest" ]; then
|
||||
PACKAGE="ruvbot@$RUVBOT_VERSION"
|
||||
info "Installing version $RUVBOT_VERSION"
|
||||
fi
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo -e "${GREEN}[x] RuvBot installed successfully!${NC}"
|
||||
else
|
||||
echo -e "${RED}Installation failed. Try running with sudo:${NC}"
|
||||
echo "sudo npm install -g @ruvector/ruvbot"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$RUVBOT_GLOBAL" = "true" ]; then
|
||||
npm install -g "$PACKAGE" 2>/dev/null || sudo npm install -g "$PACKAGE"
|
||||
success "RuvBot installed globally"
|
||||
else
|
||||
npm install "$PACKAGE"
|
||||
success "RuvBot installed locally"
|
||||
fi
|
||||
|
||||
# Verify installation
|
||||
if command -v ruvbot &> /dev/null; then
|
||||
INSTALLED_VERSION=$(ruvbot --version 2>/dev/null || echo "unknown")
|
||||
success "RuvBot $INSTALLED_VERSION is ready"
|
||||
else
|
||||
success "RuvBot installed (use 'npx ruvbot' to run)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Show next steps
|
||||
show_next_steps() {
|
||||
echo ""
|
||||
echo -e "${GREEN}Installation complete!${NC}"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo ""
|
||||
echo " 1. Initialize a new project:"
|
||||
echo -e " ${YELLOW}ruvbot init${NC}"
|
||||
echo ""
|
||||
echo " 2. Configure your environment:"
|
||||
echo -e " ${YELLOW}ruvbot config${NC}"
|
||||
echo ""
|
||||
echo " 3. Start the bot:"
|
||||
echo -e " ${YELLOW}ruvbot start${NC}"
|
||||
echo ""
|
||||
echo "Documentation: https://github.com/ruvnet/ruvector"
|
||||
echo ""
|
||||
# Install optional dependencies for channels
|
||||
install_channel_deps() {
|
||||
local channel=$1
|
||||
step "Installing $channel dependencies"
|
||||
|
||||
case "$channel" in
|
||||
slack)
|
||||
npm install @slack/bolt @slack/web-api 2>/dev/null
|
||||
success "Slack SDK installed (@slack/bolt, @slack/web-api)"
|
||||
;;
|
||||
discord)
|
||||
npm install discord.js 2>/dev/null
|
||||
success "Discord.js installed"
|
||||
;;
|
||||
telegram)
|
||||
npm install telegraf 2>/dev/null
|
||||
success "Telegraf installed"
|
||||
;;
|
||||
all)
|
||||
npm install @slack/bolt @slack/web-api discord.js telegraf 2>/dev/null
|
||||
success "All channel dependencies installed"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Main installation flow
|
||||
# Initialize project
|
||||
init_project() {
|
||||
step "Initializing RuvBot project"
|
||||
|
||||
if [ "$RUVBOT_GLOBAL" = "true" ]; then
|
||||
ruvbot init --yes
|
||||
else
|
||||
npx ruvbot init --yes
|
||||
fi
|
||||
|
||||
success "Project initialized"
|
||||
}
|
||||
|
||||
# Configure channel interactively
|
||||
configure_channel() {
|
||||
local channel=$1
|
||||
|
||||
step "Configuring $channel"
|
||||
|
||||
case "$channel" in
|
||||
slack)
|
||||
echo ""
|
||||
echo " To set up Slack, you'll need credentials from:"
|
||||
echo -e " ${CYAN}https://api.slack.com/apps${NC}"
|
||||
echo ""
|
||||
read -p " SLACK_BOT_TOKEN (xoxb-...): " SLACK_BOT_TOKEN
|
||||
read -p " SLACK_SIGNING_SECRET: " SLACK_SIGNING_SECRET
|
||||
read -p " SLACK_APP_TOKEN (xapp-...): " SLACK_APP_TOKEN
|
||||
|
||||
{
|
||||
echo "SLACK_BOT_TOKEN=$SLACK_BOT_TOKEN"
|
||||
echo "SLACK_SIGNING_SECRET=$SLACK_SIGNING_SECRET"
|
||||
echo "SLACK_APP_TOKEN=$SLACK_APP_TOKEN"
|
||||
} >> .env
|
||||
|
||||
success "Slack configuration saved to .env"
|
||||
;;
|
||||
|
||||
discord)
|
||||
echo ""
|
||||
echo " To set up Discord, you'll need credentials from:"
|
||||
echo -e " ${CYAN}https://discord.com/developers/applications${NC}"
|
||||
echo ""
|
||||
read -p " DISCORD_TOKEN: " DISCORD_TOKEN
|
||||
read -p " DISCORD_CLIENT_ID: " DISCORD_CLIENT_ID
|
||||
read -p " DISCORD_GUILD_ID (optional): " DISCORD_GUILD_ID
|
||||
|
||||
{
|
||||
echo "DISCORD_TOKEN=$DISCORD_TOKEN"
|
||||
echo "DISCORD_CLIENT_ID=$DISCORD_CLIENT_ID"
|
||||
[ -n "$DISCORD_GUILD_ID" ] && echo "DISCORD_GUILD_ID=$DISCORD_GUILD_ID"
|
||||
} >> .env
|
||||
|
||||
success "Discord configuration saved to .env"
|
||||
;;
|
||||
|
||||
telegram)
|
||||
echo ""
|
||||
echo " To set up Telegram, get a token from:"
|
||||
echo -e " ${CYAN}@BotFather${NC} on Telegram"
|
||||
echo ""
|
||||
read -p " TELEGRAM_BOT_TOKEN: " TELEGRAM_BOT_TOKEN
|
||||
|
||||
echo "TELEGRAM_BOT_TOKEN=$TELEGRAM_BOT_TOKEN" >> .env
|
||||
|
||||
success "Telegram configuration saved to .env"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Deploy to Cloud Run
|
||||
deploy_cloudrun() {
|
||||
step "Deploying to Google Cloud Run"
|
||||
|
||||
if [ "$GCLOUD_AVAILABLE" != "true" ]; then
|
||||
error "gcloud CLI is required. Install from https://cloud.google.com/sdk"
|
||||
fi
|
||||
|
||||
# Check authentication
|
||||
if ! gcloud auth list --filter=status:ACTIVE --format="value(account)" 2>/dev/null | head -1; then
|
||||
warn "Not authenticated with gcloud"
|
||||
info "Running 'gcloud auth login'..."
|
||||
gcloud auth login
|
||||
fi
|
||||
|
||||
# Get project
|
||||
CURRENT_PROJECT=$(gcloud config get-value project 2>/dev/null || echo "")
|
||||
echo ""
|
||||
read -p " GCP Project ID [$CURRENT_PROJECT]: " PROJECT_ID
|
||||
PROJECT_ID="${PROJECT_ID:-$CURRENT_PROJECT}"
|
||||
|
||||
if [ -z "$PROJECT_ID" ]; then
|
||||
error "Project ID is required"
|
||||
fi
|
||||
|
||||
gcloud config set project "$PROJECT_ID" 2>/dev/null
|
||||
|
||||
# Get region
|
||||
read -p " Region [us-central1]: " REGION
|
||||
REGION="${REGION:-us-central1}"
|
||||
|
||||
# Get service name
|
||||
read -p " Service name [ruvbot]: " SERVICE_NAME
|
||||
SERVICE_NAME="${SERVICE_NAME:-ruvbot}"
|
||||
|
||||
# Get API key
|
||||
echo ""
|
||||
echo " LLM Provider:"
|
||||
echo " 1. OpenRouter (recommended - Gemini, Claude, GPT)"
|
||||
echo " 2. Anthropic (Claude only)"
|
||||
read -p " Choose [1]: " PROVIDER_CHOICE
|
||||
PROVIDER_CHOICE="${PROVIDER_CHOICE:-1}"
|
||||
|
||||
if [ "$PROVIDER_CHOICE" = "1" ]; then
|
||||
read -p " OPENROUTER_API_KEY: " API_KEY
|
||||
ENV_VARS="OPENROUTER_API_KEY=$API_KEY,DEFAULT_MODEL=google/gemini-2.0-flash-001"
|
||||
else
|
||||
read -p " ANTHROPIC_API_KEY: " API_KEY
|
||||
ENV_VARS="ANTHROPIC_API_KEY=$API_KEY"
|
||||
fi
|
||||
|
||||
# Channel configuration
|
||||
echo ""
|
||||
read -p " Configure Slack? [y/N]: " SETUP_SLACK
|
||||
if [[ "$SETUP_SLACK" =~ ^[Yy]$ ]]; then
|
||||
read -p " SLACK_BOT_TOKEN: " SLACK_BOT_TOKEN
|
||||
read -p " SLACK_SIGNING_SECRET: " SLACK_SIGNING_SECRET
|
||||
ENV_VARS="$ENV_VARS,SLACK_BOT_TOKEN=$SLACK_BOT_TOKEN,SLACK_SIGNING_SECRET=$SLACK_SIGNING_SECRET"
|
||||
fi
|
||||
|
||||
read -p " Configure Telegram? [y/N]: " SETUP_TELEGRAM
|
||||
if [[ "$SETUP_TELEGRAM" =~ ^[Yy]$ ]]; then
|
||||
read -p " TELEGRAM_BOT_TOKEN: " TELEGRAM_BOT_TOKEN
|
||||
ENV_VARS="$ENV_VARS,TELEGRAM_BOT_TOKEN=$TELEGRAM_BOT_TOKEN"
|
||||
fi
|
||||
|
||||
# Enable required APIs
|
||||
info "Enabling required GCP APIs..."
|
||||
gcloud services enable run.googleapis.com containerregistry.googleapis.com cloudbuild.googleapis.com 2>/dev/null
|
||||
|
||||
# Create Dockerfile if it doesn't exist
|
||||
if [ ! -f "Dockerfile" ]; then
|
||||
info "Creating Dockerfile..."
|
||||
cat > Dockerfile << 'DOCKERFILE'
|
||||
FROM node:20-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install curl for health checks
|
||||
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install ruvbot
|
||||
RUN npm install -g ruvbot
|
||||
|
||||
# Create directories
|
||||
RUN mkdir -p /app/data /app/plugins /app/skills
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:${PORT:-8080}/health || exit 1
|
||||
|
||||
# Start command
|
||||
CMD ["ruvbot", "start", "--port", "8080"]
|
||||
DOCKERFILE
|
||||
success "Dockerfile created"
|
||||
fi
|
||||
|
||||
# Deploy
|
||||
info "Deploying to Cloud Run (this may take a few minutes)..."
|
||||
gcloud run deploy "$SERVICE_NAME" \
|
||||
--source . \
|
||||
--platform managed \
|
||||
--region "$REGION" \
|
||||
--allow-unauthenticated \
|
||||
--port 8080 \
|
||||
--memory 512Mi \
|
||||
--min-instances 0 \
|
||||
--max-instances 10 \
|
||||
--set-env-vars="$ENV_VARS" \
|
||||
--quiet
|
||||
|
||||
# Get URL
|
||||
SERVICE_URL=$(gcloud run services describe "$SERVICE_NAME" --region "$REGION" --format='value(status.url)')
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}═══════════════════════════════════════${NC}"
|
||||
echo -e "${BOLD}🚀 RuvBot deployed successfully!${NC}"
|
||||
echo -e "${GREEN}═══════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
echo -e " URL: ${CYAN}$SERVICE_URL${NC}"
|
||||
echo -e " Health: ${CYAN}$SERVICE_URL/health${NC}"
|
||||
echo -e " API: ${CYAN}$SERVICE_URL/api/status${NC}"
|
||||
echo -e " Models: ${CYAN}$SERVICE_URL/api/models${NC}"
|
||||
echo ""
|
||||
echo " Quick test:"
|
||||
echo -e " ${DIM}curl $SERVICE_URL/health${NC}"
|
||||
echo ""
|
||||
|
||||
# Set Telegram webhook if configured
|
||||
if [ -n "$TELEGRAM_BOT_TOKEN" ]; then
|
||||
WEBHOOK_URL="$SERVICE_URL/telegram/webhook"
|
||||
info "Setting Telegram webhook..."
|
||||
curl -s "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/setWebhook?url=$WEBHOOK_URL" > /dev/null
|
||||
success "Telegram webhook: $WEBHOOK_URL"
|
||||
fi
|
||||
}
|
||||
|
||||
# Deploy to Docker
|
||||
deploy_docker() {
|
||||
step "Deploying with Docker"
|
||||
|
||||
if [ "$DOCKER_AVAILABLE" != "true" ]; then
|
||||
error "Docker is required. Install from https://docker.com"
|
||||
fi
|
||||
|
||||
# Get configuration
|
||||
read -p " Container name [ruvbot]: " CONTAINER_NAME
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-ruvbot}"
|
||||
|
||||
read -p " Port [3000]: " PORT
|
||||
PORT="${PORT:-3000}"
|
||||
|
||||
# Create docker-compose.yml
|
||||
info "Creating docker-compose.yml..."
|
||||
cat > docker-compose.yml << COMPOSE
|
||||
version: '3.8'
|
||||
services:
|
||||
ruvbot:
|
||||
image: node:20-slim
|
||||
container_name: $CONTAINER_NAME
|
||||
working_dir: /app
|
||||
command: sh -c "npm install -g ruvbot && ruvbot start --port 3000"
|
||||
ports:
|
||||
- "$PORT:3000"
|
||||
environment:
|
||||
- OPENROUTER_API_KEY=\${OPENROUTER_API_KEY}
|
||||
- ANTHROPIC_API_KEY=\${ANTHROPIC_API_KEY}
|
||||
- SLACK_BOT_TOKEN=\${SLACK_BOT_TOKEN}
|
||||
- SLACK_SIGNING_SECRET=\${SLACK_SIGNING_SECRET}
|
||||
- SLACK_APP_TOKEN=\${SLACK_APP_TOKEN}
|
||||
- DISCORD_TOKEN=\${DISCORD_TOKEN}
|
||||
- DISCORD_CLIENT_ID=\${DISCORD_CLIENT_ID}
|
||||
- TELEGRAM_BOT_TOKEN=\${TELEGRAM_BOT_TOKEN}
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
- ./plugins:/app/plugins
|
||||
- ./skills:/app/skills
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
restart: unless-stopped
|
||||
COMPOSE
|
||||
success "docker-compose.yml created"
|
||||
|
||||
# Start containers
|
||||
read -p " Start containers now? [Y/n]: " START_NOW
|
||||
START_NOW="${START_NOW:-Y}"
|
||||
|
||||
if [[ "$START_NOW" =~ ^[Yy]$ ]]; then
|
||||
info "Starting Docker containers..."
|
||||
docker-compose up -d
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}═══════════════════════════════════════${NC}"
|
||||
echo -e "${BOLD}🚀 RuvBot is running!${NC}"
|
||||
echo -e "${GREEN}═══════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
echo -e " URL: ${CYAN}http://localhost:$PORT${NC}"
|
||||
echo -e " Health: ${CYAN}http://localhost:$PORT/health${NC}"
|
||||
echo -e " Logs: ${DIM}docker-compose logs -f${NC}"
|
||||
echo -e " Stop: ${DIM}docker-compose down${NC}"
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# Deploy to Kubernetes
|
||||
deploy_k8s() {
|
||||
step "Deploying to Kubernetes"
|
||||
|
||||
if [ "$KUBECTL_AVAILABLE" != "true" ]; then
|
||||
error "kubectl is required. Install from https://kubernetes.io/docs/tasks/tools/"
|
||||
fi
|
||||
|
||||
# Get namespace
|
||||
read -p " Namespace [default]: " NAMESPACE
|
||||
NAMESPACE="${NAMESPACE:-default}"
|
||||
|
||||
# Get API key
|
||||
read -p " OPENROUTER_API_KEY: " API_KEY
|
||||
|
||||
info "Creating Kubernetes manifests..."
|
||||
|
||||
mkdir -p k8s
|
||||
|
||||
# Create secret
|
||||
cat > k8s/secret.yaml << SECRET
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: ruvbot-secrets
|
||||
namespace: $NAMESPACE
|
||||
type: Opaque
|
||||
stringData:
|
||||
OPENROUTER_API_KEY: "$API_KEY"
|
||||
DEFAULT_MODEL: "google/gemini-2.0-flash-001"
|
||||
SECRET
|
||||
|
||||
# Create deployment
|
||||
cat > k8s/deployment.yaml << DEPLOYMENT
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: ruvbot
|
||||
namespace: $NAMESPACE
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: ruvbot
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: ruvbot
|
||||
spec:
|
||||
containers:
|
||||
- name: ruvbot
|
||||
image: node:20-slim
|
||||
command: ["sh", "-c", "npm install -g ruvbot && ruvbot start --port 3000"]
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: ruvbot-secrets
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 3000
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /ready
|
||||
port: 3000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: ruvbot
|
||||
namespace: $NAMESPACE
|
||||
spec:
|
||||
selector:
|
||||
app: ruvbot
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 3000
|
||||
type: LoadBalancer
|
||||
DEPLOYMENT
|
||||
|
||||
success "Kubernetes manifests created in k8s/"
|
||||
|
||||
read -p " Apply manifests now? [Y/n]: " APPLY_NOW
|
||||
APPLY_NOW="${APPLY_NOW:-Y}"
|
||||
|
||||
if [[ "$APPLY_NOW" =~ ^[Yy]$ ]]; then
|
||||
kubectl apply -f k8s/
|
||||
|
||||
echo ""
|
||||
success "Kubernetes resources created"
|
||||
echo ""
|
||||
echo " Check status:"
|
||||
echo -e " ${DIM}kubectl get pods -l app=ruvbot${NC}"
|
||||
echo ""
|
||||
echo " Get service URL:"
|
||||
echo -e " ${DIM}kubectl get svc ruvbot${NC}"
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# Deployment wizard
|
||||
deployment_wizard() {
|
||||
step "Deployment Options"
|
||||
echo ""
|
||||
echo " 1. Local (development)"
|
||||
echo " 2. Docker"
|
||||
echo " 3. Google Cloud Run"
|
||||
echo " 4. Kubernetes"
|
||||
echo " 5. Skip deployment"
|
||||
echo ""
|
||||
read -p " Select [5]: " DEPLOY_CHOICE
|
||||
DEPLOY_CHOICE="${DEPLOY_CHOICE:-5}"
|
||||
|
||||
case "$DEPLOY_CHOICE" in
|
||||
1)
|
||||
info "Starting local development server..."
|
||||
if [ "$RUVBOT_GLOBAL" = "true" ]; then
|
||||
ruvbot start --debug
|
||||
else
|
||||
npx ruvbot start --debug
|
||||
fi
|
||||
;;
|
||||
2) deploy_docker ;;
|
||||
3) deploy_cloudrun ;;
|
||||
4) deploy_k8s ;;
|
||||
5) info "Skipping deployment" ;;
|
||||
*) warn "Invalid option, skipping deployment" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Interactive setup wizard
|
||||
run_wizard() {
|
||||
step "RuvBot Setup Wizard"
|
||||
|
||||
# Ensure .env exists
|
||||
touch .env 2>/dev/null || true
|
||||
|
||||
# LLM Provider
|
||||
echo ""
|
||||
echo " ${BOLD}Step 1: LLM Provider${NC}"
|
||||
echo " ───────────────────"
|
||||
echo " 1. OpenRouter (Gemini 2.5, Claude, GPT - recommended)"
|
||||
echo " 2. Anthropic (Claude only)"
|
||||
echo " 3. Skip (configure later)"
|
||||
read -p " Select [1]: " PROVIDER
|
||||
PROVIDER="${PROVIDER:-1}"
|
||||
|
||||
case "$PROVIDER" in
|
||||
1)
|
||||
read -p " OPENROUTER_API_KEY: " OPENROUTER_KEY
|
||||
{
|
||||
echo "OPENROUTER_API_KEY=$OPENROUTER_KEY"
|
||||
echo "DEFAULT_MODEL=google/gemini-2.0-flash-001"
|
||||
} >> .env
|
||||
success "OpenRouter configured"
|
||||
;;
|
||||
2)
|
||||
read -p " ANTHROPIC_API_KEY: " ANTHROPIC_KEY
|
||||
echo "ANTHROPIC_API_KEY=$ANTHROPIC_KEY" >> .env
|
||||
success "Anthropic configured"
|
||||
;;
|
||||
3) info "Skipping LLM configuration" ;;
|
||||
esac
|
||||
|
||||
# Channel Configuration
|
||||
echo ""
|
||||
echo " ${BOLD}Step 2: Channel Integrations${NC}"
|
||||
echo " ────────────────────────────"
|
||||
echo " 1. Slack"
|
||||
echo " 2. Discord"
|
||||
echo " 3. Telegram"
|
||||
echo " 4. All channels"
|
||||
echo " 5. Skip (configure later)"
|
||||
read -p " Select [5]: " CHANNELS
|
||||
CHANNELS="${CHANNELS:-5}"
|
||||
|
||||
case "$CHANNELS" in
|
||||
1)
|
||||
install_channel_deps "slack"
|
||||
configure_channel "slack"
|
||||
;;
|
||||
2)
|
||||
install_channel_deps "discord"
|
||||
configure_channel "discord"
|
||||
;;
|
||||
3)
|
||||
install_channel_deps "telegram"
|
||||
configure_channel "telegram"
|
||||
;;
|
||||
4)
|
||||
install_channel_deps "all"
|
||||
configure_channel "slack"
|
||||
configure_channel "discord"
|
||||
configure_channel "telegram"
|
||||
;;
|
||||
5) info "Skipping channel configuration" ;;
|
||||
esac
|
||||
|
||||
# Deployment
|
||||
echo ""
|
||||
echo " ${BOLD}Step 3: Deployment${NC}"
|
||||
echo " ──────────────────"
|
||||
deployment_wizard
|
||||
}
|
||||
|
||||
# Print next steps
|
||||
print_next_steps() {
|
||||
echo ""
|
||||
echo -e "${BOLD}📚 Quick Start${NC}"
|
||||
echo "═══════════════════════════════════════"
|
||||
echo ""
|
||||
echo " Configure LLM provider:"
|
||||
echo -e " ${CYAN}export OPENROUTER_API_KEY=sk-or-...${NC}"
|
||||
echo ""
|
||||
echo " Run diagnostics:"
|
||||
echo -e " ${CYAN}ruvbot doctor${NC}"
|
||||
echo ""
|
||||
echo " Start the bot:"
|
||||
echo -e " ${CYAN}ruvbot start${NC}"
|
||||
echo ""
|
||||
echo " Channel setup guides:"
|
||||
echo -e " ${CYAN}ruvbot channels setup slack${NC}"
|
||||
echo -e " ${CYAN}ruvbot channels setup discord${NC}"
|
||||
echo -e " ${CYAN}ruvbot channels setup telegram${NC}"
|
||||
echo ""
|
||||
echo " Deploy templates:"
|
||||
echo -e " ${CYAN}ruvbot templates list${NC}"
|
||||
echo -e " ${CYAN}ruvbot deploy code-reviewer${NC}"
|
||||
echo ""
|
||||
echo " Deploy to Cloud Run:"
|
||||
echo -e " ${CYAN}ruvbot deploy cloudrun${NC}"
|
||||
echo ""
|
||||
echo -e "${DIM}Docs: https://github.com/ruvnet/ruvector/tree/main/npm/packages/ruvbot${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Main
|
||||
main() {
|
||||
echo "Checking requirements..."
|
||||
echo ""
|
||||
print_banner
|
||||
check_dependencies
|
||||
install_ruvbot
|
||||
|
||||
check_node
|
||||
check_npm
|
||||
# Handle channel installation
|
||||
if [ -n "$RUVBOT_CHANNEL" ]; then
|
||||
install_channel_deps "$RUVBOT_CHANNEL"
|
||||
fi
|
||||
|
||||
install_ruvbot
|
||||
show_next_steps
|
||||
# Handle initialization
|
||||
if [ "$RUVBOT_INIT" = "true" ]; then
|
||||
init_project
|
||||
fi
|
||||
|
||||
# Handle wizard
|
||||
if [ "$RUVBOT_WIZARD" = "true" ]; then
|
||||
run_wizard
|
||||
elif [ -n "$RUVBOT_DEPLOY" ]; then
|
||||
# Handle deployment without wizard
|
||||
case "$RUVBOT_DEPLOY" in
|
||||
cloudrun|cloud-run|gcp) deploy_cloudrun ;;
|
||||
docker) deploy_docker ;;
|
||||
k8s|kubernetes) deploy_k8s ;;
|
||||
*) warn "Unknown deployment target: $RUVBOT_DEPLOY" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
print_next_steps
|
||||
}
|
||||
|
||||
main
|
||||
main "$@"
|
||||
|
|
|
|||
861
npm/packages/ruvbot/src/api/public/index.html
Normal file
861
npm/packages/ruvbot/src/api/public/index.html
Normal file
|
|
@ -0,0 +1,861 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>RuvBot - AI Assistant</title>
|
||||
<meta name="description" content="Enterprise-grade self-learning AI assistant with military-strength security">
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🤖</text></svg>">
|
||||
<style>
|
||||
:root {
|
||||
--bg-primary: #0a0a0f;
|
||||
--bg-secondary: #12121a;
|
||||
--bg-tertiary: #1a1a25;
|
||||
--bg-hover: #22222f;
|
||||
--text-primary: #f0f0f5;
|
||||
--text-secondary: #a0a0b0;
|
||||
--text-muted: #606070;
|
||||
--accent: #6366f1;
|
||||
--accent-hover: #818cf8;
|
||||
--accent-subtle: rgba(99, 102, 241, 0.1);
|
||||
--border: #2a2a35;
|
||||
--success: #22c55e;
|
||||
--error: #ef4444;
|
||||
--warning: #f59e0b;
|
||||
--radius: 12px;
|
||||
--shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f8f9fa;
|
||||
--bg-tertiary: #f0f1f3;
|
||||
--bg-hover: #e8e9eb;
|
||||
--text-primary: #1a1a2e;
|
||||
--text-secondary: #4a4a5a;
|
||||
--text-muted: #8a8a9a;
|
||||
--border: #e0e0e5;
|
||||
--shadow: 0 4px 24px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 24px;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: linear-gradient(135deg, var(--accent), #8b5cf6);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--success);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* Main Chat Container */
|
||||
.chat-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
/* Messages */
|
||||
.messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.message.user {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.message-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.message.assistant .message-avatar {
|
||||
background: linear-gradient(135deg, var(--accent), #8b5cf6);
|
||||
}
|
||||
|
||||
.message.user .message-avatar {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.message-content {
|
||||
max-width: 75%;
|
||||
padding: 14px 18px;
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.message.assistant .message-content {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.message.user .message-content {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.message-content p {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.message-content p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.message-content pre {
|
||||
background: var(--bg-primary);
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
overflow-x: auto;
|
||||
margin: 12px 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.message-content code {
|
||||
font-family: 'SF Mono', Consolas, monospace;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
.message-content code:not(pre code) {
|
||||
background: var(--bg-tertiary);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.message.user .message-content code:not(pre code) {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.message.user .message-time {
|
||||
text-align: right;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
/* Typing indicator */
|
||||
.typing-indicator {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.typing-indicator span {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--text-muted);
|
||||
border-radius: 50%;
|
||||
animation: typing 1.4s infinite;
|
||||
}
|
||||
|
||||
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
|
||||
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
|
||||
|
||||
@keyframes typing {
|
||||
0%, 60%, 100% { transform: translateY(0); }
|
||||
30% { transform: translateY(-8px); }
|
||||
}
|
||||
|
||||
/* Input Area */
|
||||
.input-container {
|
||||
padding: 16px 0 24px;
|
||||
background: var(--bg-primary);
|
||||
border-top: 1px solid var(--border);
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-end;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 8px;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.input-wrapper:focus-within {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px var(--accent-subtle);
|
||||
}
|
||||
|
||||
.input-wrapper textarea {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.9375rem;
|
||||
padding: 8px 12px;
|
||||
resize: none;
|
||||
min-height: 24px;
|
||||
max-height: 200px;
|
||||
line-height: 1.5;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.input-wrapper textarea:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.input-wrapper textarea::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 12px;
|
||||
background: var(--accent);
|
||||
border: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.send-btn:hover:not(:disabled) {
|
||||
background: var(--accent-hover);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.send-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.send-btn svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* Welcome Screen */
|
||||
.welcome {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.welcome-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: linear-gradient(135deg, var(--accent), #8b5cf6);
|
||||
border-radius: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.welcome h1 {
|
||||
font-size: 1.75rem;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.welcome p {
|
||||
color: var(--text-secondary);
|
||||
max-width: 400px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.suggestions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.suggestion {
|
||||
padding: 10px 16px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 20px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.suggestion:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Model Selector */
|
||||
.model-selector {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.model-selector select {
|
||||
appearance: none;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 8px 32px 8px 12px;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.model-selector::after {
|
||||
content: '▼';
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 0.625rem;
|
||||
color: var(--text-muted);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Theme Toggle */
|
||||
.theme-toggle {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 640px) {
|
||||
.header {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
max-width: 85%;
|
||||
}
|
||||
|
||||
.suggestions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Markdown */
|
||||
.message-content ul, .message-content ol {
|
||||
margin: 12px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.message-content li {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.message-content blockquote {
|
||||
border-left: 3px solid var(--accent);
|
||||
padding-left: 16px;
|
||||
margin: 12px 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.message-content a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.message-content a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.message-content h1, .message-content h2, .message-content h3 {
|
||||
margin: 16px 0 8px;
|
||||
}
|
||||
|
||||
.message-content hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border);
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
/* Error state */
|
||||
.error-message {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid var(--error);
|
||||
color: var(--error);
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="logo">
|
||||
<div class="logo-icon">🤖</div>
|
||||
<span>RuvBot</span>
|
||||
<div class="status-dot" title="Online"></div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<div class="model-selector">
|
||||
<select id="modelSelect">
|
||||
<option value="google/gemini-2.0-flash-001">Gemini 2.0 Flash</option>
|
||||
<option value="google/gemini-2.5-pro-preview">Gemini 2.5 Pro</option>
|
||||
<option value="anthropic/claude-3.5-sonnet">Claude 3.5 Sonnet</option>
|
||||
<option value="openai/gpt-4o">GPT-4o</option>
|
||||
<option value="deepseek/deepseek-r1">DeepSeek R1</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-ghost" id="newChatBtn">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 5v14M5 12h14"/>
|
||||
</svg>
|
||||
New Chat
|
||||
</button>
|
||||
<button class="theme-toggle" id="themeToggle" title="Toggle theme">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="5"/>
|
||||
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="chat-container">
|
||||
<div class="messages" id="messages">
|
||||
<div class="welcome" id="welcome">
|
||||
<div class="welcome-icon">🤖</div>
|
||||
<h1>Welcome to RuvBot</h1>
|
||||
<p>Enterprise-grade AI assistant with military-strength security, 150x faster vector search, and 12+ LLM models.</p>
|
||||
<div class="suggestions">
|
||||
<button class="suggestion" data-prompt="What can you help me with?">What can you help me with?</button>
|
||||
<button class="suggestion" data-prompt="Explain how RuvBot's security works">Explain security features</button>
|
||||
<button class="suggestion" data-prompt="Help me write a Python function">Help me code</button>
|
||||
<button class="suggestion" data-prompt="What LLM models are available?">Available models</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-container">
|
||||
<div class="input-wrapper">
|
||||
<textarea
|
||||
id="messageInput"
|
||||
placeholder="Message RuvBot..."
|
||||
rows="1"
|
||||
autofocus
|
||||
></textarea>
|
||||
<button class="send-btn" id="sendBtn" disabled>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
Powered by <a href="https://github.com/ruvnet/ruvector" target="_blank">RuvBot</a> •
|
||||
<a href="/api/models" target="_blank">API</a> •
|
||||
<a href="/health" target="_blank">Health</a>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// State
|
||||
let sessionId = null;
|
||||
let isLoading = false;
|
||||
|
||||
// Elements
|
||||
const messagesEl = document.getElementById('messages');
|
||||
const welcomeEl = document.getElementById('welcome');
|
||||
const inputEl = document.getElementById('messageInput');
|
||||
const sendBtn = document.getElementById('sendBtn');
|
||||
const modelSelect = document.getElementById('modelSelect');
|
||||
const newChatBtn = document.getElementById('newChatBtn');
|
||||
const themeToggle = document.getElementById('themeToggle');
|
||||
|
||||
// Theme
|
||||
const savedTheme = localStorage.getItem('theme') || 'dark';
|
||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||
|
||||
themeToggle.addEventListener('click', () => {
|
||||
const current = document.documentElement.getAttribute('data-theme');
|
||||
const next = current === 'dark' ? 'light' : 'dark';
|
||||
document.documentElement.setAttribute('data-theme', next);
|
||||
localStorage.setItem('theme', next);
|
||||
});
|
||||
|
||||
// Auto-resize textarea
|
||||
inputEl.addEventListener('input', () => {
|
||||
inputEl.style.height = 'auto';
|
||||
inputEl.style.height = Math.min(inputEl.scrollHeight, 200) + 'px';
|
||||
sendBtn.disabled = !inputEl.value.trim() || isLoading;
|
||||
});
|
||||
|
||||
// Send on Enter (Shift+Enter for newline)
|
||||
inputEl.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
if (!sendBtn.disabled) sendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
sendBtn.addEventListener('click', sendMessage);
|
||||
|
||||
// Suggestions
|
||||
document.querySelectorAll('.suggestion').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
inputEl.value = btn.dataset.prompt;
|
||||
inputEl.dispatchEvent(new Event('input'));
|
||||
sendMessage();
|
||||
});
|
||||
});
|
||||
|
||||
// New chat
|
||||
newChatBtn.addEventListener('click', () => {
|
||||
sessionId = null;
|
||||
messagesEl.innerHTML = '';
|
||||
messagesEl.appendChild(welcomeEl);
|
||||
welcomeEl.style.display = 'flex';
|
||||
inputEl.value = '';
|
||||
inputEl.focus();
|
||||
});
|
||||
|
||||
// Create session
|
||||
async function createSession() {
|
||||
try {
|
||||
const res = await fetch('/api/sessions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ agentId: 'default-agent' })
|
||||
});
|
||||
const data = await res.json();
|
||||
sessionId = data.id || data.sessionId;
|
||||
return sessionId;
|
||||
} catch (err) {
|
||||
console.error('Failed to create session:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// Send message
|
||||
async function sendMessage() {
|
||||
const message = inputEl.value.trim();
|
||||
if (!message || isLoading) return;
|
||||
|
||||
isLoading = true;
|
||||
sendBtn.disabled = true;
|
||||
inputEl.value = '';
|
||||
inputEl.style.height = 'auto';
|
||||
|
||||
// Hide welcome
|
||||
welcomeEl.style.display = 'none';
|
||||
|
||||
// Add user message
|
||||
addMessage('user', message);
|
||||
|
||||
// Show typing indicator
|
||||
const typingEl = addTypingIndicator();
|
||||
|
||||
try {
|
||||
// Create session if needed
|
||||
if (!sessionId) {
|
||||
await createSession();
|
||||
}
|
||||
|
||||
// Send chat request
|
||||
const res = await fetch(`/api/sessions/${sessionId}/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
message,
|
||||
model: modelSelect.value
|
||||
})
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json();
|
||||
throw new Error(error.message || error.error || 'Request failed');
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
// Remove typing indicator
|
||||
typingEl.remove();
|
||||
|
||||
// Add assistant message
|
||||
const content = data.content || data.message || data.response || 'No response';
|
||||
addMessage('assistant', content);
|
||||
|
||||
} catch (err) {
|
||||
typingEl.remove();
|
||||
addMessage('assistant', `<div class="error-message">Error: ${err.message}</div>`, true);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
sendBtn.disabled = !inputEl.value.trim();
|
||||
inputEl.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Add message to chat
|
||||
function addMessage(role, content, isHtml = false) {
|
||||
const messageEl = document.createElement('div');
|
||||
messageEl.className = `message ${role}`;
|
||||
|
||||
const avatar = role === 'user' ? '👤' : '🤖';
|
||||
const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
|
||||
messageEl.innerHTML = `
|
||||
<div class="message-avatar">${avatar}</div>
|
||||
<div class="message-content">
|
||||
${isHtml ? content : formatMarkdown(content)}
|
||||
<div class="message-time">${time}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
messagesEl.appendChild(messageEl);
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
return messageEl;
|
||||
}
|
||||
|
||||
// Add typing indicator
|
||||
function addTypingIndicator() {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'message assistant';
|
||||
el.innerHTML = `
|
||||
<div class="message-avatar">🤖</div>
|
||||
<div class="message-content">
|
||||
<div class="typing-indicator">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
messagesEl.appendChild(el);
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
return el;
|
||||
}
|
||||
|
||||
// Simple markdown formatter
|
||||
function formatMarkdown(text) {
|
||||
if (!text) return '';
|
||||
|
||||
return text
|
||||
// Code blocks
|
||||
.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code class="language-$1">$2</code></pre>')
|
||||
// Inline code
|
||||
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
||||
// Bold
|
||||
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
||||
// Italic
|
||||
.replace(/\*([^*]+)\*/g, '<em>$1</em>')
|
||||
// Links
|
||||
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>')
|
||||
// Headers
|
||||
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
||||
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
||||
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
||||
// Lists
|
||||
.replace(/^\* (.+)$/gm, '<li>$1</li>')
|
||||
.replace(/^- (.+)$/gm, '<li>$1</li>')
|
||||
.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>')
|
||||
// Blockquotes
|
||||
.replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>')
|
||||
// Horizontal rule
|
||||
.replace(/^---$/gm, '<hr>')
|
||||
// Paragraphs
|
||||
.replace(/\n\n/g, '</p><p>')
|
||||
.replace(/^(.+)$/gm, (match) => {
|
||||
if (match.startsWith('<')) return match;
|
||||
return `<p>${match}</p>`;
|
||||
})
|
||||
// Clean up
|
||||
.replace(/<p><\/p>/g, '')
|
||||
.replace(/<p>(<[hul])/g, '$1')
|
||||
.replace(/(<\/[hul].*>)<\/p>/g, '$1');
|
||||
}
|
||||
|
||||
// Check API health
|
||||
async function checkHealth() {
|
||||
try {
|
||||
const res = await fetch('/health');
|
||||
const data = await res.json();
|
||||
console.log('RuvBot health:', data);
|
||||
} catch (err) {
|
||||
console.warn('Health check failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Init
|
||||
checkHealth();
|
||||
inputEl.focus();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
411
npm/packages/ruvbot/src/cli/commands/channels.ts
Normal file
411
npm/packages/ruvbot/src/cli/commands/channels.ts
Normal file
|
|
@ -0,0 +1,411 @@
|
|||
/**
|
||||
* RuvBot CLI - Channels Command
|
||||
*
|
||||
* Setup and manage channel integrations (Slack, Discord, Telegram, Webhooks).
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export function createChannelsCommand(): Command {
|
||||
const channels = new Command('channels')
|
||||
.alias('ch')
|
||||
.description('Manage channel integrations');
|
||||
|
||||
// List channels
|
||||
channels
|
||||
.command('list')
|
||||
.alias('ls')
|
||||
.description('List available channel integrations')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action((options) => {
|
||||
const channelList = [
|
||||
{
|
||||
name: 'slack',
|
||||
description: 'Slack workspace integration via Bolt SDK',
|
||||
package: '@slack/bolt',
|
||||
status: 'available',
|
||||
},
|
||||
{
|
||||
name: 'discord',
|
||||
description: 'Discord server integration via discord.js',
|
||||
package: 'discord.js',
|
||||
status: 'available',
|
||||
},
|
||||
{
|
||||
name: 'telegram',
|
||||
description: 'Telegram bot integration via Telegraf',
|
||||
package: 'telegraf',
|
||||
status: 'available',
|
||||
},
|
||||
{
|
||||
name: 'webhook',
|
||||
description: 'Generic webhook endpoint for custom integrations',
|
||||
package: 'built-in',
|
||||
status: 'available',
|
||||
},
|
||||
];
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(channelList, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.bold('\n📡 Available Channel Integrations\n'));
|
||||
console.log('─'.repeat(60));
|
||||
|
||||
for (const ch of channelList) {
|
||||
const icon = getChannelIcon(ch.name);
|
||||
console.log(`${icon} ${chalk.cyan(ch.name.padEnd(12))} ${ch.description}`);
|
||||
console.log(` Package: ${chalk.gray(ch.package)}`);
|
||||
console.log();
|
||||
}
|
||||
|
||||
console.log('─'.repeat(60));
|
||||
console.log(chalk.gray('\nRun `ruvbot channels setup <channel>` for setup instructions'));
|
||||
});
|
||||
|
||||
// Setup channel
|
||||
channels
|
||||
.command('setup <channel>')
|
||||
.description('Show setup instructions for a channel')
|
||||
.action((channel) => {
|
||||
const normalizedChannel = channel.toLowerCase();
|
||||
|
||||
switch (normalizedChannel) {
|
||||
case 'slack':
|
||||
printSlackSetup();
|
||||
break;
|
||||
case 'discord':
|
||||
printDiscordSetup();
|
||||
break;
|
||||
case 'telegram':
|
||||
printTelegramSetup();
|
||||
break;
|
||||
case 'webhook':
|
||||
case 'webhooks':
|
||||
printWebhookSetup();
|
||||
break;
|
||||
default:
|
||||
console.error(chalk.red(`Unknown channel: ${channel}`));
|
||||
console.log('\nAvailable channels: slack, discord, telegram, webhook');
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Test channel connection
|
||||
channels
|
||||
.command('test <channel>')
|
||||
.description('Test channel connection')
|
||||
.action(async (channel) => {
|
||||
const normalizedChannel = channel.toLowerCase();
|
||||
console.log(chalk.cyan(`\nTesting ${normalizedChannel} connection...`));
|
||||
|
||||
const envVars = getRequiredEnvVars(normalizedChannel);
|
||||
const missing = envVars.filter((v) => !process.env[v]);
|
||||
|
||||
if (missing.length > 0) {
|
||||
console.log(chalk.red('\n✗ Missing environment variables:'));
|
||||
missing.forEach((v) => console.log(chalk.red(` - ${v}`)));
|
||||
console.log(chalk.gray(`\nRun 'ruvbot channels setup ${normalizedChannel}' for instructions`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(chalk.green('✓ All required environment variables are set'));
|
||||
console.log(chalk.gray('\nStart the bot with:'));
|
||||
console.log(chalk.cyan(` ruvbot start --channel ${normalizedChannel}`));
|
||||
});
|
||||
|
||||
return channels;
|
||||
}
|
||||
|
||||
function getChannelIcon(channel: string): string {
|
||||
const icons: Record<string, string> = {
|
||||
slack: '💬',
|
||||
discord: '🎮',
|
||||
telegram: '✈️',
|
||||
webhook: '🔗',
|
||||
};
|
||||
return icons[channel] || '📡';
|
||||
}
|
||||
|
||||
function getRequiredEnvVars(channel: string): string[] {
|
||||
switch (channel) {
|
||||
case 'slack':
|
||||
return ['SLACK_BOT_TOKEN', 'SLACK_SIGNING_SECRET', 'SLACK_APP_TOKEN'];
|
||||
case 'discord':
|
||||
return ['DISCORD_TOKEN', 'DISCORD_CLIENT_ID'];
|
||||
case 'telegram':
|
||||
return ['TELEGRAM_BOT_TOKEN'];
|
||||
case 'webhook':
|
||||
return [];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function printSlackSetup(): void {
|
||||
console.log(chalk.bold('\n💬 Slack Integration Setup\n'));
|
||||
console.log('═'.repeat(60));
|
||||
|
||||
console.log(chalk.bold('\n📋 Step 1: Create a Slack App\n'));
|
||||
console.log(' 1. Go to: ' + chalk.cyan('https://api.slack.com/apps'));
|
||||
console.log(' 2. Click "Create New App" → "From Scratch"');
|
||||
console.log(' 3. Name your app (e.g., "RuvBot") and select workspace');
|
||||
|
||||
console.log(chalk.bold('\n🔐 Step 2: Configure Bot Permissions\n'));
|
||||
console.log(' Navigate to OAuth & Permissions and add these Bot Token Scopes:');
|
||||
console.log(chalk.gray(' ─────────────────────────────────────'));
|
||||
console.log(' • app_mentions:read - Receive @mentions');
|
||||
console.log(' • chat:write - Send messages');
|
||||
console.log(' • channels:history - Read channel messages');
|
||||
console.log(' • im:history - Read direct messages');
|
||||
console.log(' • reactions:write - Add reactions');
|
||||
console.log(' • files:read - Access shared files');
|
||||
|
||||
console.log(chalk.bold('\n⚡ Step 3: Enable Socket Mode\n'));
|
||||
console.log(' 1. Go to Socket Mode → Enable');
|
||||
console.log(' 2. Create App-Level Token with ' + chalk.cyan('connections:write') + ' scope');
|
||||
console.log(' 3. Save the ' + chalk.yellow('xapp-...') + ' token');
|
||||
|
||||
console.log(chalk.bold('\n📦 Step 4: Install & Get Tokens\n'));
|
||||
console.log(' 1. Go to Install App → Install to Workspace');
|
||||
console.log(' 2. Copy Bot User OAuth Token: ' + chalk.yellow('xoxb-...'));
|
||||
console.log(' 3. Copy Signing Secret from Basic Information');
|
||||
|
||||
console.log(chalk.bold('\n🔧 Step 5: Configure Environment\n'));
|
||||
console.log(chalk.gray(' ─────────────────────────────────────'));
|
||||
console.log(chalk.cyan(' export SLACK_BOT_TOKEN="xoxb-your-bot-token"'));
|
||||
console.log(chalk.cyan(' export SLACK_SIGNING_SECRET="your-signing-secret"'));
|
||||
console.log(chalk.cyan(' export SLACK_APP_TOKEN="xapp-your-app-token"'));
|
||||
|
||||
console.log(chalk.bold('\n🚀 Step 6: Start RuvBot\n'));
|
||||
console.log(chalk.cyan(' ruvbot start --channel slack'));
|
||||
|
||||
console.log(chalk.bold('\n🌐 Webhook Mode (for Cloud Run)\n'));
|
||||
console.log(' For serverless deployments, use webhook instead of Socket Mode:');
|
||||
console.log(' 1. Disable Socket Mode');
|
||||
console.log(' 2. Go to Event Subscriptions → Enable');
|
||||
console.log(' 3. Set Request URL: ' + chalk.cyan('https://your-ruvbot.run.app/slack/events'));
|
||||
console.log(' 4. Subscribe to: message.channels, message.im, app_mention');
|
||||
|
||||
console.log('\n' + '═'.repeat(60));
|
||||
console.log(chalk.gray('Install optional dependency: npm install @slack/bolt @slack/web-api\n'));
|
||||
}
|
||||
|
||||
function printDiscordSetup(): void {
|
||||
console.log(chalk.bold('\n🎮 Discord Integration Setup\n'));
|
||||
console.log('═'.repeat(60));
|
||||
|
||||
console.log(chalk.bold('\n📋 Step 1: Create a Discord Application\n'));
|
||||
console.log(' 1. Go to: ' + chalk.cyan('https://discord.com/developers/applications'));
|
||||
console.log(' 2. Click "New Application" and name it');
|
||||
|
||||
console.log(chalk.bold('\n🤖 Step 2: Create a Bot\n'));
|
||||
console.log(' 1. Go to Bot section → Add Bot');
|
||||
console.log(' 2. Enable Privileged Gateway Intents:');
|
||||
console.log(chalk.green(' ✓ MESSAGE CONTENT INTENT'));
|
||||
console.log(chalk.green(' ✓ SERVER MEMBERS INTENT'));
|
||||
console.log(' 3. Click "Reset Token" and copy the bot token');
|
||||
|
||||
console.log(chalk.bold('\n🆔 Step 3: Get Application IDs\n'));
|
||||
console.log(' 1. Copy Application ID from General Information');
|
||||
console.log(' 2. Right-click your server → Copy Server ID (for testing)');
|
||||
|
||||
console.log(chalk.bold('\n📨 Step 4: Invite Bot to Server\n'));
|
||||
console.log(' 1. Go to OAuth2 → URL Generator');
|
||||
console.log(' 2. Select scopes: ' + chalk.cyan('bot, applications.commands'));
|
||||
console.log(' 3. Select permissions:');
|
||||
console.log(' • Send Messages');
|
||||
console.log(' • Read Message History');
|
||||
console.log(' • Add Reactions');
|
||||
console.log(' • Use Slash Commands');
|
||||
console.log(' 4. Open the generated URL to invite the bot');
|
||||
|
||||
console.log(chalk.bold('\n🔧 Step 5: Configure Environment\n'));
|
||||
console.log(chalk.gray(' ─────────────────────────────────────'));
|
||||
console.log(chalk.cyan(' export DISCORD_TOKEN="your-bot-token"'));
|
||||
console.log(chalk.cyan(' export DISCORD_CLIENT_ID="your-application-id"'));
|
||||
console.log(chalk.cyan(' export DISCORD_GUILD_ID="your-server-id" # Optional'));
|
||||
|
||||
console.log(chalk.bold('\n🚀 Step 6: Start RuvBot\n'));
|
||||
console.log(chalk.cyan(' ruvbot start --channel discord'));
|
||||
|
||||
console.log('\n' + '═'.repeat(60));
|
||||
console.log(chalk.gray('Install optional dependency: npm install discord.js\n'));
|
||||
}
|
||||
|
||||
function printTelegramSetup(): void {
|
||||
console.log(chalk.bold('\n✈️ Telegram Integration Setup\n'));
|
||||
console.log('═'.repeat(60));
|
||||
|
||||
console.log(chalk.bold('\n📋 Step 1: Create a Bot with BotFather\n'));
|
||||
console.log(' 1. Open Telegram and search for ' + chalk.cyan('@BotFather'));
|
||||
console.log(' 2. Send ' + chalk.cyan('/newbot') + ' command');
|
||||
console.log(' 3. Follow prompts to name your bot');
|
||||
console.log(' 4. Copy the HTTP API token (format: ' + chalk.yellow('123456789:ABC-DEF...') + ')');
|
||||
|
||||
console.log(chalk.bold('\n🔧 Step 2: Configure Environment\n'));
|
||||
console.log(chalk.gray(' ─────────────────────────────────────'));
|
||||
console.log(chalk.cyan(' export TELEGRAM_BOT_TOKEN="your-bot-token"'));
|
||||
|
||||
console.log(chalk.bold('\n🚀 Step 3: Start RuvBot (Polling Mode)\n'));
|
||||
console.log(chalk.cyan(' ruvbot start --channel telegram'));
|
||||
|
||||
console.log(chalk.bold('\n🌐 Webhook Mode (for Production/Cloud Run)\n'));
|
||||
console.log(' For serverless deployments, use webhook mode:');
|
||||
console.log(chalk.gray(' ─────────────────────────────────────'));
|
||||
console.log(chalk.cyan(' export TELEGRAM_BOT_TOKEN="your-bot-token"'));
|
||||
console.log(chalk.cyan(' export TELEGRAM_WEBHOOK_URL="https://your-ruvbot.run.app/telegram/webhook"'));
|
||||
|
||||
console.log(chalk.bold('\n📱 Step 4: Test Your Bot\n'));
|
||||
console.log(' 1. Search for your bot by username in Telegram');
|
||||
console.log(' 2. Start a chat and send ' + chalk.cyan('/start'));
|
||||
console.log(' 3. Send messages to interact with RuvBot');
|
||||
|
||||
console.log(chalk.bold('\n⚙️ Optional: Set Bot Commands\n'));
|
||||
console.log(' Send to @BotFather:');
|
||||
console.log(chalk.cyan(' /setcommands'));
|
||||
console.log(' Then paste:');
|
||||
console.log(chalk.gray(' start - Start the bot'));
|
||||
console.log(chalk.gray(' help - Show help message'));
|
||||
console.log(chalk.gray(' status - Check bot status'));
|
||||
|
||||
console.log('\n' + '═'.repeat(60));
|
||||
console.log(chalk.gray('Install optional dependency: npm install telegraf\n'));
|
||||
}
|
||||
|
||||
function printWebhookSetup(): void {
|
||||
console.log(chalk.bold('\n🔗 Webhook Integration Setup\n'));
|
||||
console.log('═'.repeat(60));
|
||||
|
||||
console.log(chalk.bold('\n📋 Overview\n'));
|
||||
console.log(' RuvBot provides webhook endpoints for custom integrations.');
|
||||
console.log(' Use webhooks to connect with any messaging platform or service.');
|
||||
|
||||
console.log(chalk.bold('\n🔌 Available Webhook Endpoints\n'));
|
||||
console.log(chalk.gray(' ─────────────────────────────────────'));
|
||||
console.log(` POST ${chalk.cyan('/webhook/message')} - Receive messages`);
|
||||
console.log(` POST ${chalk.cyan('/webhook/event')} - Receive events`);
|
||||
console.log(` GET ${chalk.cyan('/webhook/health')} - Health check`);
|
||||
console.log(` POST ${chalk.cyan('/api/sessions/:id/chat')} - Chat endpoint`);
|
||||
|
||||
console.log(chalk.bold('\n📤 Outbound Webhooks\n'));
|
||||
console.log(' Configure RuvBot to send responses to your endpoint:');
|
||||
console.log(chalk.gray(' ─────────────────────────────────────'));
|
||||
console.log(chalk.cyan(' export WEBHOOK_URL="https://your-service.com/callback"'));
|
||||
console.log(chalk.cyan(' export WEBHOOK_SECRET="your-shared-secret"'));
|
||||
|
||||
console.log(chalk.bold('\n📥 Inbound Webhook Format\n'));
|
||||
console.log(' Send POST requests with JSON body:');
|
||||
console.log(chalk.gray(' ─────────────────────────────────────'));
|
||||
console.log(chalk.cyan(` curl -X POST https://your-ruvbot.run.app/webhook/message \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-H "X-Webhook-Secret: your-secret" \\
|
||||
-d '{
|
||||
"message": "Hello RuvBot!",
|
||||
"userId": "user-123",
|
||||
"channelId": "channel-456",
|
||||
"metadata": {}
|
||||
}'`));
|
||||
|
||||
console.log(chalk.bold('\n🔐 Security\n'));
|
||||
console.log(' 1. Always use HTTPS in production');
|
||||
console.log(' 2. Set a webhook secret for signature verification');
|
||||
console.log(' 3. Validate the X-Webhook-Signature header');
|
||||
console.log(' 4. Enable IP allowlisting if possible');
|
||||
|
||||
console.log(chalk.bold('\n📋 Configuration File\n'));
|
||||
console.log(chalk.gray(' ─────────────────────────────────────'));
|
||||
console.log(chalk.cyan(` {
|
||||
"channels": {
|
||||
"webhook": {
|
||||
"enabled": true,
|
||||
"inbound": {
|
||||
"path": "/webhook/message",
|
||||
"secret": "\${WEBHOOK_SECRET}"
|
||||
},
|
||||
"outbound": {
|
||||
"url": "\${WEBHOOK_URL}",
|
||||
"retries": 3,
|
||||
"timeout": 30000
|
||||
}
|
||||
}
|
||||
}
|
||||
}`));
|
||||
|
||||
console.log(chalk.bold('\n🚀 Start with Webhook Support\n'));
|
||||
console.log(chalk.cyan(' ruvbot start --port 3000'));
|
||||
console.log(chalk.gray(' # Webhooks are always available on the API server'));
|
||||
|
||||
console.log('\n' + '═'.repeat(60) + '\n');
|
||||
}
|
||||
|
||||
export function createWebhooksCommand(): Command {
|
||||
const webhooks = new Command('webhooks')
|
||||
.alias('wh')
|
||||
.description('Configure webhook integrations');
|
||||
|
||||
// List webhooks
|
||||
webhooks
|
||||
.command('list')
|
||||
.description('List configured webhooks')
|
||||
.action(() => {
|
||||
console.log(chalk.bold('\n🔗 Configured Webhooks\n'));
|
||||
console.log('─'.repeat(50));
|
||||
|
||||
const outboundUrl = process.env.WEBHOOK_URL;
|
||||
if (outboundUrl) {
|
||||
console.log(chalk.green('✓ Outbound webhook:'), outboundUrl);
|
||||
} else {
|
||||
console.log(chalk.gray('○ No outbound webhook configured'));
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log('Inbound endpoints (always available):');
|
||||
console.log(` POST ${chalk.cyan('/webhook/message')}`);
|
||||
console.log(` POST ${chalk.cyan('/webhook/event')}`);
|
||||
console.log(` POST ${chalk.cyan('/api/sessions/:id/chat')}`);
|
||||
console.log();
|
||||
});
|
||||
|
||||
// Test webhook
|
||||
webhooks
|
||||
.command('test <url>')
|
||||
.description('Test a webhook endpoint')
|
||||
.option('--payload <json>', 'Custom JSON payload')
|
||||
.action(async (url, options) => {
|
||||
console.log(chalk.cyan(`\nTesting webhook: ${url}\n`));
|
||||
|
||||
try {
|
||||
const payload = options.payload
|
||||
? JSON.parse(options.payload)
|
||||
: { test: true, timestamp: new Date().toISOString() };
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log(chalk.green('✓ Webhook responded successfully'));
|
||||
console.log(` Status: ${response.status}`);
|
||||
const body = await response.text();
|
||||
if (body) {
|
||||
console.log(` Response: ${body.substring(0, 200)}`);
|
||||
}
|
||||
} else {
|
||||
console.log(chalk.red('✗ Webhook failed'));
|
||||
console.log(` Status: ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(chalk.red('✗ Failed to reach webhook'));
|
||||
console.log(` Error: ${error instanceof Error ? error.message : 'Unknown'}`);
|
||||
}
|
||||
});
|
||||
|
||||
return webhooks;
|
||||
}
|
||||
|
||||
export default createChannelsCommand;
|
||||
488
npm/packages/ruvbot/src/cli/commands/deploy.ts
Normal file
488
npm/packages/ruvbot/src/cli/commands/deploy.ts
Normal file
|
|
@ -0,0 +1,488 @@
|
|||
/**
|
||||
* RuvBot CLI - Deploy Command
|
||||
*
|
||||
* Deploy RuvBot to various cloud platforms with interactive wizards.
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { execSync, spawn } from 'child_process';
|
||||
|
||||
export function createDeploymentCommand(): Command {
|
||||
const deploy = new Command('deploy-cloud')
|
||||
.alias('cloud')
|
||||
.description('Deploy RuvBot to cloud platforms');
|
||||
|
||||
// Cloud Run deployment
|
||||
deploy
|
||||
.command('cloudrun')
|
||||
.alias('gcp')
|
||||
.description('Deploy to Google Cloud Run')
|
||||
.option('--project <project>', 'GCP project ID')
|
||||
.option('--region <region>', 'Cloud Run region', 'us-central1')
|
||||
.option('--service <name>', 'Service name', 'ruvbot')
|
||||
.option('--memory <size>', 'Memory allocation', '512Mi')
|
||||
.option('--min-instances <n>', 'Minimum instances', '0')
|
||||
.option('--max-instances <n>', 'Maximum instances', '10')
|
||||
.option('--env-file <path>', 'Path to .env file')
|
||||
.option('--yes', 'Skip confirmation prompts')
|
||||
.action(async (options) => {
|
||||
await deployToCloudRun(options);
|
||||
});
|
||||
|
||||
// Docker deployment
|
||||
deploy
|
||||
.command('docker')
|
||||
.description('Deploy with Docker/Docker Compose')
|
||||
.option('--name <name>', 'Container name', 'ruvbot')
|
||||
.option('--port <port>', 'Host port', '3000')
|
||||
.option('--detach', 'Run in background', true)
|
||||
.option('--env-file <path>', 'Path to .env file')
|
||||
.action(async (options) => {
|
||||
await deployToDocker(options);
|
||||
});
|
||||
|
||||
// Kubernetes deployment
|
||||
deploy
|
||||
.command('k8s')
|
||||
.alias('kubernetes')
|
||||
.description('Deploy to Kubernetes cluster')
|
||||
.option('--namespace <ns>', 'Kubernetes namespace', 'default')
|
||||
.option('--replicas <n>', 'Number of replicas', '2')
|
||||
.option('--env-file <path>', 'Path to .env file')
|
||||
.action(async (options) => {
|
||||
await deployToK8s(options);
|
||||
});
|
||||
|
||||
// Deployment wizard
|
||||
deploy
|
||||
.command('wizard')
|
||||
.description('Interactive deployment wizard')
|
||||
.action(async () => {
|
||||
await runDeploymentWizard();
|
||||
});
|
||||
|
||||
// Status check
|
||||
deploy
|
||||
.command('status')
|
||||
.description('Check deployment status')
|
||||
.option('--platform <platform>', 'Platform: cloudrun, docker, k8s')
|
||||
.action(async (options) => {
|
||||
await checkDeploymentStatus(options);
|
||||
});
|
||||
|
||||
return deploy;
|
||||
}
|
||||
|
||||
async function deployToCloudRun(options: Record<string, unknown>): Promise<void> {
|
||||
console.log(chalk.bold('\n☁️ Google Cloud Run Deployment\n'));
|
||||
console.log('═'.repeat(50));
|
||||
|
||||
// Check gcloud
|
||||
if (!commandExists('gcloud')) {
|
||||
console.error(chalk.red('\n✗ gcloud CLI is required'));
|
||||
console.log(chalk.gray(' Install from: https://cloud.google.com/sdk'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const spinner = ora('Checking gcloud authentication...').start();
|
||||
|
||||
try {
|
||||
// Check authentication
|
||||
const account = execSync('gcloud auth list --filter=status:ACTIVE --format="value(account)"', {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
}).trim();
|
||||
|
||||
if (!account) {
|
||||
spinner.fail('Not authenticated with gcloud');
|
||||
console.log(chalk.yellow('\nRun: gcloud auth login'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
spinner.succeed(`Authenticated as ${account}`);
|
||||
|
||||
// Get or prompt for project
|
||||
let projectId = options.project as string;
|
||||
if (!projectId) {
|
||||
projectId = execSync('gcloud config get-value project', {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
}).trim();
|
||||
|
||||
if (!projectId) {
|
||||
console.error(chalk.red('\n✗ No project ID specified'));
|
||||
console.log(chalk.gray(' Use --project <id> or run: gcloud config set project <id>'));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(chalk.cyan(` Project: ${projectId}`));
|
||||
console.log(chalk.cyan(` Region: ${options.region}`));
|
||||
console.log(chalk.cyan(` Service: ${options.service}`));
|
||||
|
||||
// Enable APIs
|
||||
spinner.start('Enabling required APIs...');
|
||||
execSync('gcloud services enable run.googleapis.com containerregistry.googleapis.com cloudbuild.googleapis.com', {
|
||||
stdio: 'pipe',
|
||||
});
|
||||
spinner.succeed('APIs enabled');
|
||||
|
||||
// Build environment variables
|
||||
let envVars = '';
|
||||
if (options.envFile) {
|
||||
const fs = await import('fs/promises');
|
||||
const envContent = await fs.readFile(options.envFile as string, 'utf-8');
|
||||
const vars = envContent
|
||||
.split('\n')
|
||||
.filter((line) => line.trim() && !line.startsWith('#'))
|
||||
.map((line) => line.trim())
|
||||
.join(',');
|
||||
envVars = `--set-env-vars="${vars}"`;
|
||||
}
|
||||
|
||||
// Check for Dockerfile
|
||||
const fs = await import('fs/promises');
|
||||
let hasDockerfile = false;
|
||||
try {
|
||||
await fs.access('Dockerfile');
|
||||
hasDockerfile = true;
|
||||
} catch {
|
||||
// Create Dockerfile
|
||||
spinner.start('Creating Dockerfile...');
|
||||
await fs.writeFile(
|
||||
'Dockerfile',
|
||||
`FROM node:20-slim
|
||||
WORKDIR /app
|
||||
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||
RUN npm install -g ruvbot
|
||||
RUN mkdir -p /app/data /app/plugins /app/skills
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \\
|
||||
CMD curl -f http://localhost:\${PORT:-8080}/health || exit 1
|
||||
CMD ["ruvbot", "start", "--port", "8080"]
|
||||
`
|
||||
);
|
||||
spinner.succeed('Dockerfile created');
|
||||
}
|
||||
|
||||
// Deploy
|
||||
spinner.start('Deploying to Cloud Run (this may take a few minutes)...');
|
||||
|
||||
const deployCmd = [
|
||||
'gcloud run deploy',
|
||||
options.service,
|
||||
'--source .',
|
||||
'--platform managed',
|
||||
`--region ${options.region}`,
|
||||
'--allow-unauthenticated',
|
||||
'--port 8080',
|
||||
`--memory ${options.memory}`,
|
||||
`--min-instances ${options.minInstances}`,
|
||||
`--max-instances ${options.maxInstances}`,
|
||||
envVars,
|
||||
'--quiet',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
execSync(deployCmd, { stdio: 'inherit' });
|
||||
|
||||
// Get URL
|
||||
const serviceUrl = execSync(
|
||||
`gcloud run services describe ${options.service} --region ${options.region} --format='value(status.url)'`,
|
||||
{ encoding: 'utf-8' }
|
||||
).trim();
|
||||
|
||||
console.log('\n' + chalk.green('═'.repeat(50)));
|
||||
console.log(chalk.bold.green('🚀 Deployment successful!'));
|
||||
console.log(chalk.green('═'.repeat(50)));
|
||||
console.log(`\n URL: ${chalk.cyan(serviceUrl)}`);
|
||||
console.log(` Health: ${chalk.cyan(serviceUrl + '/health')}`);
|
||||
console.log(` API: ${chalk.cyan(serviceUrl + '/api/status')}`);
|
||||
console.log(`\n Test: ${chalk.gray(`curl ${serviceUrl}/health`)}`);
|
||||
console.log();
|
||||
} catch (error) {
|
||||
spinner.fail('Deployment failed');
|
||||
console.error(chalk.red(`\nError: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function deployToDocker(options: Record<string, unknown>): Promise<void> {
|
||||
console.log(chalk.bold('\n🐳 Docker Deployment\n'));
|
||||
console.log('═'.repeat(50));
|
||||
|
||||
if (!commandExists('docker')) {
|
||||
console.error(chalk.red('\n✗ Docker is required'));
|
||||
console.log(chalk.gray(' Install from: https://docker.com'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const fs = await import('fs/promises');
|
||||
const spinner = ora('Creating docker-compose.yml...').start();
|
||||
|
||||
try {
|
||||
const envFileMapping = options.envFile ? `env_file:\n - ${options.envFile}` : '';
|
||||
|
||||
const composeContent = `version: '3.8'
|
||||
services:
|
||||
ruvbot:
|
||||
image: node:20-slim
|
||||
container_name: ${options.name}
|
||||
working_dir: /app
|
||||
command: sh -c "npm install -g ruvbot && ruvbot start --port 3000"
|
||||
ports:
|
||||
- "${options.port}:3000"
|
||||
${envFileMapping}
|
||||
environment:
|
||||
- OPENROUTER_API_KEY=\${OPENROUTER_API_KEY}
|
||||
- ANTHROPIC_API_KEY=\${ANTHROPIC_API_KEY}
|
||||
- SLACK_BOT_TOKEN=\${SLACK_BOT_TOKEN}
|
||||
- SLACK_SIGNING_SECRET=\${SLACK_SIGNING_SECRET}
|
||||
- SLACK_APP_TOKEN=\${SLACK_APP_TOKEN}
|
||||
- DISCORD_TOKEN=\${DISCORD_TOKEN}
|
||||
- DISCORD_CLIENT_ID=\${DISCORD_CLIENT_ID}
|
||||
- TELEGRAM_BOT_TOKEN=\${TELEGRAM_BOT_TOKEN}
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
- ./plugins:/app/plugins
|
||||
- ./skills:/app/skills
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
restart: unless-stopped
|
||||
`;
|
||||
|
||||
await fs.writeFile('docker-compose.yml', composeContent);
|
||||
spinner.succeed('docker-compose.yml created');
|
||||
|
||||
// Create directories
|
||||
await fs.mkdir('data', { recursive: true });
|
||||
await fs.mkdir('plugins', { recursive: true });
|
||||
await fs.mkdir('skills', { recursive: true });
|
||||
|
||||
if (options.detach) {
|
||||
spinner.start('Starting containers...');
|
||||
execSync('docker-compose up -d', { stdio: 'pipe' });
|
||||
spinner.succeed('Containers started');
|
||||
|
||||
console.log('\n' + chalk.green('═'.repeat(50)));
|
||||
console.log(chalk.bold.green('🚀 RuvBot is running!'));
|
||||
console.log(chalk.green('═'.repeat(50)));
|
||||
console.log(`\n URL: ${chalk.cyan(`http://localhost:${options.port}`)}`);
|
||||
console.log(` Health: ${chalk.cyan(`http://localhost:${options.port}/health`)}`);
|
||||
console.log(`\n Logs: ${chalk.gray('docker-compose logs -f')}`);
|
||||
console.log(` Stop: ${chalk.gray('docker-compose down')}`);
|
||||
console.log();
|
||||
} else {
|
||||
console.log(chalk.cyan('\nRun: docker-compose up'));
|
||||
}
|
||||
} catch (error) {
|
||||
spinner.fail('Docker deployment failed');
|
||||
console.error(chalk.red(`\nError: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function deployToK8s(options: Record<string, unknown>): Promise<void> {
|
||||
console.log(chalk.bold('\n☸️ Kubernetes Deployment\n'));
|
||||
console.log('═'.repeat(50));
|
||||
|
||||
if (!commandExists('kubectl')) {
|
||||
console.error(chalk.red('\n✗ kubectl is required'));
|
||||
console.log(chalk.gray(' Install from: https://kubernetes.io/docs/tasks/tools/'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const fs = await import('fs/promises');
|
||||
const spinner = ora('Creating Kubernetes manifests...').start();
|
||||
|
||||
try {
|
||||
await fs.mkdir('k8s', { recursive: true });
|
||||
|
||||
// Deployment manifest
|
||||
const deployment = `apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: ruvbot
|
||||
namespace: ${options.namespace}
|
||||
spec:
|
||||
replicas: ${options.replicas}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: ruvbot
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: ruvbot
|
||||
spec:
|
||||
containers:
|
||||
- name: ruvbot
|
||||
image: node:20-slim
|
||||
command: ["sh", "-c", "npm install -g ruvbot && ruvbot start --port 3000"]
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: ruvbot-secrets
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 3000
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /ready
|
||||
port: 3000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: ruvbot
|
||||
namespace: ${options.namespace}
|
||||
spec:
|
||||
selector:
|
||||
app: ruvbot
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 3000
|
||||
type: LoadBalancer
|
||||
`;
|
||||
|
||||
await fs.writeFile('k8s/deployment.yaml', deployment);
|
||||
|
||||
// Secret template
|
||||
const secret = `apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: ruvbot-secrets
|
||||
namespace: ${options.namespace}
|
||||
type: Opaque
|
||||
stringData:
|
||||
OPENROUTER_API_KEY: "YOUR_API_KEY"
|
||||
DEFAULT_MODEL: "google/gemini-2.0-flash-001"
|
||||
`;
|
||||
|
||||
await fs.writeFile('k8s/secret.yaml', secret);
|
||||
|
||||
spinner.succeed('Kubernetes manifests created in k8s/');
|
||||
|
||||
console.log('\n' + chalk.yellow('⚠️ Before applying:'));
|
||||
console.log(chalk.gray(' 1. Edit k8s/secret.yaml with your API keys'));
|
||||
console.log(chalk.gray(' 2. Review k8s/deployment.yaml'));
|
||||
console.log('\n Apply with:');
|
||||
console.log(chalk.cyan(' kubectl apply -f k8s/'));
|
||||
console.log('\n Check status:');
|
||||
console.log(chalk.cyan(' kubectl get pods -l app=ruvbot'));
|
||||
console.log();
|
||||
} catch (error) {
|
||||
spinner.fail('Kubernetes manifest creation failed');
|
||||
console.error(chalk.red(`\nError: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function runDeploymentWizard(): Promise<void> {
|
||||
console.log(chalk.bold('\n🧙 RuvBot Deployment Wizard\n'));
|
||||
console.log('═'.repeat(50));
|
||||
|
||||
// This would use inquirer or similar for interactive prompts
|
||||
// For now, provide instructions
|
||||
console.log('\nSelect a deployment target:\n');
|
||||
console.log(' 1. Google Cloud Run (serverless, auto-scaling)');
|
||||
console.log(' ' + chalk.cyan('ruvbot deploy-cloud cloudrun'));
|
||||
console.log();
|
||||
console.log(' 2. Docker (local or server deployment)');
|
||||
console.log(' ' + chalk.cyan('ruvbot deploy-cloud docker'));
|
||||
console.log();
|
||||
console.log(' 3. Kubernetes (production cluster)');
|
||||
console.log(' ' + chalk.cyan('ruvbot deploy-cloud k8s'));
|
||||
console.log();
|
||||
console.log('For interactive setup, use the install script:');
|
||||
console.log(chalk.cyan(' RUVBOT_WIZARD=true curl -fsSL https://raw.githubusercontent.com/ruvnet/ruvector/main/npm/packages/ruvbot/scripts/install.sh | bash'));
|
||||
console.log();
|
||||
}
|
||||
|
||||
async function checkDeploymentStatus(options: Record<string, unknown>): Promise<void> {
|
||||
const platform = options.platform as string;
|
||||
|
||||
console.log(chalk.bold('\n📊 Deployment Status\n'));
|
||||
|
||||
if (!platform || platform === 'cloudrun') {
|
||||
console.log(chalk.cyan('Cloud Run:'));
|
||||
if (commandExists('gcloud')) {
|
||||
try {
|
||||
const services = execSync(
|
||||
'gcloud run services list --format="table(metadata.name,status.url,status.conditions[0].status)" 2>/dev/null',
|
||||
{ encoding: 'utf-8' }
|
||||
);
|
||||
console.log(services || ' No services found');
|
||||
} catch {
|
||||
console.log(chalk.gray(' Not configured or no services'));
|
||||
}
|
||||
} else {
|
||||
console.log(chalk.gray(' gcloud CLI not installed'));
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
if (!platform || platform === 'docker') {
|
||||
console.log(chalk.cyan('Docker:'));
|
||||
if (commandExists('docker')) {
|
||||
try {
|
||||
const containers = execSync(
|
||||
'docker ps --filter "name=ruvbot" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" 2>/dev/null',
|
||||
{ encoding: 'utf-8' }
|
||||
);
|
||||
console.log(containers || ' No containers running');
|
||||
} catch {
|
||||
console.log(chalk.gray(' No containers found'));
|
||||
}
|
||||
} else {
|
||||
console.log(chalk.gray(' Docker not installed'));
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
if (!platform || platform === 'k8s') {
|
||||
console.log(chalk.cyan('Kubernetes:'));
|
||||
if (commandExists('kubectl')) {
|
||||
try {
|
||||
const pods = execSync(
|
||||
'kubectl get pods -l app=ruvbot -o wide 2>/dev/null',
|
||||
{ encoding: 'utf-8' }
|
||||
);
|
||||
console.log(pods || ' No pods found');
|
||||
} catch {
|
||||
console.log(chalk.gray(' No pods found or not configured'));
|
||||
}
|
||||
} else {
|
||||
console.log(chalk.gray(' kubectl not installed'));
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
function commandExists(cmd: string): boolean {
|
||||
try {
|
||||
execSync(`which ${cmd}`, { stdio: 'pipe' });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default createDeploymentCommand;
|
||||
|
|
@ -9,3 +9,6 @@ export { createMemoryCommand } from './memory.js';
|
|||
export { createSecurityCommand } from './security.js';
|
||||
export { createPluginsCommand } from './plugins.js';
|
||||
export { createAgentCommand } from './agent.js';
|
||||
export { createChannelsCommand, createWebhooksCommand } from './channels.js';
|
||||
export { createTemplatesCommand, createDeployCommand } from './templates.js';
|
||||
export { createDeploymentCommand } from './deploy.js';
|
||||
|
|
|
|||
213
npm/packages/ruvbot/src/cli/commands/templates.ts
Normal file
213
npm/packages/ruvbot/src/cli/commands/templates.ts
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
/**
|
||||
* RuvBot CLI - Templates Command
|
||||
*
|
||||
* Deploy pre-built agent templates with a single command.
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import {
|
||||
TEMPLATES,
|
||||
getTemplate,
|
||||
listTemplates,
|
||||
getTemplatesByCategory,
|
||||
type Template,
|
||||
} from '../../templates/index.js';
|
||||
|
||||
export function createTemplatesCommand(): Command {
|
||||
const templates = new Command('templates')
|
||||
.alias('t')
|
||||
.description('Manage and deploy agent templates');
|
||||
|
||||
// List templates
|
||||
templates
|
||||
.command('list')
|
||||
.alias('ls')
|
||||
.option('-c, --category <category>', 'Filter by category (practical, intermediate, advanced, exotic)')
|
||||
.option('--json', 'Output as JSON')
|
||||
.description('List available templates')
|
||||
.action(async (options) => {
|
||||
const byCategory = getTemplatesByCategory();
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(byCategory, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('\n🤖 RuvBot Template Library\n');
|
||||
console.log('Deploy with: npx ruvbot deploy <template-id>\n');
|
||||
|
||||
const categories = options.category
|
||||
? { [options.category]: byCategory[options.category] || [] }
|
||||
: byCategory;
|
||||
|
||||
for (const [category, templates] of Object.entries(categories)) {
|
||||
const emoji = getCategoryEmoji(category);
|
||||
console.log(`${emoji} ${category.toUpperCase()}`);
|
||||
console.log('─'.repeat(50));
|
||||
|
||||
for (const t of templates as Template[]) {
|
||||
console.log(` ${t.id.padEnd(25)} ${t.name}`);
|
||||
console.log(` ${''.padEnd(25)} ${dim(t.description)}`);
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Show template details
|
||||
templates
|
||||
.command('info <template-id>')
|
||||
.description('Show detailed information about a template')
|
||||
.action(async (templateId) => {
|
||||
const template = getTemplate(templateId);
|
||||
|
||||
if (!template) {
|
||||
console.error(`Template "${templateId}" not found.`);
|
||||
console.log('\nAvailable templates:');
|
||||
listTemplates().forEach(t => console.log(` - ${t.id}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`\n${getCategoryEmoji(template.category)} ${template.name}`);
|
||||
console.log('═'.repeat(50));
|
||||
console.log(`\n${template.description}\n`);
|
||||
|
||||
console.log('📋 Configuration:');
|
||||
console.log(` Topology: ${template.config.topology}`);
|
||||
console.log(` Max Agents: ${template.config.maxAgents}`);
|
||||
if (template.config.consensus) {
|
||||
console.log(` Consensus: ${template.config.consensus}`);
|
||||
}
|
||||
if (template.config.memory) {
|
||||
console.log(` Memory: ${template.config.memory}`);
|
||||
}
|
||||
if (template.config.workers?.length) {
|
||||
console.log(` Workers: ${template.config.workers.join(', ')}`);
|
||||
}
|
||||
|
||||
console.log('\n🤖 Agents:');
|
||||
for (const agent of template.agents) {
|
||||
console.log(` • ${agent.name} (${agent.type})`);
|
||||
console.log(` ${dim(agent.role)}`);
|
||||
}
|
||||
|
||||
console.log('\n📝 Example:');
|
||||
console.log(` ${template.example}`);
|
||||
console.log();
|
||||
});
|
||||
|
||||
return templates;
|
||||
}
|
||||
|
||||
export function createDeployCommand(): Command {
|
||||
const deploy = new Command('deploy')
|
||||
.argument('<template-id>', 'Template to deploy')
|
||||
.option('--name <name>', 'Custom name for the deployment')
|
||||
.option('--model <model>', 'Override default LLM model')
|
||||
.option('--dry-run', 'Show what would be deployed without executing')
|
||||
.option('--background', 'Run in background')
|
||||
.description('Deploy a template')
|
||||
.action(async (templateId, options) => {
|
||||
const template = getTemplate(templateId);
|
||||
|
||||
if (!template) {
|
||||
console.error(`Template "${templateId}" not found.`);
|
||||
console.log('\nRun "npx ruvbot templates list" to see available templates.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`\n🚀 Deploying: ${template.name}`);
|
||||
console.log('─'.repeat(50));
|
||||
|
||||
if (options.dryRun) {
|
||||
console.log('\n[DRY RUN] Would deploy:\n');
|
||||
showDeploymentPlan(template, options);
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate deployment commands
|
||||
const commands = generateDeploymentCommands(template, options);
|
||||
|
||||
console.log('\n📦 Initializing swarm...');
|
||||
console.log(dim(` ${commands.swarmInit}`));
|
||||
|
||||
console.log('\n🤖 Spawning agents:');
|
||||
for (const cmd of commands.agentSpawns) {
|
||||
console.log(dim(` ${cmd}`));
|
||||
}
|
||||
|
||||
if (commands.workerStarts.length > 0) {
|
||||
console.log('\n⚙️ Starting background workers:');
|
||||
for (const cmd of commands.workerStarts) {
|
||||
console.log(dim(` ${cmd}`));
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n✅ Deployment complete!');
|
||||
console.log(`\n📊 Monitor with: npx ruvbot status`);
|
||||
console.log(`🛑 Stop with: npx ruvbot stop ${options.name || templateId}`);
|
||||
});
|
||||
|
||||
return deploy;
|
||||
}
|
||||
|
||||
function showDeploymentPlan(template: Template, options: Record<string, unknown>): void {
|
||||
console.log(`Template: ${template.id}`);
|
||||
console.log(`Category: ${template.category}`);
|
||||
console.log(`Topology: ${template.config.topology}`);
|
||||
console.log(`Max Agents: ${template.config.maxAgents}`);
|
||||
console.log();
|
||||
console.log('Agents to spawn:');
|
||||
for (const agent of template.agents) {
|
||||
console.log(` • ${agent.name} (${agent.type})`);
|
||||
}
|
||||
if (template.config.workers?.length) {
|
||||
console.log();
|
||||
console.log('Workers to start:');
|
||||
for (const worker of template.config.workers) {
|
||||
console.log(` • ${worker}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface DeploymentCommands {
|
||||
swarmInit: string;
|
||||
agentSpawns: string[];
|
||||
workerStarts: string[];
|
||||
}
|
||||
|
||||
function generateDeploymentCommands(
|
||||
template: Template,
|
||||
options: Record<string, unknown>
|
||||
): DeploymentCommands {
|
||||
const name = (options.name as string) || template.id;
|
||||
|
||||
// Swarm initialization
|
||||
const swarmInit = `npx @claude-flow/cli@latest swarm init --topology ${template.config.topology} --max-agents ${template.config.maxAgents}${template.config.consensus ? ` --consensus ${template.config.consensus}` : ''}`;
|
||||
|
||||
// Agent spawn commands
|
||||
const agentSpawns = template.agents.map(agent => {
|
||||
const model = (options.model as string) || agent.model || 'google/gemini-2.0-flash-001';
|
||||
return `npx @claude-flow/cli@latest agent spawn -t ${agent.type} --name ${agent.name}`;
|
||||
});
|
||||
|
||||
// Worker start commands
|
||||
const workerStarts = (template.config.workers || []).map(worker =>
|
||||
`npx @claude-flow/cli@latest hooks worker dispatch --trigger ${worker}`
|
||||
);
|
||||
|
||||
return { swarmInit, agentSpawns, workerStarts };
|
||||
}
|
||||
|
||||
function getCategoryEmoji(category: string): string {
|
||||
const emojis: Record<string, string> = {
|
||||
practical: '🔧',
|
||||
intermediate: '⚡',
|
||||
advanced: '🧠',
|
||||
exotic: '🌌',
|
||||
};
|
||||
return emojis[category] || '📦';
|
||||
}
|
||||
|
||||
function dim(text: string): string {
|
||||
return `\x1b[2m${text}\x1b[0m`;
|
||||
}
|
||||
|
|
@ -31,8 +31,17 @@ import {
|
|||
createPluginsCommand,
|
||||
createAgentCommand,
|
||||
} from './commands/index.js';
|
||||
import {
|
||||
createTemplatesCommand,
|
||||
createDeployCommand,
|
||||
} from './commands/templates.js';
|
||||
import {
|
||||
createChannelsCommand,
|
||||
createWebhooksCommand,
|
||||
} from './commands/channels.js';
|
||||
import { createDeploymentCommand } from './commands/deploy.js';
|
||||
|
||||
const VERSION = '0.1.0';
|
||||
const VERSION = '0.1.6';
|
||||
|
||||
export function createCLI(): Command {
|
||||
const program = new Command();
|
||||
|
|
@ -390,6 +399,11 @@ RUVBOT_LOG_LEVEL=info
|
|||
program.addCommand(createSecurityCommand());
|
||||
program.addCommand(createPluginsCommand());
|
||||
program.addCommand(createAgentCommand());
|
||||
program.addCommand(createTemplatesCommand());
|
||||
program.addCommand(createDeployCommand());
|
||||
program.addCommand(createChannelsCommand());
|
||||
program.addCommand(createWebhooksCommand());
|
||||
program.addCommand(createDeploymentCommand());
|
||||
|
||||
// ============================================================================
|
||||
// Version Info
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@
|
|||
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
|
||||
import { URL } from 'node:url';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import pino from 'pino';
|
||||
import { RuvBot, createRuvBot } from './RuvBot.js';
|
||||
import { createAIDefenceGuard, type AIDefenceConfig } from './security/AIDefenceGuard.js';
|
||||
|
|
@ -104,15 +106,98 @@ function sendError(res: ServerResponse, statusCode: number, message: string, cod
|
|||
sendJSON(res, statusCode, { error: message, code: code || 'ERROR' });
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Static File Serving
|
||||
// ============================================================================
|
||||
|
||||
function serveStaticFile(res: ServerResponse, filePath: string, contentType: string): boolean {
|
||||
try {
|
||||
const content = readFileSync(filePath, 'utf-8');
|
||||
res.writeHead(200, {
|
||||
'Content-Type': contentType,
|
||||
'Cache-Control': 'public, max-age=3600',
|
||||
});
|
||||
res.end(content);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getChatUIPath(): string {
|
||||
// Try multiple locations for the chat UI
|
||||
// Works in both development (src/) and production (dist/)
|
||||
const cwd = process.cwd();
|
||||
const possiblePaths = [
|
||||
// Docker/Cloud Run paths (WORKDIR /app)
|
||||
join(cwd, 'dist', 'api', 'public', 'index.html'),
|
||||
// Development paths
|
||||
join(cwd, 'src', 'api', 'public', 'index.html'),
|
||||
// Production paths (ESM)
|
||||
join(cwd, 'dist', 'esm', 'api', 'public', 'index.html'),
|
||||
// When running from node_modules
|
||||
join(cwd, 'node_modules', 'ruvbot', 'dist', 'api', 'public', 'index.html'),
|
||||
join(cwd, 'node_modules', 'ruvbot', 'src', 'api', 'public', 'index.html'),
|
||||
// Absolute paths (for Docker)
|
||||
'/app/dist/api/public/index.html',
|
||||
'/app/src/api/public/index.html',
|
||||
];
|
||||
|
||||
for (const p of possiblePaths) {
|
||||
if (existsSync(p)) {
|
||||
logger.info({ path: p }, 'Found chat UI');
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
logger.warn({ cwd, paths: possiblePaths }, 'Chat UI not found, using fallback');
|
||||
return possiblePaths[0]; // Default to first path
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Route Handlers
|
||||
// ============================================================================
|
||||
|
||||
async function handleRoot(ctx: RequestContext): Promise<void> {
|
||||
const { res } = ctx;
|
||||
const chatUIPath = getChatUIPath();
|
||||
|
||||
if (existsSync(chatUIPath)) {
|
||||
serveStaticFile(res, chatUIPath, 'text/html; charset=utf-8');
|
||||
} else {
|
||||
// Fallback: serve a simple redirect or message
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||
res.end(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>RuvBot</title>
|
||||
<style>
|
||||
body { font-family: system-ui; background: #0a0a0f; color: #f0f0f5; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
|
||||
.container { text-align: center; }
|
||||
h1 { font-size: 3rem; margin-bottom: 1rem; }
|
||||
p { color: #a0a0b0; margin-bottom: 2rem; }
|
||||
a { color: #6366f1; text-decoration: none; padding: 12px 24px; border: 1px solid #6366f1; border-radius: 8px; display: inline-block; }
|
||||
a:hover { background: #6366f1; color: white; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🤖 RuvBot</h1>
|
||||
<p>Enterprise-grade AI Assistant</p>
|
||||
<a href="/api/status">View API Status</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleHealth(ctx: RequestContext): Promise<void> {
|
||||
const { res } = ctx;
|
||||
sendJSON(res, 200, {
|
||||
status: 'healthy',
|
||||
version: '0.1.0',
|
||||
version: '0.1.6',
|
||||
uptime: Math.floor((Date.now() - startTime) / 1000),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
|
@ -312,6 +397,7 @@ async function handleChat(ctx: RequestContext): Promise<void> {
|
|||
// ============================================================================
|
||||
|
||||
const routes: Route[] = [
|
||||
{ method: 'GET', pattern: /^\/$/, handler: handleRoot },
|
||||
{ method: 'GET', pattern: /^\/health$/, handler: handleHealth },
|
||||
{ method: 'GET', pattern: /^\/ready$/, handler: handleReady },
|
||||
{ method: 'GET', pattern: /^\/api\/status$/, handler: handleStatus },
|
||||
|
|
|
|||
358
npm/packages/ruvbot/src/templates/index.ts
Normal file
358
npm/packages/ruvbot/src/templates/index.ts
Normal file
|
|
@ -0,0 +1,358 @@
|
|||
/**
|
||||
* RuvBot Template Library
|
||||
*
|
||||
* Pre-built templates for common long-running agent patterns.
|
||||
* Deploy with: npx ruvbot deploy <template-name>
|
||||
*/
|
||||
|
||||
export interface Template {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: 'practical' | 'intermediate' | 'advanced' | 'exotic';
|
||||
agents: AgentSpec[];
|
||||
config: TemplateConfig;
|
||||
example: string;
|
||||
}
|
||||
|
||||
export interface AgentSpec {
|
||||
type: string;
|
||||
name: string;
|
||||
role: string;
|
||||
model?: string;
|
||||
systemPrompt?: string;
|
||||
}
|
||||
|
||||
export interface TemplateConfig {
|
||||
topology: 'hierarchical' | 'mesh' | 'star' | 'ring' | 'hive-mind';
|
||||
maxAgents: number;
|
||||
consensus?: 'raft' | 'byzantine' | 'gossip' | 'crdt';
|
||||
memory?: 'local' | 'distributed' | 'hybrid';
|
||||
workers?: string[];
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PRACTICAL TEMPLATES
|
||||
// =============================================================================
|
||||
|
||||
export const CODE_REVIEWER: Template = {
|
||||
id: 'code-reviewer',
|
||||
name: 'Code Review Bot',
|
||||
description: 'Automated code review with security scanning and best practices',
|
||||
category: 'practical',
|
||||
agents: [
|
||||
{ type: 'reviewer', name: 'code-reviewer', role: 'Review code for quality and patterns' },
|
||||
{ type: 'security-auditor', name: 'security-scanner', role: 'Scan for vulnerabilities' },
|
||||
],
|
||||
config: {
|
||||
topology: 'star',
|
||||
maxAgents: 3,
|
||||
workers: ['audit', 'testgaps'],
|
||||
},
|
||||
example: `npx ruvbot deploy code-reviewer --repo ./my-project`,
|
||||
};
|
||||
|
||||
export const DOC_GENERATOR: Template = {
|
||||
id: 'doc-generator',
|
||||
name: 'Documentation Generator',
|
||||
description: 'Auto-generate and maintain project documentation',
|
||||
category: 'practical',
|
||||
agents: [
|
||||
{ type: 'researcher', name: 'code-analyzer', role: 'Analyze codebase structure' },
|
||||
{ type: 'api-docs', name: 'doc-writer', role: 'Generate API documentation' },
|
||||
],
|
||||
config: {
|
||||
topology: 'star',
|
||||
maxAgents: 2,
|
||||
workers: ['document', 'map'],
|
||||
},
|
||||
example: `npx ruvbot deploy doc-generator --output ./docs`,
|
||||
};
|
||||
|
||||
export const TEST_GENERATOR: Template = {
|
||||
id: 'test-generator',
|
||||
name: 'Test Suite Generator',
|
||||
description: 'Generate comprehensive test suites with TDD approach',
|
||||
category: 'practical',
|
||||
agents: [
|
||||
{ type: 'tester', name: 'test-writer', role: 'Write unit and integration tests' },
|
||||
{ type: 'coder', name: 'mock-generator', role: 'Generate mocks and fixtures' },
|
||||
],
|
||||
config: {
|
||||
topology: 'star',
|
||||
maxAgents: 3,
|
||||
workers: ['testgaps', 'benchmark'],
|
||||
},
|
||||
example: `npx ruvbot deploy test-generator --coverage 80`,
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// INTERMEDIATE TEMPLATES
|
||||
// =============================================================================
|
||||
|
||||
export const FEATURE_SWARM: Template = {
|
||||
id: 'feature-swarm',
|
||||
name: 'Feature Development Swarm',
|
||||
description: 'Parallel feature development with coordinated agents',
|
||||
category: 'intermediate',
|
||||
agents: [
|
||||
{ type: 'planner', name: 'architect', role: 'Design feature architecture' },
|
||||
{ type: 'coder', name: 'implementer', role: 'Implement feature code' },
|
||||
{ type: 'tester', name: 'qa', role: 'Write and run tests' },
|
||||
{ type: 'reviewer', name: 'reviewer', role: 'Code review and refinement' },
|
||||
],
|
||||
config: {
|
||||
topology: 'hierarchical',
|
||||
maxAgents: 6,
|
||||
consensus: 'raft',
|
||||
memory: 'hybrid',
|
||||
workers: ['optimize', 'testgaps'],
|
||||
},
|
||||
example: `npx ruvbot deploy feature-swarm --feature "Add user auth"`,
|
||||
};
|
||||
|
||||
export const REFACTOR_SQUAD: Template = {
|
||||
id: 'refactor-squad',
|
||||
name: 'Refactoring Squad',
|
||||
description: 'Coordinated codebase refactoring across multiple files',
|
||||
category: 'intermediate',
|
||||
agents: [
|
||||
{ type: 'system-architect', name: 'architect', role: 'Plan refactoring strategy' },
|
||||
{ type: 'coder', name: 'refactorer-1', role: 'Execute refactoring' },
|
||||
{ type: 'coder', name: 'refactorer-2', role: 'Execute refactoring' },
|
||||
{ type: 'tester', name: 'regression', role: 'Ensure no regressions' },
|
||||
],
|
||||
config: {
|
||||
topology: 'mesh',
|
||||
maxAgents: 5,
|
||||
consensus: 'raft',
|
||||
memory: 'distributed',
|
||||
workers: ['map', 'optimize'],
|
||||
},
|
||||
example: `npx ruvbot deploy refactor-squad --pattern "extract-service"`,
|
||||
};
|
||||
|
||||
export const CI_CD_PIPELINE: Template = {
|
||||
id: 'ci-cd-pipeline',
|
||||
name: 'CI/CD Pipeline Agent',
|
||||
description: 'Automated build, test, and deployment pipeline',
|
||||
category: 'intermediate',
|
||||
agents: [
|
||||
{ type: 'cicd-engineer', name: 'pipeline-manager', role: 'Orchestrate CI/CD' },
|
||||
{ type: 'tester', name: 'test-runner', role: 'Execute test suites' },
|
||||
{ type: 'security-auditor', name: 'security-gate', role: 'Security validation' },
|
||||
],
|
||||
config: {
|
||||
topology: 'star',
|
||||
maxAgents: 4,
|
||||
workers: ['audit', 'benchmark'],
|
||||
},
|
||||
example: `npx ruvbot deploy ci-cd-pipeline --trigger push`,
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// ADVANCED TEMPLATES
|
||||
// =============================================================================
|
||||
|
||||
export const SELF_LEARNING_BOT: Template = {
|
||||
id: 'self-learning-bot',
|
||||
name: 'Self-Learning Assistant',
|
||||
description: 'AI that improves from interactions using neural patterns',
|
||||
category: 'advanced',
|
||||
agents: [
|
||||
{ type: 'safla-neural', name: 'learner', role: 'Learn from interactions' },
|
||||
{ type: 'memory-coordinator', name: 'memory', role: 'Manage persistent memory' },
|
||||
{ type: 'coder', name: 'executor', role: 'Execute learned patterns' },
|
||||
],
|
||||
config: {
|
||||
topology: 'hierarchical',
|
||||
maxAgents: 4,
|
||||
consensus: 'raft',
|
||||
memory: 'hybrid',
|
||||
workers: ['ultralearn', 'consolidate', 'predict'],
|
||||
},
|
||||
example: `npx ruvbot deploy self-learning-bot --domain "code-assistance"`,
|
||||
};
|
||||
|
||||
export const RESEARCH_SWARM: Template = {
|
||||
id: 'research-swarm',
|
||||
name: 'Research Swarm',
|
||||
description: 'Distributed research across multiple sources and domains',
|
||||
category: 'advanced',
|
||||
agents: [
|
||||
{ type: 'researcher', name: 'lead-researcher', role: 'Coordinate research' },
|
||||
{ type: 'researcher', name: 'web-researcher', role: 'Search web sources' },
|
||||
{ type: 'researcher', name: 'code-researcher', role: 'Analyze codebases' },
|
||||
{ type: 'analyst', name: 'synthesizer', role: 'Synthesize findings' },
|
||||
],
|
||||
config: {
|
||||
topology: 'mesh',
|
||||
maxAgents: 6,
|
||||
consensus: 'gossip',
|
||||
memory: 'distributed',
|
||||
workers: ['deepdive', 'map'],
|
||||
},
|
||||
example: `npx ruvbot deploy research-swarm --topic "vector databases"`,
|
||||
};
|
||||
|
||||
export const PERFORMANCE_OPTIMIZER: Template = {
|
||||
id: 'performance-optimizer',
|
||||
name: 'Performance Optimizer',
|
||||
description: 'Continuous performance monitoring and optimization',
|
||||
category: 'advanced',
|
||||
agents: [
|
||||
{ type: 'perf-analyzer', name: 'profiler', role: 'Profile performance' },
|
||||
{ type: 'performance-optimizer', name: 'optimizer', role: 'Implement optimizations' },
|
||||
{ type: 'tester', name: 'benchmark-runner', role: 'Run benchmarks' },
|
||||
],
|
||||
config: {
|
||||
topology: 'star',
|
||||
maxAgents: 4,
|
||||
memory: 'hybrid',
|
||||
workers: ['optimize', 'benchmark'],
|
||||
},
|
||||
example: `npx ruvbot deploy performance-optimizer --target ./src`,
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// EXOTIC TEMPLATES
|
||||
// =============================================================================
|
||||
|
||||
export const BYZANTINE_VALIDATOR: Template = {
|
||||
id: 'byzantine-validator',
|
||||
name: 'Byzantine Fault-Tolerant Validator',
|
||||
description: 'High-stakes validation with Byzantine consensus (tolerates 33% malicious)',
|
||||
category: 'exotic',
|
||||
agents: [
|
||||
{ type: 'byzantine-coordinator', name: 'primary', role: 'Lead consensus' },
|
||||
{ type: 'consensus-coordinator', name: 'validator-1', role: 'Validate decisions' },
|
||||
{ type: 'consensus-coordinator', name: 'validator-2', role: 'Validate decisions' },
|
||||
{ type: 'consensus-coordinator', name: 'validator-3', role: 'Validate decisions' },
|
||||
{ type: 'security-manager', name: 'crypto-verifier', role: 'Cryptographic verification' },
|
||||
],
|
||||
config: {
|
||||
topology: 'mesh',
|
||||
maxAgents: 7,
|
||||
consensus: 'byzantine',
|
||||
memory: 'distributed',
|
||||
workers: ['audit'],
|
||||
},
|
||||
example: `npx ruvbot deploy byzantine-validator --quorum 4`,
|
||||
};
|
||||
|
||||
export const HIVE_MIND: Template = {
|
||||
id: 'hive-mind',
|
||||
name: 'Hive-Mind Collective',
|
||||
description: 'Emergent collective intelligence with queen-led coordination',
|
||||
category: 'exotic',
|
||||
agents: [
|
||||
{ type: 'queen-coordinator', name: 'queen', role: 'Strategic orchestration' },
|
||||
{ type: 'worker-specialist', name: 'worker-1', role: 'Task execution' },
|
||||
{ type: 'worker-specialist', name: 'worker-2', role: 'Task execution' },
|
||||
{ type: 'worker-specialist', name: 'worker-3', role: 'Task execution' },
|
||||
{ type: 'scout-explorer', name: 'scout-1', role: 'Information reconnaissance' },
|
||||
{ type: 'scout-explorer', name: 'scout-2', role: 'Information reconnaissance' },
|
||||
{ type: 'swarm-memory-manager', name: 'memory-sync', role: 'Distributed memory' },
|
||||
{ type: 'collective-intelligence-coordinator', name: 'hive-brain', role: 'Collective decisions' },
|
||||
],
|
||||
config: {
|
||||
topology: 'hive-mind',
|
||||
maxAgents: 15,
|
||||
consensus: 'crdt',
|
||||
memory: 'distributed',
|
||||
workers: ['ultralearn', 'consolidate', 'predict', 'map'],
|
||||
},
|
||||
example: `npx ruvbot deploy hive-mind --objective "Build complete app"`,
|
||||
};
|
||||
|
||||
export const MULTI_REPO_COORDINATOR: Template = {
|
||||
id: 'multi-repo-coordinator',
|
||||
name: 'Multi-Repository Coordinator',
|
||||
description: 'Coordinate changes across multiple repositories',
|
||||
category: 'exotic',
|
||||
agents: [
|
||||
{ type: 'repo-architect', name: 'coordinator', role: 'Cross-repo orchestration' },
|
||||
{ type: 'sync-coordinator', name: 'sync-manager', role: 'Version synchronization' },
|
||||
{ type: 'pr-manager', name: 'pr-coordinator', role: 'PR management' },
|
||||
{ type: 'release-manager', name: 'release', role: 'Release coordination' },
|
||||
],
|
||||
config: {
|
||||
topology: 'hierarchical',
|
||||
maxAgents: 8,
|
||||
consensus: 'raft',
|
||||
memory: 'distributed',
|
||||
workers: ['audit', 'document'],
|
||||
},
|
||||
example: `npx ruvbot deploy multi-repo-coordinator --repos "repo1,repo2,repo3"`,
|
||||
};
|
||||
|
||||
export const ADVERSARIAL_TESTER: Template = {
|
||||
id: 'adversarial-tester',
|
||||
name: 'Adversarial Security Tester',
|
||||
description: 'Red team vs blue team security testing with adversarial agents',
|
||||
category: 'exotic',
|
||||
agents: [
|
||||
{ type: 'security-architect', name: 'red-team-lead', role: 'Attack planning' },
|
||||
{ type: 'security-auditor', name: 'attacker-1', role: 'Execute attacks' },
|
||||
{ type: 'security-auditor', name: 'attacker-2', role: 'Execute attacks' },
|
||||
{ type: 'security-manager', name: 'blue-team-lead', role: 'Defense coordination' },
|
||||
{ type: 'security-auditor', name: 'defender', role: 'Implement defenses' },
|
||||
],
|
||||
config: {
|
||||
topology: 'mesh',
|
||||
maxAgents: 6,
|
||||
consensus: 'byzantine',
|
||||
memory: 'distributed',
|
||||
workers: ['audit', 'deepdive'],
|
||||
},
|
||||
example: `npx ruvbot deploy adversarial-tester --target ./api`,
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// TEMPLATE REGISTRY
|
||||
// =============================================================================
|
||||
|
||||
export const TEMPLATES: Record<string, Template> = {
|
||||
// Practical
|
||||
'code-reviewer': CODE_REVIEWER,
|
||||
'doc-generator': DOC_GENERATOR,
|
||||
'test-generator': TEST_GENERATOR,
|
||||
|
||||
// Intermediate
|
||||
'feature-swarm': FEATURE_SWARM,
|
||||
'refactor-squad': REFACTOR_SQUAD,
|
||||
'ci-cd-pipeline': CI_CD_PIPELINE,
|
||||
|
||||
// Advanced
|
||||
'self-learning-bot': SELF_LEARNING_BOT,
|
||||
'research-swarm': RESEARCH_SWARM,
|
||||
'performance-optimizer': PERFORMANCE_OPTIMIZER,
|
||||
|
||||
// Exotic
|
||||
'byzantine-validator': BYZANTINE_VALIDATOR,
|
||||
'hive-mind': HIVE_MIND,
|
||||
'multi-repo-coordinator': MULTI_REPO_COORDINATOR,
|
||||
'adversarial-tester': ADVERSARIAL_TESTER,
|
||||
};
|
||||
|
||||
export const TEMPLATE_LIST = Object.values(TEMPLATES);
|
||||
|
||||
export function getTemplate(id: string): Template | undefined {
|
||||
return TEMPLATES[id];
|
||||
}
|
||||
|
||||
export function listTemplates(category?: Template['category']): Template[] {
|
||||
if (category) {
|
||||
return TEMPLATE_LIST.filter(t => t.category === category);
|
||||
}
|
||||
return TEMPLATE_LIST;
|
||||
}
|
||||
|
||||
export function getTemplatesByCategory(): Record<string, Template[]> {
|
||||
return {
|
||||
practical: listTemplates('practical'),
|
||||
intermediate: listTemplates('intermediate'),
|
||||
advanced: listTemplates('advanced'),
|
||||
exotic: listTemplates('exotic'),
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue