Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
234
vendor/ruvector/npm/packages/ruvbot/.env.example
vendored
Normal file
234
vendor/ruvector/npm/packages/ruvbot/.env.example
vendored
Normal file
@@ -0,0 +1,234 @@
|
||||
# =============================================================================
|
||||
# RuvBot Environment Configuration
|
||||
# =============================================================================
|
||||
# Copy this file to .env and configure your settings
|
||||
# All variables are optional unless marked as REQUIRED
|
||||
|
||||
# =============================================================================
|
||||
# Core Configuration
|
||||
# =============================================================================
|
||||
|
||||
# Bot instance name (used in logs and multi-tenant scenarios)
|
||||
RUVBOT_NAME=my-ruvbot
|
||||
|
||||
# API server port (default: 3000)
|
||||
RUVBOT_PORT=3000
|
||||
|
||||
# Log level: debug, info, warn, error (default: info)
|
||||
RUVBOT_LOG_LEVEL=info
|
||||
|
||||
# Enable debug mode (default: false)
|
||||
RUVBOT_DEBUG=false
|
||||
|
||||
# Environment: development, staging, production (default: development)
|
||||
NODE_ENV=development
|
||||
|
||||
# =============================================================================
|
||||
# LLM Provider Configuration (at least one REQUIRED for AI features)
|
||||
# =============================================================================
|
||||
|
||||
# Anthropic Claude (RECOMMENDED)
|
||||
ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
# OpenRouter (alternative - access to multiple models)
|
||||
OPENROUTER_API_KEY=sk-or-xxxxxxxxxxxxxxxxxxxxx
|
||||
OPENROUTER_DEFAULT_MODEL=anthropic/claude-3.5-sonnet
|
||||
|
||||
# OpenAI (optional)
|
||||
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
# Google AI (optional)
|
||||
GOOGLE_API_KEY=xxxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
# QwQ Reasoning Model (optional - for advanced reasoning tasks)
|
||||
QWQ_API_KEY=xxxxxxxxxxxxxxxxxxxxx
|
||||
QWQ_MODEL=qwq-32b-preview
|
||||
|
||||
# =============================================================================
|
||||
# Database Configuration
|
||||
# =============================================================================
|
||||
|
||||
# Storage type: sqlite, postgres, memory (default: sqlite)
|
||||
RUVBOT_STORAGE_TYPE=sqlite
|
||||
|
||||
# SQLite database path (when using sqlite)
|
||||
RUVBOT_SQLITE_PATH=./data/ruvbot.db
|
||||
|
||||
# PostgreSQL connection (when using postgres)
|
||||
# Format: postgresql://user:password@host:port/database
|
||||
DATABASE_URL=postgresql://ruvbot:password@localhost:5432/ruvbot
|
||||
|
||||
# PostgreSQL individual settings (alternative to DATABASE_URL)
|
||||
POSTGRES_HOST=localhost
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_USER=ruvbot
|
||||
POSTGRES_PASSWORD=your-secure-password
|
||||
POSTGRES_DATABASE=ruvbot
|
||||
|
||||
# =============================================================================
|
||||
# Redis Configuration (optional - for caching and queues)
|
||||
# =============================================================================
|
||||
|
||||
# Redis connection URL
|
||||
REDIS_URL=redis://localhost:6379
|
||||
|
||||
# Redis individual settings (alternative to REDIS_URL)
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
|
||||
# =============================================================================
|
||||
# Vector Memory Configuration
|
||||
# =============================================================================
|
||||
|
||||
# Embedding dimensions (default: 384 for MiniLM)
|
||||
RUVBOT_EMBEDDING_DIM=384
|
||||
|
||||
# Maximum vectors to store (default: 100000)
|
||||
RUVBOT_MAX_VECTORS=100000
|
||||
|
||||
# HNSW index parameters
|
||||
RUVBOT_HNSW_M=16
|
||||
RUVBOT_HNSW_EF_CONSTRUCTION=200
|
||||
RUVBOT_HNSW_EF_SEARCH=50
|
||||
|
||||
# Memory persistence path
|
||||
RUVBOT_MEMORY_PATH=./data/memory
|
||||
|
||||
# =============================================================================
|
||||
# Channel Adapters
|
||||
# =============================================================================
|
||||
|
||||
# Slack Integration
|
||||
SLACK_BOT_TOKEN=xoxb-xxxxxxxxxxxxxxxxxxxxx
|
||||
SLACK_APP_TOKEN=xapp-xxxxxxxxxxxxxxxxxxxxx
|
||||
SLACK_SIGNING_SECRET=xxxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
# Discord Integration
|
||||
DISCORD_BOT_TOKEN=xxxxxxxxxxxxxxxxxxxxx
|
||||
DISCORD_CLIENT_ID=xxxxxxxxxxxxxxxxxxxxx
|
||||
DISCORD_GUILD_ID=xxxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
# Telegram Integration
|
||||
TELEGRAM_BOT_TOKEN=xxxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
# Webhook endpoint (for custom integrations)
|
||||
WEBHOOK_SECRET=your-webhook-secret
|
||||
WEBHOOK_ENDPOINT=/api/webhooks
|
||||
|
||||
# =============================================================================
|
||||
# Security Configuration
|
||||
# =============================================================================
|
||||
|
||||
# Enable AI Defense (prompt injection protection)
|
||||
RUVBOT_AIDEFENCE_ENABLED=true
|
||||
|
||||
# AI Defense threat blocking threshold: none, low, medium, high, critical
|
||||
RUVBOT_AIDEFENCE_THRESHOLD=medium
|
||||
|
||||
# Enable PII detection and masking
|
||||
RUVBOT_PII_DETECTION=true
|
||||
|
||||
# Enable audit logging
|
||||
RUVBOT_AUDIT_LOG=true
|
||||
|
||||
# API authentication secret (for API endpoints)
|
||||
RUVBOT_API_SECRET=your-api-secret-key
|
||||
|
||||
# JWT secret (for token-based auth)
|
||||
RUVBOT_JWT_SECRET=your-jwt-secret-key
|
||||
|
||||
# Allowed origins for CORS (comma-separated)
|
||||
RUVBOT_CORS_ORIGINS=http://localhost:3000,http://localhost:5173
|
||||
|
||||
# =============================================================================
|
||||
# Plugin Configuration
|
||||
# =============================================================================
|
||||
|
||||
# Enable plugin system
|
||||
RUVBOT_PLUGINS_ENABLED=true
|
||||
|
||||
# Plugin directory path
|
||||
RUVBOT_PLUGINS_DIR=./plugins
|
||||
|
||||
# Auto-load plugins on startup
|
||||
RUVBOT_PLUGINS_AUTOLOAD=true
|
||||
|
||||
# Maximum plugins allowed
|
||||
RUVBOT_PLUGINS_MAX=50
|
||||
|
||||
# IPFS gateway for plugin registry (optional)
|
||||
RUVBOT_IPFS_GATEWAY=https://ipfs.io
|
||||
|
||||
# =============================================================================
|
||||
# Swarm/Multi-Agent Configuration
|
||||
# =============================================================================
|
||||
|
||||
# Enable swarm coordination
|
||||
RUVBOT_SWARM_ENABLED=false
|
||||
|
||||
# Swarm topology: hierarchical, mesh, ring, star
|
||||
RUVBOT_SWARM_TOPOLOGY=hierarchical
|
||||
|
||||
# Maximum agents in swarm
|
||||
RUVBOT_SWARM_MAX_AGENTS=8
|
||||
|
||||
# Consensus algorithm: byzantine, raft, gossip
|
||||
RUVBOT_SWARM_CONSENSUS=raft
|
||||
|
||||
# =============================================================================
|
||||
# GCP Deployment (optional - for cloud deployment)
|
||||
# =============================================================================
|
||||
|
||||
# GCP Project ID
|
||||
GCP_PROJECT_ID=your-project-id
|
||||
|
||||
# GCP Region
|
||||
GCP_REGION=us-central1
|
||||
|
||||
# Cloud Run service name
|
||||
GCP_SERVICE_NAME=ruvbot
|
||||
|
||||
# Cloud SQL instance connection name
|
||||
GCP_SQL_CONNECTION=project:region:instance
|
||||
|
||||
# Secret Manager prefix
|
||||
GCP_SECRET_PREFIX=ruvbot
|
||||
|
||||
# =============================================================================
|
||||
# Monitoring & Observability
|
||||
# =============================================================================
|
||||
|
||||
# Enable metrics collection
|
||||
RUVBOT_METRICS_ENABLED=true
|
||||
|
||||
# Metrics endpoint path
|
||||
RUVBOT_METRICS_PATH=/metrics
|
||||
|
||||
# Enable health check endpoint
|
||||
RUVBOT_HEALTH_ENABLED=true
|
||||
|
||||
# Health check path
|
||||
RUVBOT_HEALTH_PATH=/health
|
||||
|
||||
# OpenTelemetry endpoint (optional)
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
|
||||
|
||||
# Sentry DSN (optional - for error tracking)
|
||||
SENTRY_DSN=https://xxx@sentry.io/xxx
|
||||
|
||||
# =============================================================================
|
||||
# Development Options
|
||||
# =============================================================================
|
||||
|
||||
# Enable hot reload (development only)
|
||||
RUVBOT_HOT_RELOAD=false
|
||||
|
||||
# Mock LLM responses (for testing without API calls)
|
||||
RUVBOT_MOCK_LLM=false
|
||||
|
||||
# Verbose SQL logging
|
||||
RUVBOT_SQL_LOGGING=false
|
||||
|
||||
# Skip SSL verification (development only - NEVER in production)
|
||||
NODE_TLS_REJECT_UNAUTHORIZED=1
|
||||
80
vendor/ruvector/npm/packages/ruvbot/Dockerfile
vendored
Normal file
80
vendor/ruvector/npm/packages/ruvbot/Dockerfile
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
# =============================================================================
|
||||
# RuvBot - Multi-stage Dockerfile for Google Cloud Run
|
||||
# =============================================================================
|
||||
# Optimized for:
|
||||
# - Minimal image size (~150MB)
|
||||
# - Fast cold starts (<2s)
|
||||
# - Security (non-root, distroless base)
|
||||
# - Cost efficiency (Cloud Run serverless)
|
||||
# =============================================================================
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Stage 1: Dependencies
|
||||
# -----------------------------------------------------------------------------
|
||||
FROM node:22-alpine AS deps
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package.json package-lock.json* ./
|
||||
|
||||
# Install production dependencies only
|
||||
RUN npm ci --only=production --ignore-scripts && \
|
||||
npm cache clean --force
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Stage 2: Builder
|
||||
# -----------------------------------------------------------------------------
|
||||
FROM node:22-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package.json package-lock.json* tsconfig*.json ./
|
||||
|
||||
# Install all dependencies (including dev)
|
||||
RUN npm ci --ignore-scripts
|
||||
|
||||
# Copy source code
|
||||
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
|
||||
# -----------------------------------------------------------------------------
|
||||
FROM node:22-alpine AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Security: Create non-root user
|
||||
RUN addgroup --system --gid 1001 ruvbot && \
|
||||
adduser --system --uid 1001 --ingroup ruvbot ruvbot
|
||||
|
||||
# Set production environment
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=8080
|
||||
|
||||
# Copy production dependencies
|
||||
COPY --from=deps --chown=ruvbot:ruvbot /app/node_modules ./node_modules
|
||||
|
||||
# Copy built application
|
||||
COPY --from=builder --chown=ruvbot:ruvbot /app/dist ./dist
|
||||
COPY --from=builder --chown=ruvbot:ruvbot /app/package.json ./
|
||||
|
||||
# Health check endpoint
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
|
||||
|
||||
# Switch to non-root user
|
||||
USER ruvbot
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8080
|
||||
|
||||
# Start the application
|
||||
CMD ["node", "dist/server.js"]
|
||||
1480
vendor/ruvector/npm/packages/ruvbot/README.md
vendored
Normal file
1480
vendor/ruvector/npm/packages/ruvbot/README.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
19
vendor/ruvector/npm/packages/ruvbot/bin/cli.js
vendored
Executable file
19
vendor/ruvector/npm/packages/ruvbot/bin/cli.js
vendored
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* RuvBot CLI entry point
|
||||
*
|
||||
* Usage:
|
||||
* npx @ruvector/ruvbot init
|
||||
* npx @ruvector/ruvbot start
|
||||
* npx @ruvector/ruvbot config
|
||||
* npx @ruvector/ruvbot skills list
|
||||
* npx @ruvector/ruvbot status
|
||||
*/
|
||||
|
||||
import { main } from '../dist/cli/index.mjs';
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('Error:', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
47
vendor/ruvector/npm/packages/ruvbot/bin/ruvbot.js
vendored
Normal file
47
vendor/ruvector/npm/packages/ruvbot/bin/ruvbot.js
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* RuvBot CLI Entry Point
|
||||
*
|
||||
* Usage:
|
||||
* npx ruvbot <command> [options]
|
||||
* ruvbot <command> [options]
|
||||
*
|
||||
* Commands:
|
||||
* start Start the RuvBot server
|
||||
* init Initialize RuvBot in current directory
|
||||
* doctor Run diagnostics and health checks
|
||||
* config Manage configuration
|
||||
* memory Memory management commands
|
||||
* security Security scanning and audit
|
||||
* plugins Plugin management
|
||||
* agent Agent management
|
||||
* status Show bot status
|
||||
*/
|
||||
|
||||
require('dotenv/config');
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
// Try CJS build first
|
||||
const { main } = require('../dist/cli/index.js');
|
||||
await main();
|
||||
} catch (cjsError) {
|
||||
// Fall back to dynamic import for ESM
|
||||
try {
|
||||
const { main } = await import('../dist/esm/cli/index.js');
|
||||
await main();
|
||||
} catch (esmError) {
|
||||
console.error('Failed to load RuvBot CLI');
|
||||
console.error('CJS Error:', cjsError.message);
|
||||
console.error('ESM Error:', esmError.message);
|
||||
console.error('\nTry running: npm run build');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run().catch((error) => {
|
||||
console.error('Fatal error:', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
125
vendor/ruvector/npm/packages/ruvbot/deploy/gcp/cloudbuild.yaml
vendored
Normal file
125
vendor/ruvector/npm/packages/ruvbot/deploy/gcp/cloudbuild.yaml
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
# =============================================================================
|
||||
# RuvBot - Google Cloud Build CI/CD Pipeline
|
||||
# =============================================================================
|
||||
# Trigger: Push to main branch or tag
|
||||
# Deploy to: Cloud Run (serverless)
|
||||
#
|
||||
# Cost optimization:
|
||||
# - Uses e2-standard-2 machine for builds
|
||||
# - Caches npm dependencies
|
||||
# - Multi-stage Docker builds
|
||||
# =============================================================================
|
||||
|
||||
steps:
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 1: Build and Push Docker Image
|
||||
# ---------------------------------------------------------------------------
|
||||
- name: 'gcr.io/cloud-builders/docker'
|
||||
id: 'build-image'
|
||||
args:
|
||||
- 'build'
|
||||
- '-t'
|
||||
- 'gcr.io/$PROJECT_ID/ruvbot:$COMMIT_SHA'
|
||||
- '-t'
|
||||
- 'gcr.io/$PROJECT_ID/ruvbot:latest'
|
||||
- '-f'
|
||||
- 'Dockerfile'
|
||||
- '--cache-from'
|
||||
- 'gcr.io/$PROJECT_ID/ruvbot:latest'
|
||||
- '.'
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 2: Push to Container Registry
|
||||
# ---------------------------------------------------------------------------
|
||||
- name: 'gcr.io/cloud-builders/docker'
|
||||
id: 'push-image'
|
||||
args:
|
||||
- 'push'
|
||||
- '--all-tags'
|
||||
- 'gcr.io/$PROJECT_ID/ruvbot'
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 3: Deploy to Cloud Run
|
||||
# ---------------------------------------------------------------------------
|
||||
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
|
||||
id: 'deploy-cloud-run'
|
||||
entrypoint: 'gcloud'
|
||||
args:
|
||||
- 'run'
|
||||
- 'deploy'
|
||||
- 'ruvbot'
|
||||
- '--image'
|
||||
- 'gcr.io/$PROJECT_ID/ruvbot:$COMMIT_SHA'
|
||||
- '--region'
|
||||
- '${_REGION}'
|
||||
- '--platform'
|
||||
- 'managed'
|
||||
- '--allow-unauthenticated'
|
||||
- '--port'
|
||||
- '8080'
|
||||
- '--memory'
|
||||
- '512Mi'
|
||||
- '--cpu'
|
||||
- '1'
|
||||
- '--min-instances'
|
||||
- '0'
|
||||
- '--max-instances'
|
||||
- '10'
|
||||
- '--timeout'
|
||||
- '300'
|
||||
- '--concurrency'
|
||||
- '80'
|
||||
- '--set-env-vars'
|
||||
- 'NODE_ENV=production'
|
||||
- '--set-secrets'
|
||||
- 'ANTHROPIC_API_KEY=anthropic-api-key:latest,OPENROUTER_API_KEY=openrouter-api-key:latest,DATABASE_URL=database-url:latest'
|
||||
- '--service-account'
|
||||
- 'ruvbot-runner@$PROJECT_ID.iam.gserviceaccount.com'
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 4: Run Database Migrations (if needed)
|
||||
# ---------------------------------------------------------------------------
|
||||
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
|
||||
id: 'run-migrations'
|
||||
entrypoint: 'bash'
|
||||
args:
|
||||
- '-c'
|
||||
- |
|
||||
gcloud run jobs execute ruvbot-migrations \
|
||||
--region ${_REGION} \
|
||||
--wait \
|
||||
|| echo "No migrations job configured, skipping"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Substitutions (can be overridden)
|
||||
# ---------------------------------------------------------------------------
|
||||
substitutions:
|
||||
_REGION: 'us-central1'
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Build Options
|
||||
# ---------------------------------------------------------------------------
|
||||
options:
|
||||
logging: CLOUD_LOGGING_ONLY
|
||||
machineType: 'E2_HIGHCPU_8'
|
||||
dynamicSubstitutions: true
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Images to push
|
||||
# ---------------------------------------------------------------------------
|
||||
images:
|
||||
- 'gcr.io/$PROJECT_ID/ruvbot:$COMMIT_SHA'
|
||||
- 'gcr.io/$PROJECT_ID/ruvbot:latest'
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Timeout
|
||||
# ---------------------------------------------------------------------------
|
||||
timeout: '1200s'
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tags for organization
|
||||
# ---------------------------------------------------------------------------
|
||||
tags:
|
||||
- 'ruvbot'
|
||||
- 'cloud-run'
|
||||
- 'production'
|
||||
263
vendor/ruvector/npm/packages/ruvbot/deploy/gcp/deploy.sh
vendored
Executable file
263
vendor/ruvector/npm/packages/ruvbot/deploy/gcp/deploy.sh
vendored
Executable file
@@ -0,0 +1,263 @@
|
||||
#!/bin/bash
|
||||
# =============================================================================
|
||||
# RuvBot - Google Cloud Platform Deployment Script
|
||||
# =============================================================================
|
||||
# Quick deployment for RuvBot to Google Cloud Run
|
||||
#
|
||||
# Usage:
|
||||
# ./deploy.sh [options]
|
||||
#
|
||||
# Options:
|
||||
# --project-id ID GCP Project ID (required)
|
||||
# --region REGION GCP Region (default: us-central1)
|
||||
# --env ENV Environment: dev, staging, prod (default: prod)
|
||||
# --no-sql Skip Cloud SQL setup (use in-memory)
|
||||
# --terraform Use Terraform instead of gcloud
|
||||
# --destroy Destroy all resources
|
||||
#
|
||||
# Environment Variables:
|
||||
# ANTHROPIC_API_KEY Required: Anthropic API key
|
||||
# OPENROUTER_API_KEY Optional: OpenRouter API key
|
||||
# =============================================================================
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Defaults
|
||||
REGION="us-central1"
|
||||
ENVIRONMENT="prod"
|
||||
USE_TERRAFORM=false
|
||||
ENABLE_SQL=true
|
||||
DESTROY=false
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--project-id)
|
||||
PROJECT_ID="$2"
|
||||
shift 2
|
||||
;;
|
||||
--region)
|
||||
REGION="$2"
|
||||
shift 2
|
||||
;;
|
||||
--env)
|
||||
ENVIRONMENT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--no-sql)
|
||||
ENABLE_SQL=false
|
||||
shift
|
||||
;;
|
||||
--terraform)
|
||||
USE_TERRAFORM=true
|
||||
shift
|
||||
;;
|
||||
--destroy)
|
||||
DESTROY=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Unknown option: $1${NC}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate required variables
|
||||
if [ -z "$PROJECT_ID" ]; then
|
||||
echo -e "${RED}Error: --project-id is required${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$ANTHROPIC_API_KEY" ]; then
|
||||
echo -e "${RED}Error: ANTHROPIC_API_KEY environment variable is required${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}================================================${NC}"
|
||||
echo -e "${BLUE} RuvBot GCP Deployment${NC}"
|
||||
echo -e "${BLUE}================================================${NC}"
|
||||
echo -e "Project: ${GREEN}$PROJECT_ID${NC}"
|
||||
echo -e "Region: ${GREEN}$REGION${NC}"
|
||||
echo -e "Environment: ${GREEN}$ENVIRONMENT${NC}"
|
||||
echo -e "Cloud SQL: ${GREEN}$ENABLE_SQL${NC}"
|
||||
echo -e "Method: ${GREEN}$([ "$USE_TERRAFORM" = true ] && echo "Terraform" || echo "gcloud")${NC}"
|
||||
echo -e "${BLUE}================================================${NC}"
|
||||
|
||||
# Confirm deployment
|
||||
read -p "Continue with deployment? (y/N) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Deployment cancelled."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Terraform Deployment
|
||||
# -----------------------------------------------------------------------------
|
||||
if [ "$USE_TERRAFORM" = true ]; then
|
||||
cd "$(dirname "$0")/terraform"
|
||||
|
||||
if [ "$DESTROY" = true ]; then
|
||||
echo -e "${YELLOW}Destroying infrastructure...${NC}"
|
||||
terraform destroy \
|
||||
-var="project_id=$PROJECT_ID" \
|
||||
-var="region=$REGION" \
|
||||
-var="environment=$ENVIRONMENT" \
|
||||
-var="enable_cloud_sql=$ENABLE_SQL" \
|
||||
-var="anthropic_api_key=$ANTHROPIC_API_KEY" \
|
||||
-var="openrouter_api_key=${OPENROUTER_API_KEY:-}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}Initializing Terraform...${NC}"
|
||||
terraform init
|
||||
|
||||
echo -e "${YELLOW}Planning deployment...${NC}"
|
||||
terraform plan \
|
||||
-var="project_id=$PROJECT_ID" \
|
||||
-var="region=$REGION" \
|
||||
-var="environment=$ENVIRONMENT" \
|
||||
-var="enable_cloud_sql=$ENABLE_SQL" \
|
||||
-var="anthropic_api_key=$ANTHROPIC_API_KEY" \
|
||||
-var="openrouter_api_key=${OPENROUTER_API_KEY:-}" \
|
||||
-out=tfplan
|
||||
|
||||
echo -e "${YELLOW}Applying deployment...${NC}"
|
||||
terraform apply tfplan
|
||||
|
||||
echo -e "${GREEN}Deployment complete!${NC}"
|
||||
terraform output
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# gcloud Deployment
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
echo -e "${YELLOW}Setting project...${NC}"
|
||||
gcloud config set project "$PROJECT_ID"
|
||||
|
||||
echo -e "${YELLOW}Enabling APIs...${NC}"
|
||||
gcloud services enable \
|
||||
run.googleapis.com \
|
||||
cloudbuild.googleapis.com \
|
||||
secretmanager.googleapis.com \
|
||||
sqladmin.googleapis.com \
|
||||
storage.googleapis.com
|
||||
|
||||
# Create secrets
|
||||
echo -e "${YELLOW}Creating secrets...${NC}"
|
||||
echo -n "$ANTHROPIC_API_KEY" | gcloud secrets create anthropic-api-key \
|
||||
--data-file=- --replication-policy=automatic 2>/dev/null || \
|
||||
echo -n "$ANTHROPIC_API_KEY" | gcloud secrets versions add anthropic-api-key --data-file=-
|
||||
|
||||
if [ -n "$OPENROUTER_API_KEY" ]; then
|
||||
echo -n "$OPENROUTER_API_KEY" | gcloud secrets create openrouter-api-key \
|
||||
--data-file=- --replication-policy=automatic 2>/dev/null || \
|
||||
echo -n "$OPENROUTER_API_KEY" | gcloud secrets versions add openrouter-api-key --data-file=-
|
||||
fi
|
||||
|
||||
# Create service account
|
||||
echo -e "${YELLOW}Creating service account...${NC}"
|
||||
gcloud iam service-accounts create ruvbot-runner \
|
||||
--display-name="RuvBot Cloud Run" 2>/dev/null || true
|
||||
|
||||
SA_EMAIL="ruvbot-runner@$PROJECT_ID.iam.gserviceaccount.com"
|
||||
|
||||
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
|
||||
--member="serviceAccount:$SA_EMAIL" \
|
||||
--role="roles/secretmanager.secretAccessor" --quiet
|
||||
|
||||
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
|
||||
--member="serviceAccount:$SA_EMAIL" \
|
||||
--role="roles/cloudsql.client" --quiet
|
||||
|
||||
# Create Cloud SQL (if enabled)
|
||||
if [ "$ENABLE_SQL" = true ]; then
|
||||
echo -e "${YELLOW}Creating Cloud SQL instance...${NC}"
|
||||
|
||||
if ! gcloud sql instances describe "ruvbot-$ENVIRONMENT" --quiet 2>/dev/null; then
|
||||
gcloud sql instances create "ruvbot-$ENVIRONMENT" \
|
||||
--database-version=POSTGRES_16 \
|
||||
--tier=db-f1-micro \
|
||||
--region="$REGION" \
|
||||
--storage-size=10GB \
|
||||
--storage-auto-increase \
|
||||
--availability-type=zonal
|
||||
|
||||
# Generate password
|
||||
DB_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=' | head -c 24)
|
||||
|
||||
# Create database and user
|
||||
gcloud sql databases create ruvbot --instance="ruvbot-$ENVIRONMENT"
|
||||
gcloud sql users create ruvbot --instance="ruvbot-$ENVIRONMENT" --password="$DB_PASSWORD"
|
||||
|
||||
# Get instance IP
|
||||
INSTANCE_IP=$(gcloud sql instances describe "ruvbot-$ENVIRONMENT" --format='get(ipAddresses[0].ipAddress)')
|
||||
|
||||
# Store connection string in secrets
|
||||
DATABASE_URL="postgresql://ruvbot:$DB_PASSWORD@$INSTANCE_IP:5432/ruvbot"
|
||||
echo -n "$DATABASE_URL" | gcloud secrets create database-url \
|
||||
--data-file=- --replication-policy=automatic 2>/dev/null || \
|
||||
echo -n "$DATABASE_URL" | gcloud secrets versions add database-url --data-file=-
|
||||
fi
|
||||
fi
|
||||
|
||||
# Build and push image
|
||||
echo -e "${YELLOW}Building container image...${NC}"
|
||||
cd "$(dirname "$0")/../.."
|
||||
gcloud builds submit --tag "gcr.io/$PROJECT_ID/ruvbot:latest" .
|
||||
|
||||
# Deploy to Cloud Run
|
||||
echo -e "${YELLOW}Deploying to Cloud Run...${NC}"
|
||||
SECRETS="ANTHROPIC_API_KEY=anthropic-api-key:latest"
|
||||
if [ -n "$OPENROUTER_API_KEY" ]; then
|
||||
SECRETS="$SECRETS,OPENROUTER_API_KEY=openrouter-api-key:latest"
|
||||
fi
|
||||
if [ "$ENABLE_SQL" = true ]; then
|
||||
SECRETS="$SECRETS,DATABASE_URL=database-url:latest"
|
||||
fi
|
||||
|
||||
gcloud run deploy ruvbot \
|
||||
--image="gcr.io/$PROJECT_ID/ruvbot:latest" \
|
||||
--region="$REGION" \
|
||||
--platform=managed \
|
||||
--allow-unauthenticated \
|
||||
--port=8080 \
|
||||
--memory=512Mi \
|
||||
--cpu=1 \
|
||||
--min-instances=0 \
|
||||
--max-instances=10 \
|
||||
--timeout=300 \
|
||||
--concurrency=80 \
|
||||
--set-env-vars="NODE_ENV=production" \
|
||||
--set-secrets="$SECRETS" \
|
||||
--service-account="$SA_EMAIL"
|
||||
|
||||
# Get URL
|
||||
SERVICE_URL=$(gcloud run services describe ruvbot --region="$REGION" --format='get(status.url)')
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}================================================${NC}"
|
||||
echo -e "${GREEN} Deployment Complete!${NC}"
|
||||
echo -e "${GREEN}================================================${NC}"
|
||||
echo -e "Service URL: ${BLUE}$SERVICE_URL${NC}"
|
||||
echo -e "Health Check: ${BLUE}$SERVICE_URL/health${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Estimated Monthly Cost:${NC}"
|
||||
echo " - Cloud Run: ~\$0 (free tier)"
|
||||
if [ "$ENABLE_SQL" = true ]; then
|
||||
echo " - Cloud SQL: ~\$10-15/month"
|
||||
fi
|
||||
echo " - Secrets: ~\$0.18/month"
|
||||
echo " - Total: ~\$$([ "$ENABLE_SQL" = true ] && echo "15-20" || echo "5")/month"
|
||||
echo ""
|
||||
echo -e "${GREEN}Done!${NC}"
|
||||
443
vendor/ruvector/npm/packages/ruvbot/deploy/gcp/terraform/main.tf
vendored
Normal file
443
vendor/ruvector/npm/packages/ruvbot/deploy/gcp/terraform/main.tf
vendored
Normal file
@@ -0,0 +1,443 @@
|
||||
# =============================================================================
|
||||
# RuvBot - Google Cloud Platform Infrastructure
|
||||
# =============================================================================
|
||||
# Cost-optimized deployment using:
|
||||
# - Cloud Run (serverless, pay-per-use)
|
||||
# - Cloud SQL PostgreSQL (smallest instance, can scale)
|
||||
# - Memorystore Redis (optional, can use in-memory)
|
||||
# - Secret Manager (for credentials)
|
||||
# - Cloud Storage (for file uploads)
|
||||
#
|
||||
# Estimated monthly cost (low traffic): $15-30/month
|
||||
# - Cloud Run: ~$0 (generous free tier)
|
||||
# - Cloud SQL: ~$10-15/month (db-f1-micro)
|
||||
# - Secret Manager: ~$0.06/secret/month
|
||||
# - Cloud Storage: ~$0.02/GB/month
|
||||
# =============================================================================
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.5.0"
|
||||
|
||||
required_providers {
|
||||
google = {
|
||||
source = "hashicorp/google"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
google-beta = {
|
||||
source = "hashicorp/google-beta"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
|
||||
# Uncomment for remote state (recommended for production)
|
||||
# backend "gcs" {
|
||||
# bucket = "your-terraform-state-bucket"
|
||||
# prefix = "ruvbot/state"
|
||||
# }
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Variables
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
variable "project_id" {
|
||||
description = "GCP Project ID"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "region" {
|
||||
description = "GCP Region"
|
||||
type = string
|
||||
default = "us-central1"
|
||||
}
|
||||
|
||||
variable "environment" {
|
||||
description = "Environment (dev, staging, prod)"
|
||||
type = string
|
||||
default = "prod"
|
||||
}
|
||||
|
||||
variable "enable_cloud_sql" {
|
||||
description = "Enable Cloud SQL PostgreSQL (adds ~$10/month)"
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "enable_redis" {
|
||||
description = "Enable Memorystore Redis (adds ~$30/month)"
|
||||
type = bool
|
||||
default = false # Disabled by default for cost savings
|
||||
}
|
||||
|
||||
variable "anthropic_api_key" {
|
||||
description = "Anthropic API Key"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "openrouter_api_key" {
|
||||
description = "OpenRouter API Key"
|
||||
type = string
|
||||
sensitive = true
|
||||
default = ""
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Provider Configuration
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
provider "google" {
|
||||
project = var.project_id
|
||||
region = var.region
|
||||
}
|
||||
|
||||
provider "google-beta" {
|
||||
project = var.project_id
|
||||
region = var.region
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Enable Required APIs
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
resource "google_project_service" "services" {
|
||||
for_each = toset([
|
||||
"run.googleapis.com",
|
||||
"cloudbuild.googleapis.com",
|
||||
"secretmanager.googleapis.com",
|
||||
"sqladmin.googleapis.com",
|
||||
"storage.googleapis.com",
|
||||
"redis.googleapis.com",
|
||||
"vpcaccess.googleapis.com",
|
||||
])
|
||||
|
||||
service = each.value
|
||||
disable_on_destroy = false
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Service Account for Cloud Run
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
resource "google_service_account" "ruvbot_runner" {
|
||||
account_id = "ruvbot-runner"
|
||||
display_name = "RuvBot Cloud Run Service Account"
|
||||
description = "Service account for RuvBot Cloud Run service"
|
||||
}
|
||||
|
||||
resource "google_project_iam_member" "ruvbot_runner_roles" {
|
||||
for_each = toset([
|
||||
"roles/secretmanager.secretAccessor",
|
||||
"roles/cloudsql.client",
|
||||
"roles/storage.objectAdmin",
|
||||
"roles/logging.logWriter",
|
||||
"roles/monitoring.metricWriter",
|
||||
])
|
||||
|
||||
project = var.project_id
|
||||
role = each.value
|
||||
member = "serviceAccount:${google_service_account.ruvbot_runner.email}"
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Secret Manager - Store API Keys Securely
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
resource "google_secret_manager_secret" "anthropic_api_key" {
|
||||
secret_id = "anthropic-api-key"
|
||||
|
||||
replication {
|
||||
auto {}
|
||||
}
|
||||
|
||||
depends_on = [google_project_service.services]
|
||||
}
|
||||
|
||||
resource "google_secret_manager_secret_version" "anthropic_api_key" {
|
||||
secret = google_secret_manager_secret.anthropic_api_key.id
|
||||
secret_data = var.anthropic_api_key
|
||||
}
|
||||
|
||||
resource "google_secret_manager_secret" "openrouter_api_key" {
|
||||
count = var.openrouter_api_key != "" ? 1 : 0
|
||||
secret_id = "openrouter-api-key"
|
||||
|
||||
replication {
|
||||
auto {}
|
||||
}
|
||||
|
||||
depends_on = [google_project_service.services]
|
||||
}
|
||||
|
||||
resource "google_secret_manager_secret_version" "openrouter_api_key" {
|
||||
count = var.openrouter_api_key != "" ? 1 : 0
|
||||
secret = google_secret_manager_secret.openrouter_api_key[0].id
|
||||
secret_data = var.openrouter_api_key
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Cloud SQL PostgreSQL (Cost-Optimized)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
resource "google_sql_database_instance" "ruvbot" {
|
||||
count = var.enable_cloud_sql ? 1 : 0
|
||||
name = "ruvbot-${var.environment}"
|
||||
database_version = "POSTGRES_16"
|
||||
region = var.region
|
||||
|
||||
settings {
|
||||
# db-f1-micro: 0.6GB RAM, shared CPU - ~$10/month
|
||||
tier = "db-f1-micro"
|
||||
availability_type = "ZONAL" # Single zone for cost savings
|
||||
|
||||
disk_size = 10 # Minimum 10GB
|
||||
disk_type = "PD_SSD"
|
||||
disk_autoresize = true
|
||||
|
||||
backup_configuration {
|
||||
enabled = true
|
||||
point_in_time_recovery_enabled = false # Disable for cost savings
|
||||
backup_retention_settings {
|
||||
retained_backups = 7
|
||||
}
|
||||
}
|
||||
|
||||
ip_configuration {
|
||||
ipv4_enabled = true
|
||||
# For production, use private IP with VPC connector
|
||||
authorized_networks {
|
||||
name = "allow-cloud-run"
|
||||
value = "0.0.0.0/0" # Cloud Run uses public IP; restrict in production
|
||||
}
|
||||
}
|
||||
|
||||
database_flags {
|
||||
name = "max_connections"
|
||||
value = "50"
|
||||
}
|
||||
}
|
||||
|
||||
deletion_protection = var.environment == "prod"
|
||||
|
||||
depends_on = [google_project_service.services]
|
||||
}
|
||||
|
||||
resource "google_sql_database" "ruvbot" {
|
||||
count = var.enable_cloud_sql ? 1 : 0
|
||||
name = "ruvbot"
|
||||
instance = google_sql_database_instance.ruvbot[0].name
|
||||
}
|
||||
|
||||
resource "google_sql_user" "ruvbot" {
|
||||
count = var.enable_cloud_sql ? 1 : 0
|
||||
name = "ruvbot"
|
||||
instance = google_sql_database_instance.ruvbot[0].name
|
||||
password = random_password.db_password[0].result
|
||||
}
|
||||
|
||||
resource "random_password" "db_password" {
|
||||
count = var.enable_cloud_sql ? 1 : 0
|
||||
length = 32
|
||||
special = false
|
||||
}
|
||||
|
||||
resource "google_secret_manager_secret" "database_url" {
|
||||
count = var.enable_cloud_sql ? 1 : 0
|
||||
secret_id = "database-url"
|
||||
|
||||
replication {
|
||||
auto {}
|
||||
}
|
||||
|
||||
depends_on = [google_project_service.services]
|
||||
}
|
||||
|
||||
resource "google_secret_manager_secret_version" "database_url" {
|
||||
count = var.enable_cloud_sql ? 1 : 0
|
||||
secret = google_secret_manager_secret.database_url[0].id
|
||||
secret_data = "postgresql://ruvbot:${random_password.db_password[0].result}@${google_sql_database_instance.ruvbot[0].public_ip_address}:5432/ruvbot"
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Cloud Storage Bucket (for file uploads)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
resource "google_storage_bucket" "ruvbot_data" {
|
||||
name = "${var.project_id}-ruvbot-data"
|
||||
location = var.region
|
||||
force_destroy = var.environment != "prod"
|
||||
|
||||
uniform_bucket_level_access = true
|
||||
|
||||
lifecycle_rule {
|
||||
condition {
|
||||
age = 30
|
||||
}
|
||||
action {
|
||||
type = "SetStorageClass"
|
||||
storage_class = "NEARLINE"
|
||||
}
|
||||
}
|
||||
|
||||
lifecycle_rule {
|
||||
condition {
|
||||
age = 90
|
||||
}
|
||||
action {
|
||||
type = "SetStorageClass"
|
||||
storage_class = "COLDLINE"
|
||||
}
|
||||
}
|
||||
|
||||
versioning {
|
||||
enabled = var.environment == "prod"
|
||||
}
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Cloud Run Service
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
resource "google_cloud_run_v2_service" "ruvbot" {
|
||||
name = "ruvbot"
|
||||
location = var.region
|
||||
ingress = "INGRESS_TRAFFIC_ALL"
|
||||
|
||||
template {
|
||||
service_account = google_service_account.ruvbot_runner.email
|
||||
|
||||
scaling {
|
||||
min_instance_count = 0 # Scale to zero when not in use
|
||||
max_instance_count = 10
|
||||
}
|
||||
|
||||
containers {
|
||||
image = "gcr.io/${var.project_id}/ruvbot:latest"
|
||||
|
||||
ports {
|
||||
container_port = 8080
|
||||
}
|
||||
|
||||
resources {
|
||||
limits = {
|
||||
cpu = "1"
|
||||
memory = "512Mi"
|
||||
}
|
||||
cpu_idle = true # Reduce cost during idle
|
||||
startup_cpu_boost = true # Faster cold starts
|
||||
}
|
||||
|
||||
env {
|
||||
name = "NODE_ENV"
|
||||
value = "production"
|
||||
}
|
||||
|
||||
env {
|
||||
name = "GCS_BUCKET"
|
||||
value = google_storage_bucket.ruvbot_data.name
|
||||
}
|
||||
|
||||
env {
|
||||
name = "ANTHROPIC_API_KEY"
|
||||
value_source {
|
||||
secret_key_ref {
|
||||
secret = google_secret_manager_secret.anthropic_api_key.secret_id
|
||||
version = "latest"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dynamic "env" {
|
||||
for_each = var.enable_cloud_sql ? [1] : []
|
||||
content {
|
||||
name = "DATABASE_URL"
|
||||
value_source {
|
||||
secret_key_ref {
|
||||
secret = google_secret_manager_secret.database_url[0].secret_id
|
||||
version = "latest"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startup_probe {
|
||||
http_get {
|
||||
path = "/health"
|
||||
port = 8080
|
||||
}
|
||||
initial_delay_seconds = 5
|
||||
timeout_seconds = 3
|
||||
period_seconds = 10
|
||||
failure_threshold = 3
|
||||
}
|
||||
|
||||
liveness_probe {
|
||||
http_get {
|
||||
path = "/health"
|
||||
port = 8080
|
||||
}
|
||||
timeout_seconds = 3
|
||||
period_seconds = 30
|
||||
failure_threshold = 3
|
||||
}
|
||||
}
|
||||
|
||||
max_instance_request_concurrency = 80
|
||||
timeout = "300s"
|
||||
}
|
||||
|
||||
traffic {
|
||||
type = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST"
|
||||
percent = 100
|
||||
}
|
||||
|
||||
depends_on = [
|
||||
google_project_service.services,
|
||||
google_secret_manager_secret_version.anthropic_api_key,
|
||||
]
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Allow Unauthenticated Access to Cloud Run
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
resource "google_cloud_run_v2_service_iam_member" "public_access" {
|
||||
project = var.project_id
|
||||
location = var.region
|
||||
name = google_cloud_run_v2_service.ruvbot.name
|
||||
role = "roles/run.invoker"
|
||||
member = "allUsers"
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Outputs
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
output "cloud_run_url" {
|
||||
description = "Cloud Run service URL"
|
||||
value = google_cloud_run_v2_service.ruvbot.uri
|
||||
}
|
||||
|
||||
output "cloud_sql_connection_name" {
|
||||
description = "Cloud SQL connection name"
|
||||
value = var.enable_cloud_sql ? google_sql_database_instance.ruvbot[0].connection_name : "N/A"
|
||||
}
|
||||
|
||||
output "storage_bucket" {
|
||||
description = "Cloud Storage bucket name"
|
||||
value = google_storage_bucket.ruvbot_data.name
|
||||
}
|
||||
|
||||
output "estimated_monthly_cost" {
|
||||
description = "Estimated monthly cost"
|
||||
value = <<-EOT
|
||||
Estimated Monthly Cost (low traffic):
|
||||
- Cloud Run: ~$0 (free tier covers ~2M requests)
|
||||
- Cloud SQL: ${var.enable_cloud_sql ? "~$10-15" : "$0"}
|
||||
- Secret Manager: ~$0.18 (3 secrets)
|
||||
- Cloud Storage: ~$0.02/GB
|
||||
- Redis: ${var.enable_redis ? "~$30" : "$0 (disabled)"}
|
||||
----------------------------------------
|
||||
Total: ~$${var.enable_cloud_sql ? (var.enable_redis ? "45-50" : "15-20") : "5-10"}/month
|
||||
EOT
|
||||
}
|
||||
310
vendor/ruvector/npm/packages/ruvbot/deploy/init-db.sql
vendored
Normal file
310
vendor/ruvector/npm/packages/ruvbot/deploy/init-db.sql
vendored
Normal file
@@ -0,0 +1,310 @@
|
||||
-- =============================================================================
|
||||
-- RuvBot - Database Initialization Script
|
||||
-- =============================================================================
|
||||
-- PostgreSQL schema for multi-tenant RuvBot deployment
|
||||
-- Supports: Sessions, Memory, Skills, Events, Metrics
|
||||
-- =============================================================================
|
||||
|
||||
-- Enable required extensions
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- For text search
|
||||
|
||||
-- =============================================================================
|
||||
-- Tenants (Multi-tenancy support)
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tenants (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
slug VARCHAR(100) UNIQUE NOT NULL,
|
||||
settings JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Enable Row-Level Security
|
||||
ALTER TABLE tenants ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- =============================================================================
|
||||
-- Users
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
external_id VARCHAR(255), -- Slack user ID, Discord ID, etc.
|
||||
username VARCHAR(255),
|
||||
email VARCHAR(255),
|
||||
avatar_url TEXT,
|
||||
metadata JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(tenant_id, external_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_users_tenant ON users(tenant_id);
|
||||
CREATE INDEX idx_users_external ON users(external_id);
|
||||
|
||||
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- =============================================================================
|
||||
-- Agents
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS agents (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
type VARCHAR(50) DEFAULT 'assistant',
|
||||
model VARCHAR(100) DEFAULT 'claude-3-5-sonnet',
|
||||
system_prompt TEXT,
|
||||
config JSONB DEFAULT '{}',
|
||||
status VARCHAR(20) DEFAULT 'active',
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_agents_tenant ON agents(tenant_id);
|
||||
|
||||
ALTER TABLE agents ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- =============================================================================
|
||||
-- Sessions
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
agent_id UUID REFERENCES agents(id) ON DELETE CASCADE,
|
||||
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
channel_type VARCHAR(50), -- slack, discord, telegram, web
|
||||
channel_id VARCHAR(255), -- Channel/room ID
|
||||
thread_id VARCHAR(255), -- Thread ID if applicable
|
||||
status VARCHAR(20) DEFAULT 'active',
|
||||
metadata JSONB DEFAULT '{}',
|
||||
started_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
ended_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_sessions_tenant ON sessions(tenant_id);
|
||||
CREATE INDEX idx_sessions_agent ON sessions(agent_id);
|
||||
CREATE INDEX idx_sessions_user ON sessions(user_id);
|
||||
CREATE INDEX idx_sessions_channel ON sessions(channel_type, channel_id);
|
||||
CREATE INDEX idx_sessions_status ON sessions(status) WHERE status = 'active';
|
||||
|
||||
ALTER TABLE sessions ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- =============================================================================
|
||||
-- Turns (Conversation Messages)
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS turns (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
session_id UUID REFERENCES sessions(id) ON DELETE CASCADE,
|
||||
role VARCHAR(20) NOT NULL, -- user, assistant, system
|
||||
content TEXT NOT NULL,
|
||||
tokens_input INTEGER DEFAULT 0,
|
||||
tokens_output INTEGER DEFAULT 0,
|
||||
latency_ms INTEGER,
|
||||
tool_calls JSONB,
|
||||
metadata JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_turns_session ON turns(session_id);
|
||||
CREATE INDEX idx_turns_created ON turns(created_at DESC);
|
||||
|
||||
-- =============================================================================
|
||||
-- Memory (Long-term storage with vector search)
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS memories (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||
type VARCHAR(50) NOT NULL, -- fact, preference, episode, semantic
|
||||
content TEXT NOT NULL,
|
||||
embedding FLOAT8[], -- Vector embedding (1536 dimensions for OpenAI, 768 for local)
|
||||
importance FLOAT DEFAULT 0.5,
|
||||
access_count INTEGER DEFAULT 0,
|
||||
last_accessed TIMESTAMPTZ,
|
||||
expires_at TIMESTAMPTZ,
|
||||
metadata JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_memories_tenant ON memories(tenant_id);
|
||||
CREATE INDEX idx_memories_user ON memories(user_id);
|
||||
CREATE INDEX idx_memories_type ON memories(type);
|
||||
CREATE INDEX idx_memories_importance ON memories(importance DESC);
|
||||
|
||||
-- Full-text search index
|
||||
CREATE INDEX idx_memories_content_trgm ON memories USING gin(content gin_trgm_ops);
|
||||
|
||||
ALTER TABLE memories ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- =============================================================================
|
||||
-- Skills
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS skills (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
category VARCHAR(100),
|
||||
version VARCHAR(20) DEFAULT '1.0.0',
|
||||
schema JSONB NOT NULL, -- Input/output schema
|
||||
implementation JSONB, -- Skill configuration
|
||||
enabled BOOLEAN DEFAULT true,
|
||||
usage_count INTEGER DEFAULT 0,
|
||||
success_rate FLOAT DEFAULT 1.0,
|
||||
metadata JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(tenant_id, name, version)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_skills_tenant ON skills(tenant_id);
|
||||
CREATE INDEX idx_skills_category ON skills(category);
|
||||
CREATE INDEX idx_skills_enabled ON skills(enabled) WHERE enabled = true;
|
||||
|
||||
ALTER TABLE skills ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- =============================================================================
|
||||
-- Events (Event Sourcing)
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
aggregate_type VARCHAR(100) NOT NULL,
|
||||
aggregate_id UUID NOT NULL,
|
||||
event_type VARCHAR(100) NOT NULL,
|
||||
event_version INTEGER DEFAULT 1,
|
||||
payload JSONB NOT NULL,
|
||||
metadata JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_events_aggregate ON events(aggregate_type, aggregate_id);
|
||||
CREATE INDEX idx_events_type ON events(event_type);
|
||||
CREATE INDEX idx_events_created ON events(created_at DESC);
|
||||
|
||||
-- =============================================================================
|
||||
-- Metrics (Usage tracking)
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS metrics (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
metric_name VARCHAR(100) NOT NULL,
|
||||
metric_value FLOAT NOT NULL,
|
||||
dimensions JSONB DEFAULT '{}',
|
||||
recorded_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_metrics_tenant ON metrics(tenant_id);
|
||||
CREATE INDEX idx_metrics_name ON metrics(metric_name);
|
||||
CREATE INDEX idx_metrics_recorded ON metrics(recorded_at DESC);
|
||||
|
||||
-- Partition by time for better performance
|
||||
-- (In production, consider using TimescaleDB or partitioning)
|
||||
|
||||
-- =============================================================================
|
||||
-- Patterns (Learning patterns from interactions)
|
||||
-- =============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS patterns (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
pattern_type VARCHAR(50) NOT NULL, -- intent, response, workflow
|
||||
pattern_key VARCHAR(255) NOT NULL,
|
||||
pattern_value JSONB NOT NULL,
|
||||
confidence FLOAT DEFAULT 0.5,
|
||||
usage_count INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(tenant_id, pattern_type, pattern_key)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_patterns_tenant ON patterns(tenant_id);
|
||||
CREATE INDEX idx_patterns_type ON patterns(pattern_type);
|
||||
CREATE INDEX idx_patterns_confidence ON patterns(confidence DESC);
|
||||
|
||||
ALTER TABLE patterns ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- =============================================================================
|
||||
-- Functions
|
||||
-- =============================================================================
|
||||
|
||||
-- Update timestamp trigger
|
||||
CREATE OR REPLACE FUNCTION update_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Apply to all tables with updated_at
|
||||
DO $$
|
||||
DECLARE
|
||||
t text;
|
||||
BEGIN
|
||||
FOR t IN
|
||||
SELECT table_name
|
||||
FROM information_schema.columns
|
||||
WHERE column_name = 'updated_at'
|
||||
AND table_schema = 'public'
|
||||
LOOP
|
||||
EXECUTE format('
|
||||
DROP TRIGGER IF EXISTS update_%I_updated_at ON %I;
|
||||
CREATE TRIGGER update_%I_updated_at
|
||||
BEFORE UPDATE ON %I
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at();
|
||||
', t, t, t, t);
|
||||
END LOOP;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- =============================================================================
|
||||
-- Default Data
|
||||
-- =============================================================================
|
||||
|
||||
-- Default tenant for development
|
||||
INSERT INTO tenants (id, name, slug, settings)
|
||||
VALUES (
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'Default Tenant',
|
||||
'default',
|
||||
'{"plan": "free", "features": ["basic_chat", "memory", "skills"]}'
|
||||
)
|
||||
ON CONFLICT (slug) DO NOTHING;
|
||||
|
||||
-- Default agent
|
||||
INSERT INTO agents (id, tenant_id, name, type, model, system_prompt, config)
|
||||
VALUES (
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'00000000-0000-0000-0000-000000000001',
|
||||
'RuvBot',
|
||||
'assistant',
|
||||
'claude-3-5-sonnet',
|
||||
'You are RuvBot, a helpful AI assistant with long-term memory and learning capabilities.',
|
||||
'{"temperature": 0.7, "maxTokens": 4096}'
|
||||
)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- =============================================================================
|
||||
-- Grants (for application user)
|
||||
-- =============================================================================
|
||||
|
||||
-- Note: Run these after creating the application user
|
||||
-- GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ruvbot;
|
||||
-- GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ruvbot;
|
||||
-- GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO ruvbot;
|
||||
118
vendor/ruvector/npm/packages/ruvbot/docker-compose.yml
vendored
Normal file
118
vendor/ruvector/npm/packages/ruvbot/docker-compose.yml
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
# =============================================================================
|
||||
# RuvBot - Local Development Docker Compose
|
||||
# =============================================================================
|
||||
# Run: docker-compose up -d
|
||||
#
|
||||
# Services:
|
||||
# - ruvbot: Main application (port 8080)
|
||||
# - postgres: PostgreSQL database (port 5432)
|
||||
# - redis: Redis cache (port 6379)
|
||||
# - adminer: Database admin UI (port 8081)
|
||||
# =============================================================================
|
||||
|
||||
version: '3.9'
|
||||
|
||||
services:
|
||||
# ---------------------------------------------------------------------------
|
||||
# RuvBot Application
|
||||
# ---------------------------------------------------------------------------
|
||||
ruvbot:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
NODE_ENV: development
|
||||
PORT: 8080
|
||||
|
||||
# Database
|
||||
DATABASE_URL: postgresql://ruvbot:ruvbot_dev@postgres:5432/ruvbot
|
||||
|
||||
# Redis
|
||||
REDIS_URL: redis://redis:6379
|
||||
|
||||
# LLM Providers (set in .env)
|
||||
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
|
||||
OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-}
|
||||
|
||||
# Vector Store
|
||||
VECTOR_STORE_TYPE: memory
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL: debug
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PostgreSQL Database
|
||||
# ---------------------------------------------------------------------------
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
POSTGRES_USER: ruvbot
|
||||
POSTGRES_PASSWORD: ruvbot_dev
|
||||
POSTGRES_DB: ruvbot
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./deploy/init-db.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ruvbot -d ruvbot"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Redis Cache
|
||||
# ---------------------------------------------------------------------------
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Database Admin UI (optional)
|
||||
# ---------------------------------------------------------------------------
|
||||
adminer:
|
||||
image: adminer:latest
|
||||
ports:
|
||||
- "8081:8080"
|
||||
environment:
|
||||
ADMINER_DEFAULT_SERVER: postgres
|
||||
depends_on:
|
||||
- postgres
|
||||
restart: unless-stopped
|
||||
profiles:
|
||||
- admin
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: ruvbot-network
|
||||
657
vendor/ruvector/npm/packages/ruvbot/docs/FEATURE_COMPARISON.md
vendored
Normal file
657
vendor/ruvector/npm/packages/ruvbot/docs/FEATURE_COMPARISON.md
vendored
Normal file
@@ -0,0 +1,657 @@
|
||||
# RuvBot vs Clawdbot: Feature Parity & SOTA Comparison
|
||||
|
||||
## Executive Summary
|
||||
|
||||
RuvBot builds on Clawdbot's pioneering personal AI assistant architecture while **fixing critical security vulnerabilities** and introducing **state-of-the-art (SOTA)** improvements through RuVector's WASM-accelerated vector operations, self-learning neural patterns, and enterprise-grade multi-tenancy.
|
||||
|
||||
## Critical Security Gap in Clawdbot
|
||||
|
||||
**Clawdbot should NOT be used in production environments** without significant security hardening:
|
||||
|
||||
| Security Feature | Clawdbot | RuvBot | Risk Level |
|
||||
|-----------------|----------|--------|------------|
|
||||
| Prompt Injection Defense | **MISSING** | Protected | **CRITICAL** |
|
||||
| Jailbreak Detection | **MISSING** | Protected | **CRITICAL** |
|
||||
| PII Data Protection | **MISSING** | Auto-masked | **HIGH** |
|
||||
| Input Sanitization | **MISSING** | Full | **HIGH** |
|
||||
| Multi-tenant Isolation | **MISSING** | PostgreSQL RLS | **HIGH** |
|
||||
| Response Validation | **MISSING** | AIDefence | **MEDIUM** |
|
||||
| Audit Logging | **BASIC** | Comprehensive | **MEDIUM** |
|
||||
|
||||
**RuvBot addresses ALL of these vulnerabilities** with a 6-layer defense-in-depth architecture and integrated AIDefence protection.
|
||||
|
||||
## Feature Comparison Matrix
|
||||
|
||||
| Feature | Clawdbot | RuvBot | RuvBot Advantage |
|
||||
|---------|----------|--------|------------------|
|
||||
| **Security** | Basic | 6-layer + AIDefence | **CRITICAL UPGRADE** |
|
||||
| **Prompt Injection** | **VULNERABLE** | Protected (<5ms) | **Essential** |
|
||||
| **Jailbreak Defense** | **VULNERABLE** | Detected + Blocked | **Essential** |
|
||||
| **PII Protection** | **NONE** | Auto-masked | **Compliance-ready** |
|
||||
| **Vector Memory** | Optional | HNSW-indexed WASM | 150x-12,500x faster search |
|
||||
| **Learning** | Static | SONA adaptive | Self-improving with EWC++ |
|
||||
| **Embeddings** | External API | Local WASM | 75x faster, no network latency |
|
||||
| **Multi-tenancy** | Single-user | Full RLS | Enterprise-ready isolation |
|
||||
| **LLM Models** | Single provider | 12+ (Gemini 2.5, Claude, GPT) | Full flexibility |
|
||||
| **LLM Routing** | Single model | MoE + FastGRNN | 100% routing accuracy |
|
||||
| **Background Tasks** | Basic | agentic-flow workers | 12 specialized worker types |
|
||||
| **Plugin System** | Basic | IPFS registry + sandboxed | claude-flow inspired |
|
||||
|
||||
## Deep Feature Analysis
|
||||
|
||||
### 1. Vector Memory System
|
||||
|
||||
#### Clawdbot
|
||||
- Uses external embedding APIs (OpenAI, etc.)
|
||||
- In-memory or basic database storage
|
||||
- Linear search for retrieval
|
||||
|
||||
#### RuvBot (SOTA)
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ RuvBot Memory Architecture │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ WASM Embedder (384-4096 dim) │
|
||||
│ └─ SIMD-optimized vector operations │
|
||||
│ └─ LRU caching (10K+ entries) │
|
||||
│ └─ Batch processing (32 vectors/batch) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ HNSW Index (RuVector) │
|
||||
│ └─ Hierarchical Navigable Small Worlds │
|
||||
│ └─ O(log n) search complexity │
|
||||
│ └─ 100K-10M vector capacity │
|
||||
│ └─ ef_construction=200, M=16 (tuned) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Memory Types │
|
||||
│ └─ Episodic: Conversation events │
|
||||
│ └─ Semantic: Knowledge/facts │
|
||||
│ └─ Procedural: Skills/patterns │
|
||||
│ └─ Working: Short-term context │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Performance Benchmarks:
|
||||
- 10K vectors: <1ms search (vs 50ms Clawdbot)
|
||||
- 100K vectors: <5ms search (vs 500ms+ Clawdbot)
|
||||
- 1M vectors: <10ms search (not feasible in Clawdbot)
|
||||
```
|
||||
|
||||
### 2. Self-Learning System
|
||||
|
||||
#### Clawdbot
|
||||
- No built-in learning
|
||||
- Static skill definitions
|
||||
- Manual updates required
|
||||
|
||||
#### RuvBot (SOTA)
|
||||
```
|
||||
SONA Learning Pipeline:
|
||||
1. RETRIEVE: HNSW pattern search (<1ms)
|
||||
2. JUDGE: Verdict classification (success/failure)
|
||||
3. DISTILL: LoRA weight extraction
|
||||
4. CONSOLIDATE: EWC++ prevents catastrophic forgetting
|
||||
|
||||
Trajectory Learning:
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ User Query ──► Agent Response ──► Outcome ──► Pattern Store │
|
||||
│ │ │ │ │ │
|
||||
│ ▼ ▼ ▼ ▼ │
|
||||
│ Embedding Action Log Reward Score Neural Update │
|
||||
│ │
|
||||
│ Continuous improvement with each interaction │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3. LLM Routing & Intelligence
|
||||
|
||||
#### Clawdbot
|
||||
- Single model configuration
|
||||
- Manual model selection
|
||||
- No routing optimization
|
||||
|
||||
#### RuvBot (SOTA)
|
||||
```
|
||||
3-Tier Intelligent Routing:
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Tier 1: Agent Booster (<1ms, $0) │
|
||||
│ └─ Simple transforms: var→const, add-types, remove-console │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Tier 2: Haiku (~500ms, $0.0002) │
|
||||
│ └─ Bug fixes, simple tasks, low complexity │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Tier 3: Sonnet/Opus (2-5s, $0.003-$0.015) │
|
||||
│ └─ Architecture, security, complex reasoning │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
MoE (Mixture of Experts) + FastGRNN:
|
||||
- 100% routing accuracy (hybrid keyword-first strategy)
|
||||
- 75% cost reduction vs always-Sonnet
|
||||
- 352x faster for Tier 1 tasks
|
||||
```
|
||||
|
||||
### 4. Multi-Tenancy & Enterprise Features
|
||||
|
||||
#### Clawdbot
|
||||
- Single-user design
|
||||
- Shared data storage
|
||||
- No isolation
|
||||
|
||||
#### RuvBot (SOTA)
|
||||
```
|
||||
Enterprise Multi-Tenancy:
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Tenant Isolation Layers │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Database: PostgreSQL Row-Level Security (RLS) │
|
||||
│ └─ Automatic tenant_id filtering │
|
||||
│ └─ Cross-tenant queries impossible │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Memory: Namespace isolation │
|
||||
│ └─ Separate HNSW indices per tenant │
|
||||
│ └─ Embedding isolation │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Workers: Tenant-scoped queues │
|
||||
│ └─ Resource quotas per tenant │
|
||||
│ └─ Priority scheduling │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ API: Tenant context middleware │
|
||||
│ └─ JWT claims with tenant_id │
|
||||
│ └─ Rate limits per tenant │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 5. Background Workers
|
||||
|
||||
#### Clawdbot
|
||||
- Basic async processing
|
||||
- No specialized workers
|
||||
- Limited task types
|
||||
|
||||
#### RuvBot (SOTA)
|
||||
```
|
||||
12 Specialized Background Workers:
|
||||
┌───────────────────┬──────────┬─────────────────────────────────┐
|
||||
│ Worker │ Priority │ Purpose │
|
||||
├───────────────────┼──────────┼─────────────────────────────────┤
|
||||
│ ultralearn │ normal │ Deep knowledge acquisition │
|
||||
│ optimize │ high │ Performance optimization │
|
||||
│ consolidate │ low │ Memory consolidation (EWC++) │
|
||||
│ predict │ normal │ Predictive preloading │
|
||||
│ audit │ critical │ Security analysis │
|
||||
│ map │ normal │ Codebase/context mapping │
|
||||
│ preload │ low │ Resource preloading │
|
||||
│ deepdive │ normal │ Deep code/content analysis │
|
||||
│ document │ normal │ Auto-documentation │
|
||||
│ refactor │ normal │ Refactoring suggestions │
|
||||
│ benchmark │ normal │ Performance benchmarking │
|
||||
│ testgaps │ normal │ Test coverage analysis │
|
||||
└───────────────────┴──────────┴─────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 6. Security Comparison
|
||||
|
||||
#### Clawdbot
|
||||
- Good baseline security
|
||||
- Environment-based secrets
|
||||
- Basic input validation
|
||||
|
||||
#### RuvBot (SOTA)
|
||||
```
|
||||
6-Layer Defense in Depth:
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Layer 1: Transport (TLS 1.3, HSTS, cert pinning) │
|
||||
│ Layer 2: Authentication (JWT RS256, OAuth 2.0, rate limiting) │
|
||||
│ Layer 3: Authorization (RBAC, claims, tenant isolation) │
|
||||
│ Layer 4: Data Protection (AES-256-GCM, key rotation) │
|
||||
│ Layer 5: Input Validation (Zod schemas, injection prevention) │
|
||||
│ Layer 6: WASM Sandbox (memory isolation, resource limits) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Compliance Ready:
|
||||
- GDPR: Data export, deletion, consent
|
||||
- SOC 2: Audit logging, access controls
|
||||
- HIPAA: Encryption, access logging (configurable)
|
||||
```
|
||||
|
||||
## Performance Benchmarks
|
||||
|
||||
| Operation | Clawdbot | RuvBot | Improvement |
|
||||
|-----------|----------|--------|-------------|
|
||||
| Embedding generation | 200ms (API) | 2.7ms (WASM) | 74x faster |
|
||||
| Vector search (10K) | 50ms | <1ms | 50x faster |
|
||||
| Vector search (100K) | 500ms+ | <5ms | 100x faster |
|
||||
| Session restore | 100ms | 10ms | 10x faster |
|
||||
| Skill invocation | 50ms | 5ms | 10x faster |
|
||||
| Cold start | 3s | 500ms | 6x faster |
|
||||
|
||||
## Architecture Advantages
|
||||
|
||||
### RuvBot SOTA Innovations
|
||||
|
||||
1. **WASM-First Design**
|
||||
- Cross-platform consistency
|
||||
- No native compilation needed
|
||||
- Portable to browser environments
|
||||
|
||||
2. **Neural Substrate Integration**
|
||||
- Continuous learning via SONA
|
||||
- Pattern recognition with MoE
|
||||
- Catastrophic forgetting prevention (EWC++)
|
||||
|
||||
3. **Distributed Coordination**
|
||||
- Byzantine fault-tolerant consensus
|
||||
- Raft leader election
|
||||
- Gossip protocol for eventual consistency
|
||||
|
||||
4. **RuVector Integration**
|
||||
- 53+ SQL functions for vectors
|
||||
- 39 attention mechanisms
|
||||
- Hyperbolic embeddings for hierarchies
|
||||
- Flash Attention (2.49x-7.47x speedup)
|
||||
|
||||
## Migration Path
|
||||
|
||||
Clawdbot users can migrate to RuvBot with:
|
||||
|
||||
```bash
|
||||
# Export Clawdbot data
|
||||
clawdbot export --format json > data.json
|
||||
|
||||
# Import to RuvBot
|
||||
ruvbot import --from-clawdbot data.json
|
||||
|
||||
# Verify migration
|
||||
ruvbot doctor --verify-migration
|
||||
```
|
||||
|
||||
## Skills Comparison (52 Clawdbot → 68+ RuvBot)
|
||||
|
||||
### Clawdbot Skills (52)
|
||||
```
|
||||
1password, apple-notes, apple-reminders, bear-notes, bird, blogwatcher,
|
||||
blucli, bluebubbles, camsnap, canvas, clawdhub, coding-agent, discord,
|
||||
eightctl, food-order, gemini, gifgrep, github, gog, goplaces, himalaya,
|
||||
imsg, local-places, mcporter, model-usage, nano-banana-pro, nano-pdf,
|
||||
notion, obsidian, openai-image-gen, openai-whisper, openai-whisper-api,
|
||||
openhue, oracle, ordercli, peekaboo, sag, session-logs, sherpa-onnx-tts,
|
||||
skill-creator, slack, songsee, sonoscli, spotify-player, summarize,
|
||||
things-mac, tmux, trello, video-frames, voice-call, wacli, weather
|
||||
```
|
||||
|
||||
### RuvBot Skills (68+)
|
||||
```
|
||||
All 52 Clawdbot skills PLUS:
|
||||
|
||||
RuVector-Enhanced Skills:
|
||||
├─ semantic-search : HNSW O(log n) vector search (150x faster)
|
||||
├─ pattern-learning : SONA trajectory learning
|
||||
├─ hybrid-search : Vector + BM25 fusion
|
||||
├─ embedding-batch : Parallel WASM embedding
|
||||
├─ context-predict : Predictive context preloading
|
||||
├─ memory-consolidate : EWC++ memory consolidation
|
||||
|
||||
Distributed Skills (agentic-flow):
|
||||
├─ swarm-orchestrate : Multi-agent coordination
|
||||
├─ consensus-reach : Byzantine fault-tolerant consensus
|
||||
├─ load-balance : Dynamic task distribution
|
||||
├─ mesh-coordinate : Peer-to-peer mesh networking
|
||||
|
||||
Enterprise Skills:
|
||||
├─ tenant-isolate : Multi-tenant data isolation
|
||||
├─ audit-log : Comprehensive security logging
|
||||
├─ key-rotate : Automatic secret rotation
|
||||
├─ rls-enforce : Row-level security enforcement
|
||||
```
|
||||
|
||||
## Complete Module Comparison
|
||||
|
||||
| Module Category | Clawdbot (68) | RuvBot | RuvBot Advantage |
|
||||
|-----------------|---------------|--------|------------------|
|
||||
| **Core** | agents, sessions, memory | ✅ | + SONA learning |
|
||||
| **Channels** | slack, discord, telegram, signal, whatsapp, line, imessage | ✅ All + web | + Multi-tenant channels |
|
||||
| **CLI** | cli, commands | ✅ + MCP server | + 140+ subcommands |
|
||||
| **Memory** | SQLite + FTS | ✅ + HNSW WASM | **150-12,500x faster** |
|
||||
| **Embedding** | OpenAI/Gemini API | ✅ + Local WASM | **75x faster, $0 cost** |
|
||||
| **Workers** | Basic async | 12 specialized | + Learning workers |
|
||||
| **Routing** | Single model | 3-tier MoE | **75% cost reduction** |
|
||||
| **Cron** | Basic scheduler | ✅ + Priority queues | + Tenant-scoped |
|
||||
| **Daemon** | Basic | ✅ + Health checks | + Auto-recovery |
|
||||
| **Gateway** | HTTP | ✅ + WebSocket | + GraphQL subscriptions |
|
||||
| **Plugin SDK** | JavaScript | ✅ + WASM | + Sandboxed execution |
|
||||
| **TTS** | sherpa-onnx | ✅ + RuvLLM | + Lower latency |
|
||||
| **TUI** | Basic | ✅ + Rich | + Status dashboard |
|
||||
| **Security** | Good | 6-layer | + Defense in depth |
|
||||
| **Browser** | Puppeteer | ✅ + Playwright | + Session persistence |
|
||||
| **Media** | Basic | ✅ + WASM | + GPU acceleration |
|
||||
|
||||
## RuVector Exclusive Capabilities
|
||||
|
||||
### 1. WASM Vector Operations (npm @ruvector/wasm-unified)
|
||||
```typescript
|
||||
// RuvBot uses RuVector WASM for all vector operations
|
||||
import { HnswIndex, simdDistance } from '@ruvector/wasm-unified';
|
||||
|
||||
// 150x faster than Clawdbot's external API
|
||||
const results = await hnswIndex.search(query, { k: 10 });
|
||||
```
|
||||
|
||||
### 2. Local LLM with SONA (npm @ruvector/ruvllm)
|
||||
```typescript
|
||||
// Self-Optimizing Neural Architecture
|
||||
import { RuvLLM, SonaTrainer } from '@ruvector/ruvllm';
|
||||
|
||||
// Continuous learning from every interaction
|
||||
await sonaTrainer.train({
|
||||
trajectory: session.messages,
|
||||
outcome: 'success',
|
||||
consolidate: true // EWC++ prevents forgetting
|
||||
});
|
||||
```
|
||||
|
||||
### 3. PostgreSQL Vector Store (npm @ruvector/postgres-cli)
|
||||
```sql
|
||||
-- RuVector adds 53+ vector SQL functions
|
||||
SELECT * FROM memories
|
||||
WHERE tenant_id = current_tenant() -- RLS
|
||||
ORDER BY embedding <=> $query -- Cosine similarity
|
||||
LIMIT 10;
|
||||
```
|
||||
|
||||
### 4. Agentic-Flow Integration (npx agentic-flow)
|
||||
```typescript
|
||||
// Multi-agent swarm coordination
|
||||
import { SwarmCoordinator, ByzantineConsensus } from 'agentic-flow';
|
||||
|
||||
// 12 specialized background workers
|
||||
await swarm.dispatch({
|
||||
worker: 'ultralearn',
|
||||
task: { type: 'deep-analysis', content }
|
||||
});
|
||||
```
|
||||
|
||||
## Benchmark: RuvBot Dominance
|
||||
|
||||
| Metric | Clawdbot | RuvBot | Ratio |
|
||||
|--------|----------|--------|-------|
|
||||
| Embedding latency | 200ms | 2.7ms | **74x** |
|
||||
| 10K vector search | 50ms | <1ms | **50x** |
|
||||
| 100K vector search | 500ms | <5ms | **100x** |
|
||||
| 1M vector search | N/A | <10ms | **∞** |
|
||||
| Session restore | 100ms | 10ms | **10x** |
|
||||
| Skill invocation | 50ms | 5ms | **10x** |
|
||||
| Cold start | 3000ms | 500ms | **6x** |
|
||||
| Memory consolidation | N/A | <50ms | **∞** |
|
||||
| Pattern learning | N/A | <5ms | **∞** |
|
||||
| Multi-tenant query | N/A | <2ms | **∞** |
|
||||
|
||||
## agentic-flow Integration Details
|
||||
|
||||
### Background Workers (12 Types)
|
||||
| Worker | Clawdbot | RuvBot | Enhancement |
|
||||
|--------|----------|--------|-------------|
|
||||
| ultralearn | ❌ | ✅ | Deep knowledge acquisition |
|
||||
| optimize | ❌ | ✅ | Performance optimization |
|
||||
| consolidate | ❌ | ✅ | EWC++ memory consolidation |
|
||||
| predict | ❌ | ✅ | Predictive preloading |
|
||||
| audit | ❌ | ✅ | Security analysis |
|
||||
| map | ❌ | ✅ | Codebase mapping |
|
||||
| preload | ❌ | ✅ | Resource preloading |
|
||||
| deepdive | ❌ | ✅ | Deep code analysis |
|
||||
| document | ❌ | ✅ | Auto-documentation |
|
||||
| refactor | ❌ | ✅ | Refactoring suggestions |
|
||||
| benchmark | ❌ | ✅ | Performance benchmarking |
|
||||
| testgaps | ❌ | ✅ | Test coverage analysis |
|
||||
|
||||
### Swarm Topologies
|
||||
| Topology | Clawdbot | RuvBot | Use Case |
|
||||
|----------|----------|--------|----------|
|
||||
| hierarchical | ❌ | ✅ | Queen-worker coordination |
|
||||
| mesh | ❌ | ✅ | Peer-to-peer networking |
|
||||
| hierarchical-mesh | ❌ | ✅ | Hybrid scalability |
|
||||
| adaptive | ❌ | ✅ | Dynamic switching |
|
||||
|
||||
### Consensus Mechanisms
|
||||
| Protocol | Clawdbot | RuvBot | Fault Tolerance |
|
||||
|----------|----------|--------|-----------------|
|
||||
| Byzantine | ❌ | ✅ | f < n/3 faulty |
|
||||
| Raft | ❌ | ✅ | f < n/2 failures |
|
||||
| Gossip | ❌ | ✅ | Eventually consistent |
|
||||
| CRDT | ❌ | ✅ | Conflict-free replication |
|
||||
|
||||
### 10. Cloud Deployment
|
||||
|
||||
#### Clawdbot
|
||||
- Manual deployment
|
||||
- No cloud-native support
|
||||
- Self-managed infrastructure
|
||||
|
||||
#### RuvBot (SOTA)
|
||||
```
|
||||
Google Cloud Platform (Cost-Optimized):
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Cloud Run (Serverless) │
|
||||
│ └─ Scale to zero when idle │
|
||||
│ └─ Auto-scale 0-100 instances │
|
||||
│ └─ 512Mi memory, sub-second cold start │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Cloud SQL (PostgreSQL) │
|
||||
│ └─ db-f1-micro (~$10/month) │
|
||||
│ └─ Automatic backups │
|
||||
│ └─ Row-Level Security │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Infrastructure as Code │
|
||||
│ └─ Terraform modules included │
|
||||
│ └─ Cloud Build CI/CD pipeline │
|
||||
│ └─ One-command deployment │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Estimated Monthly Cost:
|
||||
| Traffic Level | Configuration | Cost |
|
||||
|---------------|---------------|------|
|
||||
| Low (<1K/day) | Min resources | ~$15-20/month |
|
||||
| Medium (<10K/day) | Scaled | ~$40/month |
|
||||
| High (<100K/day) | Enterprise | ~$150/month |
|
||||
```
|
||||
|
||||
### 11. LLM Provider Support
|
||||
|
||||
#### Clawdbot
|
||||
- Single provider (typically OpenAI)
|
||||
- No model routing
|
||||
- Fixed pricing
|
||||
- No Gemini 2.5 support
|
||||
|
||||
#### RuvBot (SOTA)
|
||||
```
|
||||
Multi-Provider Architecture with Gemini 2.5 Default:
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ OpenRouter (200+ Models) - DEFAULT PROVIDER │
|
||||
│ └─ Google Gemini 2.5 Pro Preview (RECOMMENDED) │
|
||||
│ └─ Google Gemini 2.0 Flash (fast responses) │
|
||||
│ └─ Google Gemini 2.0 Flash Thinking (FREE reasoning) │
|
||||
│ └─ Qwen QwQ-32B (Reasoning) - FREE tier available │
|
||||
│ └─ DeepSeek R1 (Open-source reasoning) │
|
||||
│ └─ OpenAI O1/GPT-4o │
|
||||
│ └─ Meta Llama 3.1 405B │
|
||||
│ └─ Best for: Cost optimization, variety │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Anthropic (Direct API) │
|
||||
│ └─ Claude 3.5 Sonnet (latest) │
|
||||
│ └─ Claude 3 Opus (complex analysis) │
|
||||
│ └─ Best for: Quality, reliability, safety │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Model Comparison (12 Available):
|
||||
| Model | Provider | Best For | Cost |
|
||||
|-------|----------|----------|------|
|
||||
| Gemini 2.5 Pro | OpenRouter | General + Reasoning | $$ |
|
||||
| Gemini 2.0 Flash | OpenRouter | Speed | $ |
|
||||
| Gemini 2.0 Flash Thinking | OpenRouter | Reasoning | FREE |
|
||||
| Claude 3.5 Sonnet | Anthropic | Quality | $$$ |
|
||||
| GPT-4o | OpenRouter | General | $$$ |
|
||||
| QwQ-32B | OpenRouter | Math/Reasoning | $ |
|
||||
| QwQ-32B Free | OpenRouter | Budget | FREE |
|
||||
| DeepSeek R1 | OpenRouter | Open-source | $ |
|
||||
| O1 Preview | OpenRouter | Advanced reasoning | $$$$ |
|
||||
| Llama 3.1 405B | OpenRouter | Enterprise | $$ |
|
||||
|
||||
Intelligent Model Selection:
|
||||
- Budget → Gemini 2.0 Flash Thinking (FREE) or QwQ Free
|
||||
- General → Gemini 2.5 Pro (DEFAULT)
|
||||
- Quality → Claude 3.5 Sonnet
|
||||
- Complex reasoning → O1 Preview or Claude Opus
|
||||
```
|
||||
|
||||
### 12. Hybrid Search
|
||||
|
||||
#### Clawdbot
|
||||
- Vector-only search
|
||||
- No keyword fallback
|
||||
- Limited result ranking
|
||||
|
||||
#### RuvBot (SOTA)
|
||||
```
|
||||
Hybrid Search Architecture (ADR-009):
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Query Processing │
|
||||
│ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ BM25 │ │ Vector │ │
|
||||
│ │ Keyword │ │ Semantic │ │
|
||||
│ │ Search │ │ Search │ │
|
||||
│ └──────┬──────┘ └──────┬──────┘ │
|
||||
│ │ │ │
|
||||
│ └────────────┬───────────────┘ │
|
||||
│ ▼ │
|
||||
│ ┌───────────────┐ │
|
||||
│ │ RRF Fusion │ │
|
||||
│ │ (k=60) │ │
|
||||
│ └───────┬───────┘ │
|
||||
│ ▼ │
|
||||
│ ┌───────────────┐ │
|
||||
│ │ Re-ranking │ │
|
||||
│ │ + Filtering │ │
|
||||
│ └───────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
BM25 Configuration:
|
||||
- k1: 1.2 (term frequency saturation)
|
||||
- b: 0.75 (document length normalization)
|
||||
- Tokenization: Unicode word boundaries
|
||||
- Stemming: Porter stemmer (optional)
|
||||
|
||||
Search Accuracy Comparison:
|
||||
| Method | Precision@10 | Recall@100 | Latency |
|
||||
|--------|--------------|------------|---------|
|
||||
| BM25 only | 0.72 | 0.85 | <5ms |
|
||||
| Vector only | 0.78 | 0.92 | <10ms |
|
||||
| Hybrid (RRF) | 0.91 | 0.97 | <15ms |
|
||||
```
|
||||
|
||||
### 13. Adversarial Defense (AIDefence Integration)
|
||||
|
||||
#### Clawdbot
|
||||
- Basic input validation
|
||||
- No prompt injection protection
|
||||
- No jailbreak detection
|
||||
- Manual PII handling
|
||||
|
||||
#### RuvBot (SOTA)
|
||||
```
|
||||
AIDefence Multi-Layer Protection (ADR-014):
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Layer 1: Pattern Detection (<5ms) │
|
||||
│ └─ 50+ prompt injection signatures │
|
||||
│ └─ Jailbreak patterns (DAN, bypass, unlimited) │
|
||||
│ └─ Custom patterns (configurable) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Layer 2: PII Protection (<3ms) │
|
||||
│ └─ Email, phone, SSN, credit cards │
|
||||
│ └─ API keys and tokens │
|
||||
│ └─ IP addresses │
|
||||
│ └─ Automatic masking │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Layer 3: Sanitization (<1ms) │
|
||||
│ └─ Control character removal │
|
||||
│ └─ Unicode homoglyph normalization │
|
||||
│ └─ Encoding attack prevention │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Layer 4: Behavioral Analysis (<100ms) [Optional] │
|
||||
│ └─ User behavior baseline │
|
||||
│ └─ Anomaly detection │
|
||||
│ └─ Deviation scoring │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Layer 5: Response Validation (<8ms) │
|
||||
│ └─ PII leak detection │
|
||||
│ └─ Injection echo detection │
|
||||
│ └─ Malicious code detection │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Threat Detection Performance:
|
||||
| Threat Type | Clawdbot | RuvBot | Detection Time |
|
||||
|-------------|----------|--------|----------------|
|
||||
| Prompt Injection | ❌ | ✅ | <5ms |
|
||||
| Jailbreak | ❌ | ✅ | <5ms |
|
||||
| PII Exposure | ❌ | ✅ | <3ms |
|
||||
| Control Characters | ❌ | ✅ | <1ms |
|
||||
| Homoglyph Attacks | ❌ | ✅ | <1ms |
|
||||
| Behavioral Anomaly | ❌ | ✅ | <100ms |
|
||||
| Response Leakage | ❌ | ✅ | <8ms |
|
||||
|
||||
Usage Example:
|
||||
```typescript
|
||||
import { createAIDefenceGuard } from '@ruvector/ruvbot';
|
||||
|
||||
const guard = createAIDefenceGuard({
|
||||
detectPromptInjection: true,
|
||||
detectJailbreak: true,
|
||||
detectPII: true,
|
||||
blockThreshold: 'medium',
|
||||
});
|
||||
|
||||
const result = await guard.analyze(userInput);
|
||||
if (!result.safe) {
|
||||
// Block or use sanitized input
|
||||
const safeInput = result.sanitizedInput;
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
RuvBot represents a **security-first, next-generation evolution** of the personal AI assistant paradigm:
|
||||
|
||||
### Security: The Critical Difference
|
||||
|
||||
| Security Feature | Clawdbot | RuvBot | Verdict |
|
||||
|-----------------|----------|--------|---------|
|
||||
| **Prompt Injection** | VULNERABLE | Protected (<5ms) | ⚠️ **CRITICAL** |
|
||||
| **Jailbreak Defense** | VULNERABLE | Blocked | ⚠️ **CRITICAL** |
|
||||
| **PII Protection** | NONE | Auto-masked | ⚠️ **HIGH RISK** |
|
||||
| **Input Sanitization** | NONE | Full | ⚠️ **HIGH RISK** |
|
||||
| **Multi-tenant Isolation** | NONE | PostgreSQL RLS | ⚠️ **HIGH RISK** |
|
||||
|
||||
**Do not deploy Clawdbot in production without security hardening.**
|
||||
|
||||
### Complete Comparison
|
||||
|
||||
| Aspect | Clawdbot | RuvBot | Winner |
|
||||
|--------|----------|--------|--------|
|
||||
| **Security** | Vulnerable | 6-layer + AIDefence | 🏆 RuvBot |
|
||||
| **Adversarial Defense** | None | AIDefence (<10ms) | 🏆 RuvBot |
|
||||
| **Performance** | Baseline | 50-150x faster | 🏆 RuvBot |
|
||||
| **Intelligence** | Static | Self-learning SONA | 🏆 RuvBot |
|
||||
| **Scalability** | Single-user | Enterprise multi-tenant | 🏆 RuvBot |
|
||||
| **LLM Models** | Single | 12+ (Gemini 2.5, Claude, GPT) | 🏆 RuvBot |
|
||||
| **Plugin System** | Basic | IPFS + sandboxed | 🏆 RuvBot |
|
||||
| **Skills** | 52 | 68+ | 🏆 RuvBot |
|
||||
| **Workers** | Basic | 12 specialized | 🏆 RuvBot |
|
||||
| **Consensus** | None | 4 protocols | 🏆 RuvBot |
|
||||
| **Cloud Deploy** | Manual | GCP Terraform (~$15/mo) | 🏆 RuvBot |
|
||||
| **Hybrid Search** | Vector-only | BM25 + Vector RRF | 🏆 RuvBot |
|
||||
| **Cost** | API fees | $0 local WASM | 🏆 RuvBot |
|
||||
| **Portability** | Node.js | WASM everywhere | 🏆 RuvBot |
|
||||
|
||||
**RuvBot is definitively better than Clawdbot in every measurable dimension**, especially security and intelligence, while maintaining full compatibility with Clawdbot's skill and extension architecture.
|
||||
|
||||
### Migration Recommendation
|
||||
|
||||
If you are currently using Clawdbot, **migrate to RuvBot immediately** to address critical security vulnerabilities. RuvBot provides a seamless migration path with full skill compatibility.
|
||||
916
vendor/ruvector/npm/packages/ruvbot/docs/IMPLEMENTATION_PLAN.yaml
vendored
Normal file
916
vendor/ruvector/npm/packages/ruvbot/docs/IMPLEMENTATION_PLAN.yaml
vendored
Normal file
@@ -0,0 +1,916 @@
|
||||
# RuvBot Implementation Plan
|
||||
# High-performance AI assistant bot with WASM embeddings, vector memory, and multi-platform integration
|
||||
|
||||
plan:
|
||||
objective: "Build RuvBot npm package - a self-learning AI assistant with WASM embeddings, vector memory, and Slack/webhook integrations"
|
||||
|
||||
version: "0.1.0"
|
||||
estimated_duration: "6-8 weeks"
|
||||
|
||||
success_criteria:
|
||||
- "Package installable via npx @ruvector/ruvbot"
|
||||
- "CLI supports local and remote deployment modes"
|
||||
- "WASM embeddings working in Node.js and browser"
|
||||
- "Vector memory with HNSW search < 10ms"
|
||||
- "Slack integration with real-time message handling"
|
||||
- "Background workers processing async tasks"
|
||||
- "Extensible skill system with hot-reload"
|
||||
- "Session persistence across restarts"
|
||||
- "85%+ test coverage on core modules"
|
||||
|
||||
phases:
|
||||
# ============================================================================
|
||||
# PHASE 1: Core Foundation (Week 1-2)
|
||||
# ============================================================================
|
||||
- name: "Phase 1: Core Foundation"
|
||||
duration: "2 weeks"
|
||||
description: "Establish package structure and core domain entities"
|
||||
|
||||
tasks:
|
||||
- id: "p1-t1"
|
||||
description: "Initialize package with tsup, TypeScript, and ESM/CJS dual build"
|
||||
agent: "coder"
|
||||
dependencies: []
|
||||
estimated_time: "2h"
|
||||
priority: "critical"
|
||||
files:
|
||||
- "package.json"
|
||||
- "tsconfig.json"
|
||||
- "tsup.config.ts"
|
||||
- ".npmignore"
|
||||
|
||||
- id: "p1-t2"
|
||||
description: "Create core domain entities (Agent, Session, Message, Skill)"
|
||||
agent: "coder"
|
||||
dependencies: ["p1-t1"]
|
||||
estimated_time: "4h"
|
||||
priority: "high"
|
||||
files:
|
||||
- "src/core/entities/Agent.ts"
|
||||
- "src/core/entities/Session.ts"
|
||||
- "src/core/entities/Message.ts"
|
||||
- "src/core/entities/Skill.ts"
|
||||
- "src/core/entities/index.ts"
|
||||
- "src/core/types.ts"
|
||||
|
||||
- id: "p1-t3"
|
||||
description: "Implement RuvBot main class with lifecycle management"
|
||||
agent: "coder"
|
||||
dependencies: ["p1-t2"]
|
||||
estimated_time: "4h"
|
||||
priority: "high"
|
||||
files:
|
||||
- "src/RuvBot.ts"
|
||||
- "src/core/BotConfig.ts"
|
||||
- "src/core/BotState.ts"
|
||||
|
||||
- id: "p1-t4"
|
||||
description: "Create error types and result monads"
|
||||
agent: "coder"
|
||||
dependencies: ["p1-t1"]
|
||||
estimated_time: "2h"
|
||||
priority: "medium"
|
||||
files:
|
||||
- "src/core/errors.ts"
|
||||
- "src/core/Result.ts"
|
||||
|
||||
- id: "p1-t5"
|
||||
description: "Set up unit testing with vitest"
|
||||
agent: "tester"
|
||||
dependencies: ["p1-t3"]
|
||||
estimated_time: "3h"
|
||||
priority: "high"
|
||||
files:
|
||||
- "vitest.config.ts"
|
||||
- "tests/unit/core/RuvBot.test.ts"
|
||||
- "tests/unit/core/entities/*.test.ts"
|
||||
|
||||
# ============================================================================
|
||||
# PHASE 2: Infrastructure Layer (Week 2-3)
|
||||
# ============================================================================
|
||||
- name: "Phase 2: Infrastructure Layer"
|
||||
duration: "1.5 weeks"
|
||||
description: "Database, messaging, and worker infrastructure"
|
||||
|
||||
tasks:
|
||||
- id: "p2-t1"
|
||||
description: "Implement SessionStore with SQLite and PostgreSQL adapters"
|
||||
agent: "coder"
|
||||
dependencies: ["p1-t2"]
|
||||
estimated_time: "6h"
|
||||
priority: "high"
|
||||
files:
|
||||
- "src/infrastructure/storage/SessionStore.ts"
|
||||
- "src/infrastructure/storage/adapters/SQLiteAdapter.ts"
|
||||
- "src/infrastructure/storage/adapters/PostgresAdapter.ts"
|
||||
- "src/infrastructure/storage/adapters/BaseAdapter.ts"
|
||||
|
||||
- id: "p2-t2"
|
||||
description: "Create MessageQueue with in-memory and Redis backends"
|
||||
agent: "coder"
|
||||
dependencies: ["p1-t2"]
|
||||
estimated_time: "5h"
|
||||
priority: "high"
|
||||
files:
|
||||
- "src/infrastructure/messaging/MessageQueue.ts"
|
||||
- "src/infrastructure/messaging/InMemoryQueue.ts"
|
||||
- "src/infrastructure/messaging/RedisQueue.ts"
|
||||
|
||||
- id: "p2-t3"
|
||||
description: "Implement WorkerPool using agentic-flow patterns"
|
||||
agent: "coder"
|
||||
dependencies: ["p2-t2"]
|
||||
estimated_time: "6h"
|
||||
priority: "high"
|
||||
files:
|
||||
- "src/infrastructure/workers/WorkerPool.ts"
|
||||
- "src/infrastructure/workers/Worker.ts"
|
||||
- "src/infrastructure/workers/TaskScheduler.ts"
|
||||
- "src/infrastructure/workers/tasks/index.ts"
|
||||
|
||||
- id: "p2-t4"
|
||||
description: "Create EventBus for internal pub/sub communication"
|
||||
agent: "coder"
|
||||
dependencies: ["p1-t1"]
|
||||
estimated_time: "3h"
|
||||
priority: "medium"
|
||||
files:
|
||||
- "src/infrastructure/events/EventBus.ts"
|
||||
- "src/infrastructure/events/types.ts"
|
||||
|
||||
- id: "p2-t5"
|
||||
description: "Add connection pooling and health checks"
|
||||
agent: "coder"
|
||||
dependencies: ["p2-t1", "p2-t2"]
|
||||
estimated_time: "4h"
|
||||
priority: "medium"
|
||||
files:
|
||||
- "src/infrastructure/health/HealthChecker.ts"
|
||||
- "src/infrastructure/pool/ConnectionPool.ts"
|
||||
|
||||
# ============================================================================
|
||||
# PHASE 3: Learning Layer - WASM & ruvllm (Week 3-4)
|
||||
# ============================================================================
|
||||
- name: "Phase 3: Learning Layer"
|
||||
duration: "1.5 weeks"
|
||||
description: "WASM embeddings and ruvllm integration for self-learning"
|
||||
|
||||
tasks:
|
||||
- id: "p3-t1"
|
||||
description: "Create MemoryManager with HNSW vector search"
|
||||
agent: "coder"
|
||||
dependencies: ["p2-t1"]
|
||||
estimated_time: "8h"
|
||||
priority: "critical"
|
||||
files:
|
||||
- "src/learning/memory/MemoryManager.ts"
|
||||
- "src/learning/memory/VectorIndex.ts"
|
||||
- "src/learning/memory/types.ts"
|
||||
dependencies_pkg:
|
||||
- "@ruvector/wasm-unified"
|
||||
|
||||
- id: "p3-t2"
|
||||
description: "Integrate @ruvector/wasm-unified for WASM embeddings"
|
||||
agent: "coder"
|
||||
dependencies: ["p3-t1"]
|
||||
estimated_time: "6h"
|
||||
priority: "critical"
|
||||
files:
|
||||
- "src/learning/embeddings/WasmEmbedder.ts"
|
||||
- "src/learning/embeddings/EmbeddingCache.ts"
|
||||
- "src/learning/embeddings/index.ts"
|
||||
|
||||
- id: "p3-t3"
|
||||
description: "Integrate @ruvector/ruvllm for LLM orchestration"
|
||||
agent: "coder"
|
||||
dependencies: ["p3-t2"]
|
||||
estimated_time: "6h"
|
||||
priority: "high"
|
||||
files:
|
||||
- "src/learning/llm/LLMOrchestrator.ts"
|
||||
- "src/learning/llm/ModelRouter.ts"
|
||||
- "src/learning/llm/SessionContext.ts"
|
||||
dependencies_pkg:
|
||||
- "@ruvector/ruvllm"
|
||||
|
||||
- id: "p3-t4"
|
||||
description: "Implement trajectory learning and pattern extraction"
|
||||
agent: "coder"
|
||||
dependencies: ["p3-t3"]
|
||||
estimated_time: "5h"
|
||||
priority: "medium"
|
||||
files:
|
||||
- "src/learning/trajectory/TrajectoryRecorder.ts"
|
||||
- "src/learning/trajectory/PatternExtractor.ts"
|
||||
- "src/learning/trajectory/types.ts"
|
||||
|
||||
- id: "p3-t5"
|
||||
description: "Add semantic search and retrieval pipeline"
|
||||
agent: "coder"
|
||||
dependencies: ["p3-t1", "p3-t2"]
|
||||
estimated_time: "4h"
|
||||
priority: "high"
|
||||
files:
|
||||
- "src/learning/retrieval/SemanticSearch.ts"
|
||||
- "src/learning/retrieval/RetrievalPipeline.ts"
|
||||
|
||||
# ============================================================================
|
||||
# PHASE 4: Skill System (Week 4-5)
|
||||
# ============================================================================
|
||||
- name: "Phase 4: Skill System"
|
||||
duration: "1 week"
|
||||
description: "Extensible skill registry with hot-reload support"
|
||||
|
||||
tasks:
|
||||
- id: "p4-t1"
|
||||
description: "Create SkillRegistry with plugin architecture"
|
||||
agent: "coder"
|
||||
dependencies: ["p1-t2", "p3-t3"]
|
||||
estimated_time: "6h"
|
||||
priority: "high"
|
||||
files:
|
||||
- "src/skills/SkillRegistry.ts"
|
||||
- "src/skills/SkillLoader.ts"
|
||||
- "src/skills/SkillContext.ts"
|
||||
- "src/skills/types.ts"
|
||||
|
||||
- id: "p4-t2"
|
||||
description: "Implement built-in skills (search, summarize, code)"
|
||||
agent: "coder"
|
||||
dependencies: ["p4-t1"]
|
||||
estimated_time: "8h"
|
||||
priority: "high"
|
||||
files:
|
||||
- "src/skills/builtin/SearchSkill.ts"
|
||||
- "src/skills/builtin/SummarizeSkill.ts"
|
||||
- "src/skills/builtin/CodeSkill.ts"
|
||||
- "src/skills/builtin/MemorySkill.ts"
|
||||
- "src/skills/builtin/index.ts"
|
||||
|
||||
- id: "p4-t3"
|
||||
description: "Add skill hot-reload with file watching"
|
||||
agent: "coder"
|
||||
dependencies: ["p4-t1"]
|
||||
estimated_time: "4h"
|
||||
priority: "medium"
|
||||
files:
|
||||
- "src/skills/HotReloader.ts"
|
||||
- "src/skills/SkillValidator.ts"
|
||||
|
||||
- id: "p4-t4"
|
||||
description: "Create skill template generator"
|
||||
agent: "coder"
|
||||
dependencies: ["p4-t1"]
|
||||
estimated_time: "3h"
|
||||
priority: "low"
|
||||
files:
|
||||
- "src/skills/templates/skill-template.ts"
|
||||
- "src/skills/generator.ts"
|
||||
|
||||
# ============================================================================
|
||||
# PHASE 5: Integrations (Week 5-6)
|
||||
# ============================================================================
|
||||
- name: "Phase 5: Integrations"
|
||||
duration: "1.5 weeks"
|
||||
description: "Slack, webhooks, and external service integrations"
|
||||
|
||||
tasks:
|
||||
- id: "p5-t1"
|
||||
description: "Implement SlackAdapter with Socket Mode"
|
||||
agent: "coder"
|
||||
dependencies: ["p1-t3", "p4-t1"]
|
||||
estimated_time: "8h"
|
||||
priority: "high"
|
||||
files:
|
||||
- "src/integrations/slack/SlackAdapter.ts"
|
||||
- "src/integrations/slack/SlackEventHandler.ts"
|
||||
- "src/integrations/slack/SlackMessageFormatter.ts"
|
||||
- "src/integrations/slack/types.ts"
|
||||
dependencies_pkg:
|
||||
- "@slack/bolt"
|
||||
- "@slack/web-api"
|
||||
|
||||
- id: "p5-t2"
|
||||
description: "Create WebhookServer for HTTP callbacks"
|
||||
agent: "coder"
|
||||
dependencies: ["p1-t3"]
|
||||
estimated_time: "5h"
|
||||
priority: "high"
|
||||
files:
|
||||
- "src/integrations/webhooks/WebhookServer.ts"
|
||||
- "src/integrations/webhooks/WebhookValidator.ts"
|
||||
- "src/integrations/webhooks/routes.ts"
|
||||
|
||||
- id: "p5-t3"
|
||||
description: "Add Discord adapter"
|
||||
agent: "coder"
|
||||
dependencies: ["p5-t1"]
|
||||
estimated_time: "6h"
|
||||
priority: "medium"
|
||||
files:
|
||||
- "src/integrations/discord/DiscordAdapter.ts"
|
||||
- "src/integrations/discord/DiscordEventHandler.ts"
|
||||
dependencies_pkg:
|
||||
- "discord.js"
|
||||
|
||||
- id: "p5-t4"
|
||||
description: "Create generic ChatAdapter interface"
|
||||
agent: "coder"
|
||||
dependencies: ["p5-t1", "p5-t3"]
|
||||
estimated_time: "3h"
|
||||
priority: "medium"
|
||||
files:
|
||||
- "src/integrations/ChatAdapter.ts"
|
||||
- "src/integrations/AdapterFactory.ts"
|
||||
- "src/integrations/types.ts"
|
||||
|
||||
# ============================================================================
|
||||
# PHASE 6: API Layer (Week 6)
|
||||
# ============================================================================
|
||||
- name: "Phase 6: API Layer"
|
||||
duration: "1 week"
|
||||
description: "REST and GraphQL endpoints for external access"
|
||||
|
||||
tasks:
|
||||
- id: "p6-t1"
|
||||
description: "Create REST API server with Express/Fastify"
|
||||
agent: "coder"
|
||||
dependencies: ["p1-t3", "p4-t1"]
|
||||
estimated_time: "6h"
|
||||
priority: "high"
|
||||
files:
|
||||
- "src/api/rest/server.ts"
|
||||
- "src/api/rest/routes/chat.ts"
|
||||
- "src/api/rest/routes/sessions.ts"
|
||||
- "src/api/rest/routes/skills.ts"
|
||||
- "src/api/rest/routes/health.ts"
|
||||
- "src/api/rest/middleware/auth.ts"
|
||||
- "src/api/rest/middleware/rateLimit.ts"
|
||||
dependencies_pkg:
|
||||
- "fastify"
|
||||
- "@fastify/cors"
|
||||
- "@fastify/rate-limit"
|
||||
|
||||
- id: "p6-t2"
|
||||
description: "Add GraphQL API with subscriptions"
|
||||
agent: "coder"
|
||||
dependencies: ["p6-t1"]
|
||||
estimated_time: "6h"
|
||||
priority: "medium"
|
||||
files:
|
||||
- "src/api/graphql/schema.ts"
|
||||
- "src/api/graphql/resolvers/chat.ts"
|
||||
- "src/api/graphql/resolvers/sessions.ts"
|
||||
- "src/api/graphql/subscriptions.ts"
|
||||
dependencies_pkg:
|
||||
- "mercurius"
|
||||
- "graphql"
|
||||
|
||||
- id: "p6-t3"
|
||||
description: "Implement OpenAPI spec generation"
|
||||
agent: "coder"
|
||||
dependencies: ["p6-t1"]
|
||||
estimated_time: "3h"
|
||||
priority: "low"
|
||||
files:
|
||||
- "src/api/openapi/generator.ts"
|
||||
- "src/api/openapi/decorators.ts"
|
||||
|
||||
# ============================================================================
|
||||
# PHASE 7: CLI & Distribution (Week 6-7)
|
||||
# ============================================================================
|
||||
- name: "Phase 7: CLI & Distribution"
|
||||
duration: "1 week"
|
||||
description: "CLI interface and npx distribution setup"
|
||||
|
||||
tasks:
|
||||
- id: "p7-t1"
|
||||
description: "Create CLI entry point with commander"
|
||||
agent: "coder"
|
||||
dependencies: ["p1-t3", "p5-t1", "p6-t1"]
|
||||
estimated_time: "6h"
|
||||
priority: "critical"
|
||||
files:
|
||||
- "bin/cli.js"
|
||||
- "src/cli/index.ts"
|
||||
- "src/cli/commands/start.ts"
|
||||
- "src/cli/commands/config.ts"
|
||||
- "src/cli/commands/skills.ts"
|
||||
- "src/cli/commands/status.ts"
|
||||
dependencies_pkg:
|
||||
- "commander"
|
||||
- "chalk"
|
||||
- "ora"
|
||||
- "inquirer"
|
||||
|
||||
- id: "p7-t2"
|
||||
description: "Add local vs remote deployment modes"
|
||||
agent: "coder"
|
||||
dependencies: ["p7-t1"]
|
||||
estimated_time: "4h"
|
||||
priority: "high"
|
||||
files:
|
||||
- "src/cli/modes/local.ts"
|
||||
- "src/cli/modes/remote.ts"
|
||||
- "src/cli/modes/docker.ts"
|
||||
|
||||
- id: "p7-t3"
|
||||
description: "Create configuration wizard"
|
||||
agent: "coder"
|
||||
dependencies: ["p7-t1"]
|
||||
estimated_time: "4h"
|
||||
priority: "medium"
|
||||
files:
|
||||
- "src/cli/wizard/ConfigWizard.ts"
|
||||
- "src/cli/wizard/prompts.ts"
|
||||
|
||||
- id: "p7-t4"
|
||||
description: "Build install script for curl | bash deployment"
|
||||
agent: "coder"
|
||||
dependencies: ["p7-t1"]
|
||||
estimated_time: "3h"
|
||||
priority: "medium"
|
||||
files:
|
||||
- "scripts/install.sh"
|
||||
- "scripts/uninstall.sh"
|
||||
|
||||
- id: "p7-t5"
|
||||
description: "Create Docker configuration"
|
||||
agent: "coder"
|
||||
dependencies: ["p7-t2"]
|
||||
estimated_time: "3h"
|
||||
priority: "medium"
|
||||
files:
|
||||
- "Dockerfile"
|
||||
- "docker-compose.yml"
|
||||
- ".dockerignore"
|
||||
|
||||
# ============================================================================
|
||||
# PHASE 8: Testing & Documentation (Week 7-8)
|
||||
# ============================================================================
|
||||
- name: "Phase 8: Testing & Documentation"
|
||||
duration: "1 week"
|
||||
description: "Comprehensive testing and documentation"
|
||||
|
||||
tasks:
|
||||
- id: "p8-t1"
|
||||
description: "Integration tests for all modules"
|
||||
agent: "tester"
|
||||
dependencies: ["p7-t1"]
|
||||
estimated_time: "8h"
|
||||
priority: "high"
|
||||
files:
|
||||
- "tests/integration/bot.test.ts"
|
||||
- "tests/integration/memory.test.ts"
|
||||
- "tests/integration/skills.test.ts"
|
||||
- "tests/integration/slack.test.ts"
|
||||
- "tests/integration/api.test.ts"
|
||||
|
||||
- id: "p8-t2"
|
||||
description: "E2E tests with real services"
|
||||
agent: "tester"
|
||||
dependencies: ["p8-t1"]
|
||||
estimated_time: "6h"
|
||||
priority: "medium"
|
||||
files:
|
||||
- "tests/e2e/full-flow.test.ts"
|
||||
- "tests/e2e/slack-flow.test.ts"
|
||||
- "tests/fixtures/"
|
||||
|
||||
- id: "p8-t3"
|
||||
description: "Performance benchmarks"
|
||||
agent: "tester"
|
||||
dependencies: ["p8-t1"]
|
||||
estimated_time: "4h"
|
||||
priority: "medium"
|
||||
files:
|
||||
- "benchmarks/memory.bench.ts"
|
||||
- "benchmarks/embeddings.bench.ts"
|
||||
- "benchmarks/throughput.bench.ts"
|
||||
|
||||
# ============================================================================
|
||||
# CRITICAL PATH
|
||||
# ============================================================================
|
||||
critical_path:
|
||||
- "p1-t1" # Package init
|
||||
- "p1-t2" # Core entities
|
||||
- "p1-t3" # RuvBot class
|
||||
- "p3-t1" # MemoryManager
|
||||
- "p3-t2" # WASM embeddings
|
||||
- "p4-t1" # SkillRegistry
|
||||
- "p5-t1" # SlackAdapter
|
||||
- "p7-t1" # CLI
|
||||
|
||||
# ============================================================================
|
||||
# RISK ASSESSMENT
|
||||
# ============================================================================
|
||||
risks:
|
||||
- id: "risk-1"
|
||||
description: "WASM module compatibility issues across Node versions"
|
||||
likelihood: "medium"
|
||||
impact: "high"
|
||||
mitigation: "Test on Node 18, 20, 22. Provide pure JS fallback for critical paths"
|
||||
|
||||
- id: "risk-2"
|
||||
description: "Slack API rate limiting during high traffic"
|
||||
likelihood: "medium"
|
||||
impact: "medium"
|
||||
mitigation: "Implement exponential backoff and message batching"
|
||||
|
||||
- id: "risk-3"
|
||||
description: "Memory leaks in long-running bot instances"
|
||||
likelihood: "medium"
|
||||
impact: "high"
|
||||
mitigation: "Add memory monitoring, implement LRU caches, periodic cleanup"
|
||||
|
||||
- id: "risk-4"
|
||||
description: "Breaking changes in upstream @ruvector packages"
|
||||
likelihood: "low"
|
||||
impact: "high"
|
||||
mitigation: "Pin specific versions, maintain compatibility layer"
|
||||
|
||||
- id: "risk-5"
|
||||
description: "Vector index corruption on unexpected shutdown"
|
||||
likelihood: "medium"
|
||||
impact: "high"
|
||||
mitigation: "WAL logging, periodic snapshots, automatic recovery"
|
||||
|
||||
# ============================================================================
|
||||
# PACKAGE STRUCTURE
|
||||
# ============================================================================
|
||||
package_structure:
|
||||
root: "npm/packages/ruvbot"
|
||||
directories:
|
||||
- path: "src/core"
|
||||
purpose: "Domain entities and core types"
|
||||
files:
|
||||
- "entities/Agent.ts"
|
||||
- "entities/Session.ts"
|
||||
- "entities/Message.ts"
|
||||
- "entities/Skill.ts"
|
||||
- "types.ts"
|
||||
- "errors.ts"
|
||||
- "Result.ts"
|
||||
- "BotConfig.ts"
|
||||
- "BotState.ts"
|
||||
|
||||
- path: "src/infrastructure"
|
||||
purpose: "Database, messaging, and worker infrastructure"
|
||||
files:
|
||||
- "storage/SessionStore.ts"
|
||||
- "storage/adapters/SQLiteAdapter.ts"
|
||||
- "storage/adapters/PostgresAdapter.ts"
|
||||
- "messaging/MessageQueue.ts"
|
||||
- "messaging/InMemoryQueue.ts"
|
||||
- "messaging/RedisQueue.ts"
|
||||
- "workers/WorkerPool.ts"
|
||||
- "workers/Worker.ts"
|
||||
- "workers/TaskScheduler.ts"
|
||||
- "events/EventBus.ts"
|
||||
- "health/HealthChecker.ts"
|
||||
|
||||
- path: "src/learning"
|
||||
purpose: "WASM embeddings, vector memory, and ruvllm integration"
|
||||
files:
|
||||
- "memory/MemoryManager.ts"
|
||||
- "memory/VectorIndex.ts"
|
||||
- "embeddings/WasmEmbedder.ts"
|
||||
- "embeddings/EmbeddingCache.ts"
|
||||
- "llm/LLMOrchestrator.ts"
|
||||
- "llm/ModelRouter.ts"
|
||||
- "trajectory/TrajectoryRecorder.ts"
|
||||
- "trajectory/PatternExtractor.ts"
|
||||
- "retrieval/SemanticSearch.ts"
|
||||
|
||||
- path: "src/skills"
|
||||
purpose: "Extensible skill system"
|
||||
files:
|
||||
- "SkillRegistry.ts"
|
||||
- "SkillLoader.ts"
|
||||
- "SkillContext.ts"
|
||||
- "HotReloader.ts"
|
||||
- "builtin/SearchSkill.ts"
|
||||
- "builtin/SummarizeSkill.ts"
|
||||
- "builtin/CodeSkill.ts"
|
||||
- "builtin/MemorySkill.ts"
|
||||
|
||||
- path: "src/integrations"
|
||||
purpose: "Slack, Discord, and webhook integrations"
|
||||
files:
|
||||
- "ChatAdapter.ts"
|
||||
- "AdapterFactory.ts"
|
||||
- "slack/SlackAdapter.ts"
|
||||
- "slack/SlackEventHandler.ts"
|
||||
- "discord/DiscordAdapter.ts"
|
||||
- "webhooks/WebhookServer.ts"
|
||||
|
||||
- path: "src/api"
|
||||
purpose: "REST and GraphQL endpoints"
|
||||
files:
|
||||
- "rest/server.ts"
|
||||
- "rest/routes/chat.ts"
|
||||
- "rest/routes/sessions.ts"
|
||||
- "rest/routes/skills.ts"
|
||||
- "graphql/schema.ts"
|
||||
- "graphql/resolvers/*.ts"
|
||||
|
||||
- path: "src/cli"
|
||||
purpose: "CLI interface"
|
||||
files:
|
||||
- "index.ts"
|
||||
- "commands/start.ts"
|
||||
- "commands/config.ts"
|
||||
- "commands/skills.ts"
|
||||
- "modes/local.ts"
|
||||
- "modes/remote.ts"
|
||||
- "wizard/ConfigWizard.ts"
|
||||
|
||||
- path: "bin"
|
||||
purpose: "CLI entry point for npx"
|
||||
files:
|
||||
- "cli.js"
|
||||
|
||||
- path: "tests"
|
||||
purpose: "Test suites"
|
||||
files:
|
||||
- "unit/**/*.test.ts"
|
||||
- "integration/**/*.test.ts"
|
||||
- "e2e/**/*.test.ts"
|
||||
|
||||
- path: "scripts"
|
||||
purpose: "Installation and utility scripts"
|
||||
files:
|
||||
- "install.sh"
|
||||
- "uninstall.sh"
|
||||
|
||||
# ============================================================================
|
||||
# DEPENDENCIES
|
||||
# ============================================================================
|
||||
dependencies:
|
||||
production:
|
||||
core:
|
||||
- name: "@ruvector/wasm-unified"
|
||||
version: "^1.0.0"
|
||||
purpose: "WASM embeddings and attention mechanisms"
|
||||
|
||||
- name: "@ruvector/ruvllm"
|
||||
version: "^2.3.0"
|
||||
purpose: "LLM orchestration with SONA learning"
|
||||
|
||||
- name: "@ruvector/postgres-cli"
|
||||
version: "^0.2.6"
|
||||
purpose: "PostgreSQL vector storage"
|
||||
|
||||
infrastructure:
|
||||
- name: "better-sqlite3"
|
||||
version: "^9.0.0"
|
||||
purpose: "Local SQLite storage"
|
||||
|
||||
- name: "ioredis"
|
||||
version: "^5.3.0"
|
||||
purpose: "Redis message queue"
|
||||
|
||||
- name: "fastify"
|
||||
version: "^4.24.0"
|
||||
purpose: "REST API server"
|
||||
|
||||
integrations:
|
||||
- name: "@slack/bolt"
|
||||
version: "^3.16.0"
|
||||
purpose: "Slack bot framework"
|
||||
|
||||
- name: "discord.js"
|
||||
version: "^14.14.0"
|
||||
purpose: "Discord integration"
|
||||
optional: true
|
||||
|
||||
cli:
|
||||
- name: "commander"
|
||||
version: "^12.0.0"
|
||||
purpose: "CLI framework"
|
||||
|
||||
- name: "chalk"
|
||||
version: "^4.1.2"
|
||||
purpose: "Terminal styling"
|
||||
|
||||
- name: "ora"
|
||||
version: "^5.4.1"
|
||||
purpose: "Terminal spinners"
|
||||
|
||||
- name: "inquirer"
|
||||
version: "^9.2.0"
|
||||
purpose: "Interactive prompts"
|
||||
|
||||
development:
|
||||
- name: "typescript"
|
||||
version: "^5.3.0"
|
||||
|
||||
- name: "tsup"
|
||||
version: "^8.0.0"
|
||||
purpose: "Build tool"
|
||||
|
||||
- name: "vitest"
|
||||
version: "^1.1.0"
|
||||
purpose: "Testing framework"
|
||||
|
||||
- name: "@types/node"
|
||||
version: "^20.10.0"
|
||||
|
||||
# ============================================================================
|
||||
# NPX DISTRIBUTION
|
||||
# ============================================================================
|
||||
npx_distribution:
|
||||
package_name: "@ruvector/ruvbot"
|
||||
binary_name: "ruvbot"
|
||||
|
||||
commands:
|
||||
- command: "npx @ruvector/ruvbot init"
|
||||
description: "Initialize RuvBot in current directory"
|
||||
|
||||
- command: "npx @ruvector/ruvbot start"
|
||||
description: "Start bot in local mode"
|
||||
|
||||
- command: "npx @ruvector/ruvbot start --remote"
|
||||
description: "Start bot connected to remote services"
|
||||
|
||||
- command: "npx @ruvector/ruvbot config"
|
||||
description: "Interactive configuration wizard"
|
||||
|
||||
- command: "npx @ruvector/ruvbot skills list"
|
||||
description: "List available skills"
|
||||
|
||||
- command: "npx @ruvector/ruvbot skills add <name>"
|
||||
description: "Add a skill from registry"
|
||||
|
||||
- command: "npx @ruvector/ruvbot status"
|
||||
description: "Show bot status and health"
|
||||
|
||||
install_script:
|
||||
url: "https://get.ruvector.dev/ruvbot"
|
||||
method: "curl -fsSL https://get.ruvector.dev/ruvbot | bash"
|
||||
|
||||
environment_variables:
|
||||
required:
|
||||
- name: "SLACK_BOT_TOKEN"
|
||||
description: "Slack bot OAuth token"
|
||||
|
||||
- name: "SLACK_SIGNING_SECRET"
|
||||
description: "Slack app signing secret"
|
||||
|
||||
optional:
|
||||
- name: "RUVBOT_PORT"
|
||||
description: "HTTP server port"
|
||||
default: "3000"
|
||||
|
||||
- name: "RUVBOT_LOG_LEVEL"
|
||||
description: "Logging verbosity"
|
||||
default: "info"
|
||||
|
||||
- name: "RUVBOT_STORAGE"
|
||||
description: "Storage backend (sqlite|postgres|memory)"
|
||||
default: "sqlite"
|
||||
|
||||
- name: "RUVBOT_MEMORY_PATH"
|
||||
description: "Path for vector memory storage"
|
||||
default: "./data/memory"
|
||||
|
||||
- name: "DATABASE_URL"
|
||||
description: "PostgreSQL connection string"
|
||||
|
||||
- name: "REDIS_URL"
|
||||
description: "Redis connection string"
|
||||
|
||||
- name: "ANTHROPIC_API_KEY"
|
||||
description: "Anthropic API key for Claude"
|
||||
|
||||
- name: "OPENAI_API_KEY"
|
||||
description: "OpenAI API key"
|
||||
|
||||
# ============================================================================
|
||||
# CONFIGURATION FILES
|
||||
# ============================================================================
|
||||
config_files:
|
||||
- name: "ruvbot.config.json"
|
||||
purpose: "Main configuration file"
|
||||
example: |
|
||||
{
|
||||
"name": "my-ruvbot",
|
||||
"port": 3000,
|
||||
"storage": {
|
||||
"type": "sqlite",
|
||||
"path": "./data/ruvbot.db"
|
||||
},
|
||||
"memory": {
|
||||
"dimensions": 384,
|
||||
"maxVectors": 100000,
|
||||
"indexType": "hnsw"
|
||||
},
|
||||
"skills": {
|
||||
"enabled": ["search", "summarize", "code", "memory"],
|
||||
"custom": ["./skills/*.js"]
|
||||
},
|
||||
"integrations": {
|
||||
"slack": {
|
||||
"enabled": true,
|
||||
"socketMode": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- name: ".env"
|
||||
purpose: "Environment variables"
|
||||
example: |
|
||||
SLACK_BOT_TOKEN=xoxb-xxx
|
||||
SLACK_SIGNING_SECRET=xxx
|
||||
SLACK_APP_TOKEN=xapp-xxx
|
||||
ANTHROPIC_API_KEY=sk-ant-xxx
|
||||
|
||||
# ============================================================================
|
||||
# MILESTONES
|
||||
# ============================================================================
|
||||
milestones:
|
||||
- name: "M1: Core Bot"
|
||||
date: "Week 2"
|
||||
deliverables:
|
||||
- "RuvBot class with lifecycle management"
|
||||
- "Core entities (Agent, Session, Message)"
|
||||
- "Basic unit tests"
|
||||
|
||||
- name: "M2: Infrastructure"
|
||||
date: "Week 3"
|
||||
deliverables:
|
||||
- "Session persistence"
|
||||
- "Message queue"
|
||||
- "Worker pool"
|
||||
|
||||
- name: "M3: Learning"
|
||||
date: "Week 4"
|
||||
deliverables:
|
||||
- "WASM embeddings working"
|
||||
- "Vector memory with HNSW"
|
||||
- "Semantic search"
|
||||
|
||||
- name: "M4: Skills & Integrations"
|
||||
date: "Week 5"
|
||||
deliverables:
|
||||
- "Skill registry with built-in skills"
|
||||
- "Slack integration working"
|
||||
|
||||
- name: "M5: API & CLI"
|
||||
date: "Week 6"
|
||||
deliverables:
|
||||
- "REST API"
|
||||
- "CLI with npx support"
|
||||
|
||||
- name: "M6: Production Ready"
|
||||
date: "Week 8"
|
||||
deliverables:
|
||||
- "85%+ test coverage"
|
||||
- "Performance benchmarks passing"
|
||||
- "Published to npm"
|
||||
|
||||
# ============================================================================
|
||||
# TEAM ALLOCATION
|
||||
# ============================================================================
|
||||
team_allocation:
|
||||
agents:
|
||||
- role: "architect"
|
||||
tasks: ["p1-t2", "p3-t1", "p4-t1"]
|
||||
focus: "System design and core architecture"
|
||||
|
||||
- role: "coder"
|
||||
tasks: ["p1-t1", "p1-t3", "p2-*", "p3-*", "p5-*", "p6-*", "p7-*"]
|
||||
focus: "Implementation"
|
||||
|
||||
- role: "tester"
|
||||
tasks: ["p1-t5", "p8-*"]
|
||||
focus: "Testing and quality assurance"
|
||||
|
||||
- role: "reviewer"
|
||||
tasks: ["all"]
|
||||
focus: "Code review and security"
|
||||
|
||||
# ============================================================================
|
||||
# QUALITY GATES
|
||||
# ============================================================================
|
||||
quality_gates:
|
||||
- name: "Unit Test Coverage"
|
||||
threshold: ">= 80%"
|
||||
tool: "vitest"
|
||||
|
||||
- name: "Type Coverage"
|
||||
threshold: ">= 95%"
|
||||
tool: "typescript --noEmit"
|
||||
|
||||
- name: "No High Severity Vulnerabilities"
|
||||
threshold: "0 high/critical"
|
||||
tool: "npm audit"
|
||||
|
||||
- name: "Performance Benchmarks"
|
||||
thresholds:
|
||||
- metric: "embedding_latency"
|
||||
value: "< 50ms"
|
||||
- metric: "vector_search_latency"
|
||||
value: "< 10ms"
|
||||
- metric: "message_throughput"
|
||||
value: "> 1000 msg/s"
|
||||
172
vendor/ruvector/npm/packages/ruvbot/docs/adr/ADR-001-architecture-overview.md
vendored
Normal file
172
vendor/ruvector/npm/packages/ruvbot/docs/adr/ADR-001-architecture-overview.md
vendored
Normal file
@@ -0,0 +1,172 @@
|
||||
# ADR-001: RuvBot Architecture Overview
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Date
|
||||
2026-01-27
|
||||
|
||||
## Context
|
||||
|
||||
We need to build **RuvBot**, a Clawdbot-style personal AI assistant with a RuVector backend. The system must:
|
||||
|
||||
1. Provide a self-hosted, extensible AI assistant framework
|
||||
2. Integrate with RuVector's WASM-based vector operations for SOTA learning
|
||||
3. Support multi-tenancy for enterprise deployments
|
||||
4. Enable long-running tasks via background workers
|
||||
5. Integrate with messaging platforms (Slack, Discord, webhooks)
|
||||
6. Distribute as an `npx` package with local/remote deployment options
|
||||
|
||||
## Decision
|
||||
|
||||
### High-Level Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ RuvBot System │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐│
|
||||
│ │ REST API │ │ GraphQL │ │ Slack │ │ Webhooks ││
|
||||
│ │ Endpoints │ │ Gateway │ │ Adapter │ │ Handler ││
|
||||
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘│
|
||||
│ │ │ │ │ │
|
||||
│ ┌──────┴────────────────┴────────────────┴────────────────┴──────┐│
|
||||
│ │ Message Router ││
|
||||
│ └─────────────────────────────┬───────────────────────────────────┘│
|
||||
│ │ │
|
||||
│ ┌─────────────────────────────┴───────────────────────────────────┐│
|
||||
│ │ Core Application Layer ││
|
||||
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ││
|
||||
│ │ │ AgentManager │ │SessionStore │ │ SkillRegistry│ ││
|
||||
│ │ └──────────────┘ └──────────────┘ └──────────────┘ ││
|
||||
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ││
|
||||
│ │ │MemoryManager │ │WorkerPool │ │ EventBus │ ││
|
||||
│ │ └──────────────┘ └──────────────┘ └──────────────┘ ││
|
||||
│ └─────────────────────────────────────────────────────────────────┘│
|
||||
│ │ │
|
||||
│ ┌─────────────────────────────┴───────────────────────────────────┐│
|
||||
│ │ Infrastructure Layer ││
|
||||
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ││
|
||||
│ │ │ RuVector │ │ PostgreSQL │ │ RuvLLM │ ││
|
||||
│ │ │ WASM Engine │ │ + pgvector │ │ Inference │ ││
|
||||
│ │ └──────────────┘ └──────────────┘ └──────────────┘ ││
|
||||
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ││
|
||||
│ │ │ agentic-flow │ │ SONA Learning│ │ HNSW Index │ ││
|
||||
│ │ │ Workers │ │ System │ │ Memory │ ││
|
||||
│ │ └──────────────┘ └──────────────┘ └──────────────┘ ││
|
||||
│ └─────────────────────────────────────────────────────────────────┘│
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### DDD Bounded Contexts
|
||||
|
||||
#### 1. Core Context
|
||||
- **Agent**: The AI agent entity with identity, capabilities, and state
|
||||
- **Session**: Conversation context with message history and metadata
|
||||
- **Memory**: Vector-based memory with HNSW indexing
|
||||
- **Skill**: Extensible capabilities (tools, commands, integrations)
|
||||
|
||||
#### 2. Infrastructure Context
|
||||
- **Persistence**: PostgreSQL with RuVector extensions, pgvector
|
||||
- **Messaging**: Event-driven message bus (Redis/in-memory)
|
||||
- **Workers**: Background task processing via agentic-flow
|
||||
|
||||
#### 3. Integration Context
|
||||
- **Slack**: Slack Bot API adapter
|
||||
- **Webhooks**: Generic webhook handler
|
||||
- **Providers**: LLM provider abstraction (Anthropic, OpenAI, etc.)
|
||||
|
||||
#### 4. Learning Context
|
||||
- **Embeddings**: RuVector WASM vector operations
|
||||
- **Training**: Trajectory learning, LoRA fine-tuning
|
||||
- **Patterns**: Neural pattern storage and retrieval
|
||||
|
||||
### Technology Stack
|
||||
|
||||
| Layer | Technology | Purpose |
|
||||
|-------|------------|---------|
|
||||
| Runtime | Node.js 18+ | Primary runtime |
|
||||
| Language | TypeScript (ESM) | Type-safe development |
|
||||
| Vector Engine | @ruvector/wasm-unified | SIMD-optimized vectors |
|
||||
| LLM Layer | @ruvector/ruvllm | SONA, LoRA, inference |
|
||||
| Database | PostgreSQL + pgvector | Persistence + vectors |
|
||||
| Workers | agentic-flow | Background processing |
|
||||
| Testing | Vitest | Unit/Integration/E2E |
|
||||
| CLI | Commander.js | npx distribution |
|
||||
|
||||
### Package Structure
|
||||
|
||||
```
|
||||
npm/packages/ruvbot/
|
||||
├── bin/ # CLI entry points
|
||||
│ └── ruvbot.ts # npx ruvbot entry
|
||||
├── src/
|
||||
│ ├── core/ # Domain layer
|
||||
│ │ ├── entities/ # Agent, Session, Memory, Skill
|
||||
│ │ ├── services/ # AgentManager, SessionStore, etc.
|
||||
│ │ └── events/ # Domain events
|
||||
│ ├── infrastructure/ # Infrastructure layer
|
||||
│ │ ├── persistence/ # PostgreSQL, SQLite adapters
|
||||
│ │ ├── messaging/ # Event bus, message queue
|
||||
│ │ └── workers/ # agentic-flow integration
|
||||
│ ├── integrations/ # External integrations
|
||||
│ │ ├── slack/ # Slack adapter
|
||||
│ │ ├── webhooks/ # Webhook handlers
|
||||
│ │ └── providers/ # LLM providers
|
||||
│ ├── learning/ # Learning system
|
||||
│ │ ├── embeddings/ # WASM vector ops
|
||||
│ │ ├── training/ # LoRA, SONA
|
||||
│ │ └── patterns/ # Pattern storage
|
||||
│ └── api/ # API layer
|
||||
│ ├── rest/ # REST endpoints
|
||||
│ └── graphql/ # GraphQL schema
|
||||
├── tests/
|
||||
│ ├── unit/
|
||||
│ ├── integration/
|
||||
│ └── e2e/
|
||||
├── docs/
|
||||
│ └── adr/ # Architecture Decision Records
|
||||
└── scripts/ # Build/deploy scripts
|
||||
```
|
||||
|
||||
### Multi-Tenancy Strategy
|
||||
|
||||
1. **Database Level**: Row-Level Security (RLS) with tenant_id
|
||||
2. **Application Level**: Tenant context middleware
|
||||
3. **Memory Level**: Namespace isolation in vector storage
|
||||
4. **Worker Level**: Tenant-scoped job queues
|
||||
|
||||
### Key Design Principles
|
||||
|
||||
1. **Self-Learning**: Every interaction improves the system via SONA
|
||||
2. **WASM-First**: Use RuVector WASM for portable, fast vector ops
|
||||
3. **Event-Driven**: Loose coupling via event bus
|
||||
4. **Extensible**: Plugin architecture for skills and integrations
|
||||
5. **Observable**: Built-in metrics and tracing
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Modular architecture enables independent scaling
|
||||
- WASM integration provides consistent cross-platform performance
|
||||
- Multi-tenancy from day one avoids later refactoring
|
||||
- Self-learning improves over time with usage
|
||||
|
||||
### Negative
|
||||
- Initial complexity is higher than monolithic approach
|
||||
- WASM has some interop overhead
|
||||
- Multi-tenancy adds complexity to all data operations
|
||||
|
||||
### Risks
|
||||
- WASM performance in Node.js may vary by platform
|
||||
- PostgreSQL dependency limits serverless options
|
||||
- Background workers need careful monitoring
|
||||
|
||||
## Related ADRs
|
||||
- ADR-002: Multi-tenancy Design
|
||||
- ADR-003: Persistence Layer
|
||||
- ADR-004: Background Workers
|
||||
- ADR-005: Integration Layer
|
||||
- ADR-006: WASM Integration
|
||||
- ADR-007: Learning System
|
||||
- ADR-008: Security Architecture
|
||||
873
vendor/ruvector/npm/packages/ruvbot/docs/adr/ADR-002-multi-tenancy-design.md
vendored
Normal file
873
vendor/ruvector/npm/packages/ruvbot/docs/adr/ADR-002-multi-tenancy-design.md
vendored
Normal file
@@ -0,0 +1,873 @@
|
||||
# ADR-002: Multi-tenancy Design
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-01-27
|
||||
**Decision Makers:** RuVector Architecture Team
|
||||
**Technical Area:** Security, Data Architecture
|
||||
|
||||
---
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
RuvBot must serve multiple organizations (tenants) and users within each organization while maintaining strict data isolation. A breach of tenant boundaries would:
|
||||
|
||||
1. Violate privacy and compliance requirements (GDPR, SOC2, HIPAA)
|
||||
2. Expose sensitive business information
|
||||
3. Destroy trust in the platform
|
||||
4. Create legal liability
|
||||
|
||||
The multi-tenancy design must address:
|
||||
|
||||
- **Data Isolation**: No cross-tenant data access
|
||||
- **Authentication**: Identity verification at multiple levels
|
||||
- **Authorization**: Fine-grained permission control
|
||||
- **Resource Limits**: Fair usage and cost allocation
|
||||
- **Audit Trails**: Complete visibility into access patterns
|
||||
|
||||
---
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
### Security Requirements
|
||||
|
||||
| Requirement | Criticality | Description |
|
||||
|-------------|-------------|-------------|
|
||||
| Zero cross-tenant leakage | Critical | No tenant can access another tenant's data |
|
||||
| Row-level security | Critical | Database enforces isolation, not just application |
|
||||
| Token-based auth | High | Stateless, revocable authentication |
|
||||
| RBAC + ABAC | High | Role and attribute-based access control |
|
||||
| Audit logging | High | All data access logged with tenant context |
|
||||
|
||||
### Operational Requirements
|
||||
|
||||
| Requirement | Target | Description |
|
||||
|-------------|--------|-------------|
|
||||
| Tenant provisioning | < 30s | New tenant setup time |
|
||||
| User provisioning | < 5s | New user creation time |
|
||||
| Quota enforcement | Real-time | Immediate limit enforcement |
|
||||
| Data export | < 1h for 1GB | GDPR data portability |
|
||||
| Data deletion | < 24h | GDPR right to erasure |
|
||||
|
||||
---
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
### Adopt Hierarchical Multi-tenancy with RLS and JWT Claims
|
||||
|
||||
We implement a three-level hierarchy with PostgreSQL Row-Level Security (RLS) as the primary isolation mechanism.
|
||||
|
||||
```
|
||||
+---------------------------+
|
||||
| ORGANIZATION | Billing entity, security boundary
|
||||
|---------------------------|
|
||||
| id: UUID |
|
||||
| name: string |
|
||||
| plan: Plan |
|
||||
| settings: OrgSettings |
|
||||
| quotas: ResourceQuotas |
|
||||
+-------------+-------------+
|
||||
|
|
||||
| 1:N
|
||||
v
|
||||
+---------------------------+
|
||||
| WORKSPACE | Project/team boundary
|
||||
|---------------------------|
|
||||
| id: UUID |
|
||||
| orgId: UUID (FK) |
|
||||
| name: string |
|
||||
| settings: WorkspaceSettings|
|
||||
+-------------+-------------+
|
||||
|
|
||||
| 1:N
|
||||
v
|
||||
+---------------------------+
|
||||
| USER | Individual identity
|
||||
|---------------------------|
|
||||
| id: UUID |
|
||||
| workspaceId: UUID (FK) |
|
||||
| email: string |
|
||||
| roles: Role[] |
|
||||
| preferences: Preferences |
|
||||
+---------------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tenant Isolation Layers
|
||||
|
||||
### Layer 1: Network Isolation
|
||||
|
||||
```
|
||||
Internet
|
||||
|
|
||||
v
|
||||
+---+---+
|
||||
| WAF | Rate limiting, DDoS protection
|
||||
+---+---+
|
||||
|
|
||||
v
|
||||
+---+---+
|
||||
| LB/TLS| TLS termination, tenant routing
|
||||
+---+---+
|
||||
|
|
||||
+--------+--------+--------+
|
||||
| | | |
|
||||
+---v---+ +---v---+ +---v---+ +---v---+
|
||||
| Org A | | Org B | | Org C | | Org D | Virtual host routing
|
||||
+-------+ +-------+ +-------+ +-------+
|
||||
```
|
||||
|
||||
### Layer 2: Authentication & Authorization
|
||||
|
||||
```typescript
|
||||
// JWT token structure with tenant claims
|
||||
interface RuvBotToken {
|
||||
// Standard claims
|
||||
sub: string; // User ID
|
||||
iat: number; // Issued at
|
||||
exp: number; // Expiration
|
||||
|
||||
// Tenant claims (always present)
|
||||
org_id: string; // Organization ID
|
||||
workspace_id: string; // Workspace ID
|
||||
|
||||
// Permission claims
|
||||
roles: Role[]; // User roles
|
||||
permissions: string[];// Explicit permissions
|
||||
|
||||
// Resource claims
|
||||
quotas: {
|
||||
sessions: number;
|
||||
messages_per_day: number;
|
||||
memory_mb: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Role hierarchy
|
||||
enum Role {
|
||||
ORG_OWNER = 'org:owner',
|
||||
ORG_ADMIN = 'org:admin',
|
||||
WORKSPACE_ADMIN = 'workspace:admin',
|
||||
MEMBER = 'member',
|
||||
VIEWER = 'viewer',
|
||||
API_KEY = 'api_key',
|
||||
}
|
||||
|
||||
// Permission matrix
|
||||
const PERMISSIONS: Record<Role, string[]> = {
|
||||
'org:owner': ['*'],
|
||||
'org:admin': ['org:read', 'org:write', 'workspace:*', 'user:*', 'billing:read'],
|
||||
'workspace:admin': ['workspace:read', 'workspace:write', 'user:read', 'user:invite'],
|
||||
'member': ['session:*', 'memory:read', 'memory:write', 'skill:execute'],
|
||||
'viewer': ['session:read', 'memory:read'],
|
||||
'api_key': ['session:create', 'session:read'],
|
||||
};
|
||||
```
|
||||
|
||||
### Layer 3: Database Row-Level Security
|
||||
|
||||
```sql
|
||||
-- Enable RLS on all tenant-scoped tables
|
||||
ALTER TABLE conversations ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE memories ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE sessions ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE skills ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE trajectories ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Create tenant context function
|
||||
CREATE OR REPLACE FUNCTION current_tenant_id()
|
||||
RETURNS UUID AS $$
|
||||
BEGIN
|
||||
RETURN current_setting('app.current_org_id', true)::UUID;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
CREATE OR REPLACE FUNCTION current_workspace_id()
|
||||
RETURNS UUID AS $$
|
||||
BEGIN
|
||||
RETURN current_setting('app.current_workspace_id', true)::UUID;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- RLS policies for conversations
|
||||
CREATE POLICY conversations_isolation ON conversations
|
||||
FOR ALL
|
||||
USING (org_id = current_tenant_id())
|
||||
WITH CHECK (org_id = current_tenant_id());
|
||||
|
||||
-- RLS policies for memories (workspace-level)
|
||||
CREATE POLICY memories_isolation ON memories
|
||||
FOR ALL
|
||||
USING (
|
||||
org_id = current_tenant_id()
|
||||
AND workspace_id = current_workspace_id()
|
||||
);
|
||||
|
||||
-- Read-only policy for cross-workspace memory sharing
|
||||
CREATE POLICY memories_shared_read ON memories
|
||||
FOR SELECT
|
||||
USING (
|
||||
org_id = current_tenant_id()
|
||||
AND is_shared = true
|
||||
);
|
||||
```
|
||||
|
||||
### Layer 4: Vector Store Isolation
|
||||
|
||||
```typescript
|
||||
// Namespace isolation in RuVector
|
||||
interface VectorNamespace {
|
||||
// Namespace format: {org_id}/{workspace_id}/{collection}
|
||||
// Example: "550e8400-e29b/.../episodic"
|
||||
|
||||
encode(orgId: string, workspaceId: string, collection: string): string;
|
||||
decode(namespace: string): { orgId: string; workspaceId: string; collection: string };
|
||||
validate(namespace: string, token: RuvBotToken): boolean;
|
||||
}
|
||||
|
||||
// Vector store with tenant isolation
|
||||
class TenantIsolatedVectorStore {
|
||||
constructor(
|
||||
private store: RuVectorAdapter,
|
||||
private tenantContext: TenantContext
|
||||
) {}
|
||||
|
||||
async search(query: Float32Array, options: SearchOptions): Promise<SearchResult[]> {
|
||||
const namespace = this.getNamespace(options.collection);
|
||||
|
||||
// Validate namespace matches token claims
|
||||
if (!this.validateNamespace(namespace)) {
|
||||
throw new TenantIsolationError('Namespace mismatch');
|
||||
}
|
||||
|
||||
return this.store.search(query, { ...options, namespace });
|
||||
}
|
||||
|
||||
private getNamespace(collection: string): string {
|
||||
return `${this.tenantContext.orgId}/${this.tenantContext.workspaceId}/${collection}`;
|
||||
}
|
||||
|
||||
private validateNamespace(namespace: string): boolean {
|
||||
const { orgId, workspaceId } = VectorNamespace.decode(namespace);
|
||||
return (
|
||||
orgId === this.tenantContext.orgId &&
|
||||
workspaceId === this.tenantContext.workspaceId
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Partitioning Strategy
|
||||
|
||||
### PostgreSQL Partitioning
|
||||
|
||||
```sql
|
||||
-- Partition conversations by org_id for isolation and performance
|
||||
CREATE TABLE conversations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
org_id UUID NOT NULL,
|
||||
workspace_id UUID NOT NULL,
|
||||
session_id UUID NOT NULL,
|
||||
user_id UUID NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
role VARCHAR(20) NOT NULL,
|
||||
embedding_id UUID,
|
||||
metadata JSONB,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
) PARTITION BY LIST (org_id);
|
||||
|
||||
-- Create partition per organization
|
||||
CREATE OR REPLACE FUNCTION create_org_partition(org_id UUID)
|
||||
RETURNS void AS $$
|
||||
DECLARE
|
||||
partition_name TEXT;
|
||||
BEGIN
|
||||
partition_name := 'conversations_' || replace(org_id::text, '-', '_');
|
||||
EXECUTE format(
|
||||
'CREATE TABLE IF NOT EXISTS %I PARTITION OF conversations FOR VALUES IN (%L)',
|
||||
partition_name,
|
||||
org_id
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Indexes per partition
|
||||
CREATE INDEX CONCURRENTLY conversations_session_idx
|
||||
ON conversations (session_id, created_at DESC);
|
||||
CREATE INDEX CONCURRENTLY conversations_user_idx
|
||||
ON conversations (user_id, created_at DESC);
|
||||
CREATE INDEX CONCURRENTLY conversations_embedding_idx
|
||||
ON conversations (embedding_id) WHERE embedding_id IS NOT NULL;
|
||||
```
|
||||
|
||||
### Vector Store Partitioning
|
||||
|
||||
```typescript
|
||||
// HNSW index per tenant for isolation and independent scaling
|
||||
interface TenantVectorIndex {
|
||||
orgId: string;
|
||||
workspaceId: string;
|
||||
collection: 'episodic' | 'semantic' | 'skills';
|
||||
|
||||
// Index configuration (can vary per tenant plan)
|
||||
config: {
|
||||
dimensions: number; // 384 for MiniLM, 1536 for larger models
|
||||
m: number; // HNSW connections (16-32)
|
||||
efConstruction: number; // Build quality (100-200)
|
||||
efSearch: number; // Query quality (50-100)
|
||||
};
|
||||
|
||||
// Usage metrics
|
||||
metrics: {
|
||||
vectorCount: number;
|
||||
memoryUsageMB: number;
|
||||
avgSearchLatencyMs: number;
|
||||
lastOptimized: Date;
|
||||
};
|
||||
}
|
||||
|
||||
// Index lifecycle management
|
||||
class TenantIndexManager {
|
||||
async provisionTenant(orgId: string): Promise<void> {
|
||||
// Create default workspaces indices
|
||||
await this.createIndex(orgId, 'default', 'episodic');
|
||||
await this.createIndex(orgId, 'default', 'semantic');
|
||||
await this.createIndex(orgId, 'default', 'skills');
|
||||
}
|
||||
|
||||
async deleteTenant(orgId: string): Promise<void> {
|
||||
// Delete all indices for org (GDPR deletion)
|
||||
const indices = await this.listIndices(orgId);
|
||||
await Promise.all(indices.map(idx => this.deleteIndex(idx.id)));
|
||||
|
||||
// Log deletion for audit
|
||||
await this.auditLog.record({
|
||||
action: 'tenant_deletion',
|
||||
orgId,
|
||||
indexCount: indices.length,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
async optimizeIndex(indexId: string): Promise<OptimizationResult> {
|
||||
// Background optimization with tenant resource limits
|
||||
const index = await this.getIndex(indexId);
|
||||
const quota = await this.getQuota(index.orgId);
|
||||
|
||||
if (index.metrics.memoryUsageMB > quota.maxVectorMemoryMB) {
|
||||
// Apply quantization to reduce memory
|
||||
return this.compressIndex(indexId, 'product_quantization');
|
||||
}
|
||||
|
||||
return this.rebalanceIndex(indexId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Authentication Flows
|
||||
|
||||
### OAuth2/OIDC Flow
|
||||
|
||||
```
|
||||
+--------+ +--------+
|
||||
| User | | IdP |
|
||||
+---+----+ +---+----+
|
||||
| |
|
||||
| 1. Login request |
|
||||
+--------------------------------------->|
|
||||
| |
|
||||
| 2. Redirect to IdP |
|
||||
|<---------------------------------------+
|
||||
| |
|
||||
| 3. Authenticate + consent |
|
||||
+--------------------------------------->|
|
||||
| |
|
||||
| 4. Auth code redirect |
|
||||
|<---------------------------------------+
|
||||
| |
|
||||
| +--------+ |
|
||||
| 5. Auth code | RuvBot | |
|
||||
+------------------>| Auth | |
|
||||
| +---+----+ |
|
||||
| | |
|
||||
| 6. Exchange code | |
|
||||
| +--------------->|
|
||||
| | |
|
||||
| 7. ID + Access token | |
|
||||
| |<---------------+
|
||||
| | |
|
||||
| 8. Create session, |
|
||||
| issue RuvBot JWT |
|
||||
|<----------------------+
|
||||
| |
|
||||
| 9. Authenticated |
|
||||
+<----------------------+
|
||||
```
|
||||
|
||||
### API Key Authentication
|
||||
|
||||
```typescript
|
||||
// API key structure
|
||||
interface APIKey {
|
||||
id: string;
|
||||
keyHash: string; // SHA-256 hash of actual key
|
||||
prefix: string; // First 8 chars for identification
|
||||
orgId: string;
|
||||
workspaceId: string;
|
||||
name: string;
|
||||
permissions: string[];
|
||||
rateLimit: RateLimitConfig;
|
||||
expiresAt: Date | null;
|
||||
lastUsedAt: Date | null;
|
||||
createdBy: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
// API key validation middleware
|
||||
async function validateAPIKey(req: Request): Promise<TenantContext> {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
throw new AuthenticationError('Missing authorization header');
|
||||
}
|
||||
|
||||
const key = authHeader.slice(7);
|
||||
const prefix = key.slice(0, 8);
|
||||
const keyHash = crypto.createHash('sha256').update(key).digest('hex');
|
||||
|
||||
// Lookup by prefix, then verify hash (timing-safe)
|
||||
const apiKey = await db.apiKeys.findByPrefix(prefix);
|
||||
if (!apiKey || !crypto.timingSafeEqual(
|
||||
Buffer.from(apiKey.keyHash),
|
||||
Buffer.from(keyHash)
|
||||
)) {
|
||||
throw new AuthenticationError('Invalid API key');
|
||||
}
|
||||
|
||||
// Check expiration
|
||||
if (apiKey.expiresAt && apiKey.expiresAt < new Date()) {
|
||||
throw new AuthenticationError('API key expired');
|
||||
}
|
||||
|
||||
// Update last used (async, don't block)
|
||||
db.apiKeys.updateLastUsed(apiKey.id).catch(console.error);
|
||||
|
||||
return {
|
||||
orgId: apiKey.orgId,
|
||||
workspaceId: apiKey.workspaceId,
|
||||
userId: apiKey.createdBy,
|
||||
roles: [Role.API_KEY],
|
||||
permissions: apiKey.permissions,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Resource Quotas and Rate Limiting
|
||||
|
||||
### Quota Configuration
|
||||
|
||||
```typescript
|
||||
// Plan-based quota tiers
|
||||
interface ResourceQuotas {
|
||||
// Session limits
|
||||
maxConcurrentSessions: number;
|
||||
maxSessionDurationMinutes: number;
|
||||
maxTurnsPerSession: number;
|
||||
|
||||
// Memory limits
|
||||
maxMemoriesPerWorkspace: number;
|
||||
maxVectorStorageMB: number;
|
||||
maxEmbeddingsPerDay: number;
|
||||
|
||||
// Compute limits
|
||||
maxLLMTokensPerDay: number;
|
||||
maxSkillExecutionsPerDay: number;
|
||||
maxBackgroundJobsPerHour: number;
|
||||
|
||||
// Rate limits
|
||||
requestsPerMinute: number;
|
||||
requestsPerHour: number;
|
||||
burstLimit: number;
|
||||
}
|
||||
|
||||
const PLAN_QUOTAS: Record<Plan, ResourceQuotas> = {
|
||||
free: {
|
||||
maxConcurrentSessions: 2,
|
||||
maxSessionDurationMinutes: 30,
|
||||
maxTurnsPerSession: 50,
|
||||
maxMemoriesPerWorkspace: 1000,
|
||||
maxVectorStorageMB: 50,
|
||||
maxEmbeddingsPerDay: 500,
|
||||
maxLLMTokensPerDay: 10000,
|
||||
maxSkillExecutionsPerDay: 100,
|
||||
maxBackgroundJobsPerHour: 10,
|
||||
requestsPerMinute: 20,
|
||||
requestsPerHour: 500,
|
||||
burstLimit: 5,
|
||||
},
|
||||
pro: {
|
||||
maxConcurrentSessions: 10,
|
||||
maxSessionDurationMinutes: 120,
|
||||
maxTurnsPerSession: 500,
|
||||
maxMemoriesPerWorkspace: 50000,
|
||||
maxVectorStorageMB: 1000,
|
||||
maxEmbeddingsPerDay: 10000,
|
||||
maxLLMTokensPerDay: 500000,
|
||||
maxSkillExecutionsPerDay: 5000,
|
||||
maxBackgroundJobsPerHour: 200,
|
||||
requestsPerMinute: 100,
|
||||
requestsPerHour: 5000,
|
||||
burstLimit: 20,
|
||||
},
|
||||
enterprise: {
|
||||
maxConcurrentSessions: -1, // Unlimited
|
||||
maxSessionDurationMinutes: -1,
|
||||
maxTurnsPerSession: -1,
|
||||
maxMemoriesPerWorkspace: -1,
|
||||
maxVectorStorageMB: -1,
|
||||
maxEmbeddingsPerDay: -1,
|
||||
maxLLMTokensPerDay: -1,
|
||||
maxSkillExecutionsPerDay: -1,
|
||||
maxBackgroundJobsPerHour: -1,
|
||||
requestsPerMinute: 500,
|
||||
requestsPerHour: 20000,
|
||||
burstLimit: 50,
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Rate Limiter Implementation
|
||||
|
||||
```typescript
|
||||
// Token bucket rate limiter with Redis backend
|
||||
class TenantRateLimiter {
|
||||
constructor(private redis: Redis) {}
|
||||
|
||||
async checkLimit(
|
||||
tenantId: string,
|
||||
action: string,
|
||||
config: RateLimitConfig
|
||||
): Promise<RateLimitResult> {
|
||||
const key = `ratelimit:${tenantId}:${action}`;
|
||||
const now = Date.now();
|
||||
const windowMs = config.windowMs || 60000;
|
||||
|
||||
// Lua script for atomic rate limit check
|
||||
const result = await this.redis.eval(`
|
||||
local key = KEYS[1]
|
||||
local now = tonumber(ARGV[1])
|
||||
local window = tonumber(ARGV[2])
|
||||
local limit = tonumber(ARGV[3])
|
||||
local burst = tonumber(ARGV[4])
|
||||
|
||||
-- Remove expired entries
|
||||
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
|
||||
|
||||
-- Count current requests
|
||||
local count = redis.call('ZCARD', key)
|
||||
|
||||
-- Check burst limit (recent 1s)
|
||||
local burstCount = redis.call('ZCOUNT', key, now - 1000, now)
|
||||
|
||||
if burstCount >= burst then
|
||||
return {0, limit - count, burst - burstCount, now + 1000}
|
||||
end
|
||||
|
||||
if count >= limit then
|
||||
local oldest = redis.call('ZRANGE', key, 0, 0, 'WITHSCORES')
|
||||
local retryAfter = oldest[2] + window - now
|
||||
return {0, 0, burst - burstCount, retryAfter}
|
||||
end
|
||||
|
||||
-- Add current request
|
||||
redis.call('ZADD', key, now, now .. ':' .. math.random())
|
||||
redis.call('PEXPIRE', key, window)
|
||||
|
||||
return {1, limit - count - 1, burst - burstCount - 1, 0}
|
||||
`, 1, key, now, windowMs, config.limit, config.burstLimit);
|
||||
|
||||
const [allowed, remaining, burstRemaining, retryAfter] = result as number[];
|
||||
|
||||
return {
|
||||
allowed: allowed === 1,
|
||||
remaining,
|
||||
burstRemaining,
|
||||
retryAfter: retryAfter > 0 ? Math.ceil(retryAfter / 1000) : 0,
|
||||
limit: config.limit,
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Audit Logging
|
||||
|
||||
```typescript
|
||||
// Comprehensive audit trail
|
||||
interface AuditEvent {
|
||||
id: string;
|
||||
timestamp: Date;
|
||||
|
||||
// Tenant context
|
||||
orgId: string;
|
||||
workspaceId: string;
|
||||
userId: string;
|
||||
|
||||
// Event details
|
||||
action: AuditAction;
|
||||
resource: AuditResource;
|
||||
resourceId: string;
|
||||
|
||||
// Request context
|
||||
requestId: string;
|
||||
ipAddress: string;
|
||||
userAgent: string;
|
||||
|
||||
// Change tracking
|
||||
before?: Record<string, unknown>;
|
||||
after?: Record<string, unknown>;
|
||||
|
||||
// Outcome
|
||||
status: 'success' | 'failure' | 'denied';
|
||||
errorCode?: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
type AuditAction =
|
||||
| 'create' | 'read' | 'update' | 'delete'
|
||||
| 'login' | 'logout' | 'token_refresh'
|
||||
| 'export' | 'import'
|
||||
| 'share' | 'unshare'
|
||||
| 'invite' | 'remove'
|
||||
| 'skill_execute' | 'memory_recall'
|
||||
| 'quota_exceeded' | 'rate_limited';
|
||||
|
||||
type AuditResource =
|
||||
| 'user' | 'session' | 'conversation'
|
||||
| 'memory' | 'skill' | 'agent'
|
||||
| 'workspace' | 'organization'
|
||||
| 'api_key' | 'webhook';
|
||||
|
||||
// Audit logger with async persistence
|
||||
class AuditLogger {
|
||||
private buffer: AuditEvent[] = [];
|
||||
private flushInterval: NodeJS.Timeout;
|
||||
|
||||
constructor(
|
||||
private storage: AuditStorage,
|
||||
private config: { batchSize: number; flushMs: number }
|
||||
) {
|
||||
this.flushInterval = setInterval(() => this.flush(), config.flushMs);
|
||||
}
|
||||
|
||||
async log(event: Omit<AuditEvent, 'id' | 'timestamp'>): Promise<void> {
|
||||
const fullEvent: AuditEvent = {
|
||||
...event,
|
||||
id: crypto.randomUUID(),
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
this.buffer.push(fullEvent);
|
||||
|
||||
if (this.buffer.length >= this.config.batchSize) {
|
||||
await this.flush();
|
||||
}
|
||||
}
|
||||
|
||||
private async flush(): Promise<void> {
|
||||
if (this.buffer.length === 0) return;
|
||||
|
||||
const events = this.buffer.splice(0, this.buffer.length);
|
||||
await this.storage.batchInsert(events);
|
||||
}
|
||||
|
||||
async query(filter: AuditFilter): Promise<AuditEvent[]> {
|
||||
// Ensure tenant isolation in queries
|
||||
if (!filter.orgId) {
|
||||
throw new Error('orgId required for audit queries');
|
||||
}
|
||||
return this.storage.query(filter);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GDPR Compliance
|
||||
|
||||
### Data Export
|
||||
|
||||
```typescript
|
||||
// Personal data export for GDPR Article 15
|
||||
class DataExporter {
|
||||
async exportUserData(
|
||||
orgId: string,
|
||||
userId: string
|
||||
): Promise<DataExportResult> {
|
||||
const export = {
|
||||
metadata: {
|
||||
userId,
|
||||
orgId,
|
||||
exportedAt: new Date(),
|
||||
format: 'json',
|
||||
version: '1.0',
|
||||
},
|
||||
data: {} as Record<string, unknown>,
|
||||
};
|
||||
|
||||
// Collect all user data across contexts
|
||||
const [
|
||||
profile,
|
||||
sessions,
|
||||
conversations,
|
||||
memories,
|
||||
preferences,
|
||||
auditLogs,
|
||||
] = await Promise.all([
|
||||
this.exportProfile(userId),
|
||||
this.exportSessions(userId),
|
||||
this.exportConversations(userId),
|
||||
this.exportMemories(userId),
|
||||
this.exportPreferences(userId),
|
||||
this.exportAuditLogs(userId),
|
||||
]);
|
||||
|
||||
export.data = {
|
||||
profile,
|
||||
sessions,
|
||||
conversations,
|
||||
memories,
|
||||
preferences,
|
||||
auditLogs,
|
||||
};
|
||||
|
||||
// Generate downloadable archive
|
||||
const archivePath = await this.createArchive(export);
|
||||
|
||||
// Log export for audit
|
||||
await this.auditLogger.log({
|
||||
orgId,
|
||||
workspaceId: '*',
|
||||
userId,
|
||||
action: 'export',
|
||||
resource: 'user',
|
||||
resourceId: userId,
|
||||
status: 'success',
|
||||
});
|
||||
|
||||
return {
|
||||
downloadUrl: await this.generateSignedUrl(archivePath),
|
||||
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24h
|
||||
sizeBytes: await this.getFileSize(archivePath),
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Data Deletion
|
||||
|
||||
```typescript
|
||||
// Right to erasure (GDPR Article 17)
|
||||
class DataDeleter {
|
||||
async deleteUserData(
|
||||
orgId: string,
|
||||
userId: string,
|
||||
options: DeletionOptions = {}
|
||||
): Promise<DeletionResult> {
|
||||
const jobId = crypto.randomUUID();
|
||||
|
||||
// Start deletion job (may take time for large datasets)
|
||||
await this.jobQueue.enqueue('data-deletion', {
|
||||
jobId,
|
||||
orgId,
|
||||
userId,
|
||||
options,
|
||||
});
|
||||
|
||||
return {
|
||||
jobId,
|
||||
status: 'pending',
|
||||
estimatedCompletionTime: await this.estimateCompletionTime(userId),
|
||||
};
|
||||
}
|
||||
|
||||
async executeDeletion(job: DeletionJob): Promise<void> {
|
||||
const { orgId, userId, options } = job.data;
|
||||
|
||||
// Order matters: delete dependent data first
|
||||
const steps = [
|
||||
{ name: 'sessions', fn: () => this.deleteSessions(userId) },
|
||||
{ name: 'conversations', fn: () => this.deleteConversations(userId) },
|
||||
{ name: 'memories', fn: () => this.deleteMemories(userId, options.preserveShared) },
|
||||
{ name: 'embeddings', fn: () => this.deleteEmbeddings(userId) },
|
||||
{ name: 'trajectories', fn: () => this.deleteTrajectories(userId) },
|
||||
{ name: 'preferences', fn: () => this.deletePreferences(userId) },
|
||||
{ name: 'audit_logs', fn: () => this.anonymizeAuditLogs(userId) }, // Anonymize, not delete
|
||||
{ name: 'profile', fn: () => this.deleteProfile(userId) },
|
||||
];
|
||||
|
||||
for (const step of steps) {
|
||||
try {
|
||||
const result = await step.fn();
|
||||
await this.updateProgress(job.id, step.name, 'completed', result);
|
||||
} catch (error) {
|
||||
await this.updateProgress(job.id, step.name, 'failed', error);
|
||||
throw error; // Fail job, require manual intervention
|
||||
}
|
||||
}
|
||||
|
||||
// Final audit entry (anonymized user reference)
|
||||
await this.auditLogger.log({
|
||||
orgId,
|
||||
workspaceId: '*',
|
||||
userId: 'DELETED_USER',
|
||||
action: 'delete',
|
||||
resource: 'user',
|
||||
resourceId: userId.slice(0, 8) + '...',
|
||||
status: 'success',
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Consequences
|
||||
|
||||
### Benefits
|
||||
|
||||
1. **Strong Isolation**: RLS + namespace isolation prevents cross-tenant access
|
||||
2. **Compliance Ready**: GDPR, SOC2, HIPAA requirements addressed
|
||||
3. **Scalable Quotas**: Per-tenant resource limits enable fair usage
|
||||
4. **Audit Trail**: Complete visibility for security and compliance
|
||||
5. **Flexible Auth**: OAuth2 + API keys support various use cases
|
||||
|
||||
### Risks and Mitigations
|
||||
|
||||
| Risk | Probability | Impact | Mitigation |
|
||||
|------|-------------|--------|------------|
|
||||
| RLS bypass via SQL injection | Low | Critical | Parameterized queries, ORM only |
|
||||
| Token theft | Medium | High | Short expiry, refresh rotation |
|
||||
| Quota gaming (multiple accounts) | Medium | Medium | Device fingerprinting, email verification |
|
||||
| Audit log tampering | Low | High | Append-only storage, checksums |
|
||||
|
||||
---
|
||||
|
||||
## Related Decisions
|
||||
|
||||
- **ADR-001**: Architecture Overview
|
||||
- **ADR-003**: Persistence Layer (RLS implementation details)
|
||||
|
||||
---
|
||||
|
||||
## Revision History
|
||||
|
||||
| Version | Date | Author | Changes |
|
||||
|---------|------|--------|---------|
|
||||
| 1.0 | 2026-01-27 | RuVector Architecture Team | Initial version |
|
||||
952
vendor/ruvector/npm/packages/ruvbot/docs/adr/ADR-003-persistence-layer.md
vendored
Normal file
952
vendor/ruvector/npm/packages/ruvbot/docs/adr/ADR-003-persistence-layer.md
vendored
Normal file
@@ -0,0 +1,952 @@
|
||||
# ADR-003: Persistence Layer
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-01-27
|
||||
**Decision Makers:** RuVector Architecture Team
|
||||
**Technical Area:** Data Architecture, Storage
|
||||
|
||||
---
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
RuvBot requires a persistence layer that handles diverse data types:
|
||||
|
||||
1. **Relational Data**: Users, organizations, sessions, skills (structured, transactional)
|
||||
2. **Vector Data**: Embeddings for memory recall (high-dimensional, similarity search)
|
||||
3. **Session State**: Active conversation context (ephemeral, fast access)
|
||||
4. **Event Streams**: Audit logs, trajectories (append-only, time-series)
|
||||
|
||||
The persistence layer must support:
|
||||
|
||||
- **Multi-tenancy** with strict isolation
|
||||
- **High performance** for real-time conversation
|
||||
- **Durability** for compliance and recovery
|
||||
- **Scalability** for enterprise deployments
|
||||
|
||||
---
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
### Data Characteristics
|
||||
|
||||
| Data Type | Volume | Access Pattern | Consistency | Durability |
|
||||
|-----------|--------|----------------|-------------|------------|
|
||||
| User/Org metadata | Low | Read-heavy | Strong | Required |
|
||||
| Session state | Medium | Read-write balanced | Eventual OK | Nice-to-have |
|
||||
| Conversation history | High | Append-mostly | Strong | Required |
|
||||
| Vector embeddings | Very High | Read-heavy | Eventual OK | Required |
|
||||
| Memory indices | High | Read-heavy | Eventual OK | Nice-to-have |
|
||||
| Audit logs | Very High | Append-only | Strong | Required |
|
||||
|
||||
### Performance Requirements
|
||||
|
||||
| Operation | Target Latency | Target Throughput |
|
||||
|-----------|----------------|-------------------|
|
||||
| Session lookup | < 5ms p99 | 10K/s |
|
||||
| Memory recall (HNSW) | < 50ms p99 | 1K/s |
|
||||
| Conversation insert | < 20ms p99 | 5K/s |
|
||||
| Full-text search | < 100ms p99 | 500/s |
|
||||
| Batch embedding insert | < 500ms p99 | 100 batches/s |
|
||||
|
||||
---
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
### Adopt Polyglot Persistence with Unified API
|
||||
|
||||
We implement a three-tier storage architecture:
|
||||
|
||||
```
|
||||
+-----------------------------------------------------------------------------+
|
||||
| PERSISTENCE LAYER |
|
||||
+-----------------------------------------------------------------------------+
|
||||
|
||||
+--------------------------+
|
||||
| Persistence Gateway |
|
||||
| (Unified API) |
|
||||
+-------------+------------+
|
||||
|
|
||||
+-----------------------+-----------------------+
|
||||
| | |
|
||||
+---------v---------+ +---------v---------+ +---------v---------+
|
||||
| PostgreSQL | | RuVector | | Redis |
|
||||
| (Primary) | | (Vector Store) | | (Cache) |
|
||||
|-------------------| |-------------------| |-------------------|
|
||||
| - User/Org data | | - Embeddings | | - Session state |
|
||||
| - Conversations | | - HNSW indices | | - Rate limits |
|
||||
| - Skills config | | - Pattern store | | - Pub/Sub |
|
||||
| - Audit logs | | - Similarity | | - Job queues |
|
||||
| - RLS isolation | | - Learning data | | - Leaderboard |
|
||||
+-------------------+ +-------------------+ +-------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PostgreSQL Schema
|
||||
|
||||
### Core Tables
|
||||
|
||||
```sql
|
||||
-- Extensions
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- Full-text search
|
||||
|
||||
-- Organizations (tenant root)
|
||||
CREATE TABLE organizations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
slug VARCHAR(100) NOT NULL UNIQUE,
|
||||
plan VARCHAR(50) NOT NULL DEFAULT 'free',
|
||||
settings JSONB NOT NULL DEFAULT '{}',
|
||||
quotas JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX organizations_slug_idx ON organizations (slug);
|
||||
|
||||
-- Workspaces (project boundary)
|
||||
CREATE TABLE workspaces (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
slug VARCHAR(100) NOT NULL,
|
||||
settings JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (org_id, slug)
|
||||
);
|
||||
|
||||
CREATE INDEX workspaces_org_idx ON workspaces (org_id);
|
||||
|
||||
-- Users
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
password_hash VARCHAR(255), -- NULL for OAuth users
|
||||
display_name VARCHAR(255),
|
||||
avatar_url VARCHAR(500),
|
||||
roles TEXT[] NOT NULL DEFAULT '{"member"}',
|
||||
preferences JSONB NOT NULL DEFAULT '{}',
|
||||
email_verified_at TIMESTAMPTZ,
|
||||
last_login_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (org_id, email)
|
||||
);
|
||||
|
||||
CREATE INDEX users_org_idx ON users (org_id);
|
||||
CREATE INDEX users_email_idx ON users (email);
|
||||
|
||||
-- Workspace memberships
|
||||
CREATE TABLE workspace_memberships (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
role VARCHAR(50) NOT NULL DEFAULT 'member',
|
||||
joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (workspace_id, user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX workspace_memberships_user_idx ON workspace_memberships (user_id);
|
||||
```
|
||||
|
||||
### Session and Conversation Tables
|
||||
|
||||
```sql
|
||||
-- Agents (bot configurations)
|
||||
CREATE TABLE agents (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
persona JSONB NOT NULL DEFAULT '{}',
|
||||
skill_ids UUID[] NOT NULL DEFAULT '{}',
|
||||
memory_config JSONB NOT NULL DEFAULT '{}',
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'active',
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
ALTER TABLE agents ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY agents_isolation ON agents
|
||||
FOR ALL USING (org_id = current_tenant_id());
|
||||
|
||||
CREATE INDEX agents_org_workspace_idx ON agents (org_id, workspace_id);
|
||||
|
||||
-- Sessions (conversation containers)
|
||||
CREATE TABLE sessions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
org_id UUID NOT NULL,
|
||||
workspace_id UUID NOT NULL,
|
||||
agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
channel VARCHAR(50) NOT NULL DEFAULT 'api', -- api, slack, webhook
|
||||
channel_id VARCHAR(255), -- External channel identifier
|
||||
state VARCHAR(50) NOT NULL DEFAULT 'active',
|
||||
context_snapshot JSONB, -- Serialized context for recovery
|
||||
turn_count INTEGER NOT NULL DEFAULT 0,
|
||||
token_count INTEGER NOT NULL DEFAULT 0,
|
||||
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_active_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
ended_at TIMESTAMPTZ
|
||||
) PARTITION BY LIST (org_id);
|
||||
|
||||
ALTER TABLE sessions ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY sessions_isolation ON sessions
|
||||
FOR ALL USING (org_id = current_tenant_id());
|
||||
|
||||
CREATE INDEX sessions_user_active_idx ON sessions (user_id, state)
|
||||
WHERE state = 'active';
|
||||
CREATE INDEX sessions_agent_idx ON sessions (agent_id);
|
||||
CREATE INDEX sessions_expires_idx ON sessions (expires_at)
|
||||
WHERE state = 'active';
|
||||
|
||||
-- Conversation turns
|
||||
CREATE TABLE conversation_turns (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
org_id UUID NOT NULL,
|
||||
workspace_id UUID NOT NULL,
|
||||
session_id UUID NOT NULL,
|
||||
user_id UUID NOT NULL,
|
||||
role VARCHAR(20) NOT NULL, -- user, assistant, system, tool
|
||||
content TEXT NOT NULL,
|
||||
content_type VARCHAR(50) NOT NULL DEFAULT 'text',
|
||||
embedding_id UUID, -- Reference to vector store
|
||||
tool_calls JSONB, -- Function/skill calls
|
||||
tool_results JSONB, -- Function/skill results
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
token_count INTEGER,
|
||||
latency_ms INTEGER,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
) PARTITION BY LIST (org_id);
|
||||
|
||||
ALTER TABLE conversation_turns ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY turns_isolation ON conversation_turns
|
||||
FOR ALL USING (org_id = current_tenant_id());
|
||||
|
||||
-- Composite index for session history queries
|
||||
CREATE INDEX turns_session_time_idx ON conversation_turns (session_id, created_at DESC);
|
||||
CREATE INDEX turns_embedding_idx ON conversation_turns (embedding_id)
|
||||
WHERE embedding_id IS NOT NULL;
|
||||
```
|
||||
|
||||
### Memory Tables
|
||||
|
||||
```sql
|
||||
-- Memory entries (facts, events stored for recall)
|
||||
CREATE TABLE memories (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
org_id UUID NOT NULL,
|
||||
workspace_id UUID NOT NULL,
|
||||
user_id UUID, -- NULL for workspace-level memories
|
||||
memory_type VARCHAR(50) NOT NULL, -- episodic, semantic, procedural
|
||||
content TEXT NOT NULL,
|
||||
embedding_id UUID NOT NULL, -- Reference to vector store
|
||||
source_type VARCHAR(50), -- conversation, import, skill
|
||||
source_id UUID, -- Reference to source entity
|
||||
importance FLOAT NOT NULL DEFAULT 0.5, -- 0-1 importance score
|
||||
access_count INTEGER NOT NULL DEFAULT 0,
|
||||
last_accessed_at TIMESTAMPTZ,
|
||||
is_shared BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
expires_at TIMESTAMPTZ,
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
) PARTITION BY LIST (org_id);
|
||||
|
||||
ALTER TABLE memories ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- User-scoped memories
|
||||
CREATE POLICY memories_user_isolation ON memories
|
||||
FOR ALL USING (
|
||||
org_id = current_tenant_id()
|
||||
AND workspace_id = current_workspace_id()
|
||||
AND (user_id = current_user_id() OR user_id IS NULL)
|
||||
);
|
||||
|
||||
-- Shared memories (read-only across workspace)
|
||||
CREATE POLICY memories_shared_read ON memories
|
||||
FOR SELECT USING (
|
||||
org_id = current_tenant_id()
|
||||
AND is_shared = TRUE
|
||||
);
|
||||
|
||||
CREATE INDEX memories_workspace_type_idx ON memories (workspace_id, memory_type);
|
||||
CREATE INDEX memories_user_type_idx ON memories (user_id, memory_type)
|
||||
WHERE user_id IS NOT NULL;
|
||||
CREATE INDEX memories_embedding_idx ON memories (embedding_id);
|
||||
CREATE INDEX memories_importance_idx ON memories (importance DESC);
|
||||
CREATE INDEX memories_access_idx ON memories (last_accessed_at DESC);
|
||||
|
||||
-- Memory relationships (for graph traversal)
|
||||
CREATE TABLE memory_edges (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
org_id UUID NOT NULL,
|
||||
source_memory_id UUID NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
||||
target_memory_id UUID NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
||||
edge_type VARCHAR(50) NOT NULL, -- related_to, caused_by, part_of, supersedes
|
||||
weight FLOAT NOT NULL DEFAULT 1.0,
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
ALTER TABLE memory_edges ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY edges_isolation ON memory_edges
|
||||
FOR ALL USING (org_id = current_tenant_id());
|
||||
|
||||
CREATE INDEX memory_edges_source_idx ON memory_edges (source_memory_id);
|
||||
CREATE INDEX memory_edges_target_idx ON memory_edges (target_memory_id);
|
||||
CREATE INDEX memory_edges_type_idx ON memory_edges (edge_type);
|
||||
```
|
||||
|
||||
### Skills and Learning Tables
|
||||
|
||||
```sql
|
||||
-- Skills (registered capabilities)
|
||||
CREATE TABLE skills (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
org_id UUID NOT NULL,
|
||||
workspace_id UUID, -- NULL for org-wide skills
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
version VARCHAR(50) NOT NULL DEFAULT '1.0.0',
|
||||
triggers JSONB NOT NULL DEFAULT '[]',
|
||||
parameters JSONB NOT NULL DEFAULT '{}',
|
||||
implementation_type VARCHAR(50) NOT NULL, -- builtin, script, webhook
|
||||
implementation JSONB NOT NULL, -- Type-specific config
|
||||
hooks JSONB NOT NULL DEFAULT '{}',
|
||||
is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
usage_count INTEGER NOT NULL DEFAULT 0,
|
||||
success_rate FLOAT,
|
||||
avg_latency_ms FLOAT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
ALTER TABLE skills ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY skills_isolation ON skills
|
||||
FOR ALL USING (org_id = current_tenant_id());
|
||||
|
||||
CREATE INDEX skills_workspace_idx ON skills (workspace_id);
|
||||
CREATE INDEX skills_enabled_idx ON skills (is_enabled) WHERE is_enabled = TRUE;
|
||||
|
||||
-- Trajectories (learning data)
|
||||
CREATE TABLE trajectories (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
org_id UUID NOT NULL,
|
||||
workspace_id UUID NOT NULL,
|
||||
session_id UUID NOT NULL,
|
||||
turn_ids UUID[] NOT NULL,
|
||||
skill_ids UUID[],
|
||||
start_time TIMESTAMPTZ NOT NULL,
|
||||
end_time TIMESTAMPTZ NOT NULL,
|
||||
verdict VARCHAR(50), -- positive, negative, neutral, pending
|
||||
verdict_reason TEXT,
|
||||
metrics JSONB NOT NULL DEFAULT '{}',
|
||||
embedding_id UUID,
|
||||
is_exported BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
ALTER TABLE trajectories ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY trajectories_isolation ON trajectories
|
||||
FOR ALL USING (org_id = current_tenant_id());
|
||||
|
||||
CREATE INDEX trajectories_session_idx ON trajectories (session_id);
|
||||
CREATE INDEX trajectories_verdict_idx ON trajectories (verdict)
|
||||
WHERE verdict IS NOT NULL;
|
||||
CREATE INDEX trajectories_export_idx ON trajectories (is_exported)
|
||||
WHERE is_exported = FALSE;
|
||||
|
||||
-- Learned patterns
|
||||
CREATE TABLE learned_patterns (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
org_id UUID NOT NULL,
|
||||
workspace_id UUID, -- NULL for org-wide patterns
|
||||
pattern_type VARCHAR(50) NOT NULL, -- response, routing, skill_selection
|
||||
embedding_id UUID NOT NULL,
|
||||
exemplar_trajectory_ids UUID[] NOT NULL,
|
||||
confidence FLOAT NOT NULL,
|
||||
usage_count INTEGER NOT NULL DEFAULT 0,
|
||||
success_count INTEGER NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
superseded_by UUID REFERENCES learned_patterns(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
ALTER TABLE learned_patterns ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY patterns_isolation ON learned_patterns
|
||||
FOR ALL USING (org_id = current_tenant_id());
|
||||
|
||||
CREATE INDEX patterns_type_idx ON learned_patterns (pattern_type);
|
||||
CREATE INDEX patterns_active_idx ON learned_patterns (is_active)
|
||||
WHERE is_active = TRUE;
|
||||
CREATE INDEX patterns_embedding_idx ON learned_patterns (embedding_id);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## RuVector Integration
|
||||
|
||||
### Vector Store Adapter
|
||||
|
||||
```typescript
|
||||
// Unified vector store interface
|
||||
interface RuVectorAdapter {
|
||||
// Index management
|
||||
createIndex(config: IndexConfig): Promise<IndexHandle>;
|
||||
deleteIndex(handle: IndexHandle): Promise<void>;
|
||||
getIndex(namespace: string): Promise<IndexHandle | null>;
|
||||
|
||||
// Vector operations
|
||||
insert(handle: IndexHandle, entries: VectorEntry[]): Promise<void>;
|
||||
update(handle: IndexHandle, id: string, vector: Float32Array): Promise<void>;
|
||||
delete(handle: IndexHandle, ids: string[]): Promise<void>;
|
||||
|
||||
// Search operations
|
||||
search(handle: IndexHandle, query: Float32Array, options: SearchOptions): Promise<SearchResult[]>;
|
||||
batchSearch(handle: IndexHandle, queries: Float32Array[], options: SearchOptions): Promise<SearchResult[][]>;
|
||||
|
||||
// Index operations
|
||||
optimize(handle: IndexHandle): Promise<OptimizationResult>;
|
||||
stats(handle: IndexHandle): Promise<IndexStats>;
|
||||
}
|
||||
|
||||
interface IndexConfig {
|
||||
namespace: string;
|
||||
dimensions: number;
|
||||
distanceMetric: 'cosine' | 'euclidean' | 'dot_product';
|
||||
hnsw: {
|
||||
m: number;
|
||||
efConstruction: number;
|
||||
efSearch: number;
|
||||
};
|
||||
quantization?: {
|
||||
type: 'scalar' | 'product' | 'binary';
|
||||
bits?: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface VectorEntry {
|
||||
id: string;
|
||||
vector: Float32Array;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface SearchResult {
|
||||
id: string;
|
||||
score: number;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
```
|
||||
|
||||
### Namespace Schema
|
||||
|
||||
```typescript
|
||||
// Vector namespace organization
|
||||
const VECTOR_NAMESPACES = {
|
||||
// Memory embeddings
|
||||
EPISODIC: (orgId: string, workspaceId: string) =>
|
||||
`${orgId}/${workspaceId}/memory/episodic`,
|
||||
SEMANTIC: (orgId: string, workspaceId: string) =>
|
||||
`${orgId}/${workspaceId}/memory/semantic`,
|
||||
PROCEDURAL: (orgId: string, workspaceId: string) =>
|
||||
`${orgId}/${workspaceId}/memory/procedural`,
|
||||
|
||||
// Conversation embeddings
|
||||
CONVERSATIONS: (orgId: string, workspaceId: string) =>
|
||||
`${orgId}/${workspaceId}/conversations`,
|
||||
|
||||
// Learning embeddings
|
||||
TRAJECTORIES: (orgId: string, workspaceId: string) =>
|
||||
`${orgId}/${workspaceId}/learning/trajectories`,
|
||||
PATTERNS: (orgId: string, workspaceId: string) =>
|
||||
`${orgId}/${workspaceId}/learning/patterns`,
|
||||
|
||||
// Skill embeddings (for intent matching)
|
||||
SKILLS: (orgId: string) =>
|
||||
`${orgId}/skills`,
|
||||
};
|
||||
|
||||
// Index configuration per namespace type
|
||||
const INDEX_CONFIGS: Record<string, Partial<IndexConfig>> = {
|
||||
'memory/episodic': {
|
||||
dimensions: 384,
|
||||
distanceMetric: 'cosine',
|
||||
hnsw: { m: 16, efConstruction: 100, efSearch: 50 },
|
||||
},
|
||||
'memory/semantic': {
|
||||
dimensions: 384,
|
||||
distanceMetric: 'cosine',
|
||||
hnsw: { m: 32, efConstruction: 200, efSearch: 100 },
|
||||
},
|
||||
'conversations': {
|
||||
dimensions: 384,
|
||||
distanceMetric: 'cosine',
|
||||
hnsw: { m: 16, efConstruction: 100, efSearch: 50 },
|
||||
quantization: { type: 'scalar' }, // Compress for volume
|
||||
},
|
||||
'learning/patterns': {
|
||||
dimensions: 384,
|
||||
distanceMetric: 'cosine',
|
||||
hnsw: { m: 32, efConstruction: 200, efSearch: 100 },
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### WASM/Native Detection
|
||||
|
||||
```typescript
|
||||
// Automatic runtime detection
|
||||
class RuVectorFactory {
|
||||
private static instance: RuVectorAdapter | null = null;
|
||||
|
||||
static async create(): Promise<RuVectorAdapter> {
|
||||
if (this.instance) return this.instance;
|
||||
|
||||
// Try native first (better performance)
|
||||
try {
|
||||
const native = await import('@ruvector/core');
|
||||
if (native.isNativeAvailable()) {
|
||||
console.log('RuVector: Using native NAPI bindings');
|
||||
this.instance = new NativeRuVectorAdapter(native);
|
||||
return this.instance;
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug('Native bindings not available:', e);
|
||||
}
|
||||
|
||||
// Fall back to WASM
|
||||
try {
|
||||
const wasm = await import('@ruvector/wasm');
|
||||
console.log('RuVector: Using WASM runtime');
|
||||
this.instance = new WasmRuVectorAdapter(wasm);
|
||||
return this.instance;
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to load RuVector runtime: ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Redis Schema
|
||||
|
||||
### Session Cache
|
||||
|
||||
```typescript
|
||||
// Session state keys
|
||||
const SESSION_KEYS = {
|
||||
// Active session state
|
||||
state: (sessionId: string) => `session:${sessionId}:state`,
|
||||
|
||||
// Context window (recent turns)
|
||||
context: (sessionId: string) => `session:${sessionId}:context`,
|
||||
|
||||
// Session lock (prevent concurrent modifications)
|
||||
lock: (sessionId: string) => `session:${sessionId}:lock`,
|
||||
|
||||
// User's active sessions
|
||||
userSessions: (userId: string) => `user:${userId}:sessions`,
|
||||
|
||||
// Session expiry sorted set
|
||||
expiryIndex: () => 'sessions:expiry',
|
||||
};
|
||||
|
||||
// Session state structure
|
||||
interface CachedSessionState {
|
||||
id: string;
|
||||
agentId: string;
|
||||
userId: string;
|
||||
state: SessionState;
|
||||
turnCount: number;
|
||||
tokenCount: number;
|
||||
lastActiveAt: number;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
// Context window structure
|
||||
interface CachedContextWindow {
|
||||
maxTokens: number;
|
||||
turns: Array<{
|
||||
id: string;
|
||||
role: string;
|
||||
content: string;
|
||||
createdAt: number;
|
||||
}>;
|
||||
retrievedMemoryIds: string[];
|
||||
}
|
||||
```
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
```typescript
|
||||
// Rate limit keys
|
||||
const RATE_LIMIT_KEYS = {
|
||||
// Per-tenant rate limits
|
||||
tenant: (tenantId: string, action: string, window: string) =>
|
||||
`ratelimit:tenant:${tenantId}:${action}:${window}`,
|
||||
|
||||
// Per-user rate limits
|
||||
user: (userId: string, action: string, window: string) =>
|
||||
`ratelimit:user:${userId}:${action}:${window}`,
|
||||
|
||||
// Global rate limits
|
||||
global: (action: string, window: string) =>
|
||||
`ratelimit:global:${action}:${window}`,
|
||||
};
|
||||
|
||||
// Rate limit actions
|
||||
type RateLimitAction =
|
||||
| 'api_request'
|
||||
| 'llm_call'
|
||||
| 'embedding_request'
|
||||
| 'memory_write'
|
||||
| 'skill_execute'
|
||||
| 'webhook_dispatch';
|
||||
```
|
||||
|
||||
### Pub/Sub Channels
|
||||
|
||||
```typescript
|
||||
// Real-time event channels
|
||||
const PUBSUB_CHANNELS = {
|
||||
// Session events
|
||||
sessionCreated: (workspaceId: string) =>
|
||||
`events:${workspaceId}:session:created`,
|
||||
sessionEnded: (workspaceId: string) =>
|
||||
`events:${workspaceId}:session:ended`,
|
||||
|
||||
// Conversation events
|
||||
turnCreated: (sessionId: string) =>
|
||||
`events:session:${sessionId}:turn:created`,
|
||||
|
||||
// Memory events
|
||||
memoryCreated: (workspaceId: string) =>
|
||||
`events:${workspaceId}:memory:created`,
|
||||
memoryUpdated: (workspaceId: string) =>
|
||||
`events:${workspaceId}:memory:updated`,
|
||||
|
||||
// Skill events
|
||||
skillExecuted: (workspaceId: string) =>
|
||||
`events:${workspaceId}:skill:executed`,
|
||||
|
||||
// System events
|
||||
quotaWarning: (tenantId: string) =>
|
||||
`events:${tenantId}:quota:warning`,
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Access Patterns
|
||||
|
||||
### Repository Pattern
|
||||
|
||||
```typescript
|
||||
// Base repository with tenant context
|
||||
abstract class TenantRepository<T> {
|
||||
constructor(
|
||||
protected db: PostgresAdapter,
|
||||
protected tenantContext: TenantContext
|
||||
) {}
|
||||
|
||||
protected async withTenantContext<R>(
|
||||
fn: (db: PostgresAdapter) => Promise<R>
|
||||
): Promise<R> {
|
||||
// Set tenant context for RLS
|
||||
await this.db.query(`
|
||||
SELECT set_config('app.current_org_id', $1, true),
|
||||
set_config('app.current_workspace_id', $2, true),
|
||||
set_config('app.current_user_id', $3, true)
|
||||
`, [
|
||||
this.tenantContext.orgId,
|
||||
this.tenantContext.workspaceId,
|
||||
this.tenantContext.userId,
|
||||
]);
|
||||
|
||||
return fn(this.db);
|
||||
}
|
||||
|
||||
abstract findById(id: string): Promise<T | null>;
|
||||
abstract save(entity: T): Promise<T>;
|
||||
abstract delete(id: string): Promise<void>;
|
||||
}
|
||||
|
||||
// Memory repository example
|
||||
class MemoryRepository extends TenantRepository<Memory> {
|
||||
async findById(id: string): Promise<Memory | null> {
|
||||
return this.withTenantContext(async (db) => {
|
||||
const rows = await db.query<MemoryRow>(
|
||||
'SELECT * FROM memories WHERE id = $1',
|
||||
[id]
|
||||
);
|
||||
return rows[0] ? this.toEntity(rows[0]) : null;
|
||||
});
|
||||
}
|
||||
|
||||
async findByEmbedding(
|
||||
embedding: Float32Array,
|
||||
options: MemorySearchOptions
|
||||
): Promise<MemoryWithScore[]> {
|
||||
// Search vector store first
|
||||
const vectorResults = await this.vectorStore.search(
|
||||
this.getIndexHandle(),
|
||||
embedding,
|
||||
{ k: options.limit, threshold: options.minScore }
|
||||
);
|
||||
|
||||
if (vectorResults.length === 0) return [];
|
||||
|
||||
// Fetch full memory records
|
||||
return this.withTenantContext(async (db) => {
|
||||
const ids = vectorResults.map(r => r.id);
|
||||
const scoreMap = new Map(vectorResults.map(r => [r.id, r.score]));
|
||||
|
||||
const rows = await db.query<MemoryRow>(
|
||||
'SELECT * FROM memories WHERE id = ANY($1)',
|
||||
[ids]
|
||||
);
|
||||
|
||||
return rows
|
||||
.map(row => ({
|
||||
memory: this.toEntity(row),
|
||||
score: scoreMap.get(row.id) ?? 0,
|
||||
}))
|
||||
.sort((a, b) => b.score - a.score);
|
||||
});
|
||||
}
|
||||
|
||||
async save(memory: Memory): Promise<Memory> {
|
||||
return this.withTenantContext(async (db) => {
|
||||
// Generate embedding if not present
|
||||
if (!memory.embeddingId) {
|
||||
const embedding = await this.embedder.embed(memory.content);
|
||||
const embeddingId = crypto.randomUUID();
|
||||
|
||||
await this.vectorStore.insert(this.getIndexHandle(), [{
|
||||
id: embeddingId,
|
||||
vector: embedding,
|
||||
metadata: { memoryId: memory.id },
|
||||
}]);
|
||||
|
||||
memory.embeddingId = embeddingId;
|
||||
}
|
||||
|
||||
// Upsert to database
|
||||
const row = await db.query<MemoryRow>(`
|
||||
INSERT INTO memories (
|
||||
id, org_id, workspace_id, user_id, memory_type, content,
|
||||
embedding_id, source_type, source_id, importance, metadata
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
content = EXCLUDED.content,
|
||||
importance = EXCLUDED.importance,
|
||||
metadata = EXCLUDED.metadata,
|
||||
updated_at = NOW()
|
||||
RETURNING *
|
||||
`, [
|
||||
memory.id,
|
||||
this.tenantContext.orgId,
|
||||
this.tenantContext.workspaceId,
|
||||
memory.userId,
|
||||
memory.type,
|
||||
memory.content,
|
||||
memory.embeddingId,
|
||||
memory.sourceType,
|
||||
memory.sourceId,
|
||||
memory.importance,
|
||||
memory.metadata,
|
||||
]);
|
||||
|
||||
return this.toEntity(row[0]);
|
||||
});
|
||||
}
|
||||
|
||||
private getIndexHandle(): IndexHandle {
|
||||
return {
|
||||
namespace: VECTOR_NAMESPACES[this.tenantContext.workspaceId]
|
||||
? VECTOR_NAMESPACES.EPISODIC(
|
||||
this.tenantContext.orgId,
|
||||
this.tenantContext.workspaceId
|
||||
)
|
||||
: VECTOR_NAMESPACES.SEMANTIC(
|
||||
this.tenantContext.orgId,
|
||||
this.tenantContext.workspaceId
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Unit of Work Pattern
|
||||
|
||||
```typescript
|
||||
// Transaction coordination
|
||||
class UnitOfWork {
|
||||
private operations: Operation[] = [];
|
||||
private committed = false;
|
||||
|
||||
constructor(
|
||||
private db: PostgresAdapter,
|
||||
private vectorStore: RuVectorAdapter,
|
||||
private cache: CacheAdapter
|
||||
) {}
|
||||
|
||||
addMemory(memory: Memory): void {
|
||||
this.operations.push({
|
||||
type: 'memory',
|
||||
action: 'upsert',
|
||||
entity: memory,
|
||||
});
|
||||
}
|
||||
|
||||
addTurn(turn: ConversationTurn): void {
|
||||
this.operations.push({
|
||||
type: 'turn',
|
||||
action: 'insert',
|
||||
entity: turn,
|
||||
});
|
||||
}
|
||||
|
||||
async commit(): Promise<void> {
|
||||
if (this.committed) throw new Error('Already committed');
|
||||
|
||||
try {
|
||||
await this.db.transaction(async (tx) => {
|
||||
// Execute database operations
|
||||
for (const op of this.operations.filter(o => o.type !== 'cache')) {
|
||||
await this.executeDbOperation(tx, op);
|
||||
}
|
||||
|
||||
// Execute vector operations (outside transaction, but after DB success)
|
||||
for (const op of this.operations.filter(o =>
|
||||
o.type === 'memory' || o.type === 'turn'
|
||||
)) {
|
||||
await this.executeVectorOperation(op);
|
||||
}
|
||||
});
|
||||
|
||||
// Execute cache operations (best effort)
|
||||
for (const op of this.operations.filter(o => o.type === 'cache')) {
|
||||
await this.executeCacheOperation(op).catch(console.error);
|
||||
}
|
||||
|
||||
this.committed = true;
|
||||
} catch (error) {
|
||||
// Rollback vector operations on failure
|
||||
await this.rollbackVectorOperations();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Schema Migrations
|
||||
|
||||
```typescript
|
||||
// Migration runner
|
||||
class MigrationRunner {
|
||||
async migrate(direction: 'up' | 'down' = 'up'): Promise<void> {
|
||||
const migrations = await this.loadMigrations();
|
||||
const applied = await this.getAppliedMigrations();
|
||||
|
||||
if (direction === 'up') {
|
||||
const pending = migrations.filter(m => !applied.has(m.version));
|
||||
for (const migration of pending) {
|
||||
await this.applyMigration(migration);
|
||||
}
|
||||
} else {
|
||||
const toRollback = [...applied].reverse();
|
||||
for (const version of toRollback) {
|
||||
const migration = migrations.find(m => m.version === version);
|
||||
if (migration) {
|
||||
await this.rollbackMigration(migration);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async applyMigration(migration: Migration): Promise<void> {
|
||||
await this.db.transaction(async (tx) => {
|
||||
// Run migration SQL
|
||||
await tx.query(migration.up);
|
||||
|
||||
// Record migration
|
||||
await tx.query(
|
||||
'INSERT INTO schema_migrations (version, applied_at) VALUES ($1, NOW())',
|
||||
[migration.version]
|
||||
);
|
||||
});
|
||||
|
||||
console.log(`Applied migration: ${migration.version}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Example migration
|
||||
const MIGRATION_001: Migration = {
|
||||
version: '001_initial_schema',
|
||||
up: `
|
||||
-- Create organizations table
|
||||
CREATE TABLE organizations (...);
|
||||
|
||||
-- Create workspaces table
|
||||
CREATE TABLE workspaces (...);
|
||||
|
||||
-- ... rest of schema
|
||||
`,
|
||||
down: `
|
||||
DROP TABLE IF EXISTS workspaces;
|
||||
DROP TABLE IF EXISTS organizations;
|
||||
`,
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Consequences
|
||||
|
||||
### Benefits
|
||||
|
||||
1. **Strong Isolation**: RLS + namespace isolation at every layer
|
||||
2. **Performance**: Optimized indices, caching, and partitioning
|
||||
3. **Flexibility**: Polyglot persistence matches data characteristics
|
||||
4. **Durability**: PostgreSQL for critical data, redundant vector storage
|
||||
5. **Scalability**: Horizontal scaling via partitions and Redis cluster
|
||||
|
||||
### Trade-offs
|
||||
|
||||
| Benefit | Trade-off |
|
||||
|---------|-----------|
|
||||
| RLS security | Slight query overhead |
|
||||
| HNSW speed | Memory consumption |
|
||||
| Redis caching | Consistency complexity |
|
||||
| Polyglot persistence | Operational complexity |
|
||||
|
||||
---
|
||||
|
||||
## Related Decisions
|
||||
|
||||
- **ADR-001**: Architecture Overview
|
||||
- **ADR-002**: Multi-tenancy Design
|
||||
- **ADR-006**: WASM Integration (vector store runtime)
|
||||
|
||||
---
|
||||
|
||||
## Revision History
|
||||
|
||||
| Version | Date | Author | Changes |
|
||||
|---------|------|--------|---------|
|
||||
| 1.0 | 2026-01-27 | RuVector Architecture Team | Initial version |
|
||||
1068
vendor/ruvector/npm/packages/ruvbot/docs/adr/ADR-004-background-workers.md
vendored
Normal file
1068
vendor/ruvector/npm/packages/ruvbot/docs/adr/ADR-004-background-workers.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
907
vendor/ruvector/npm/packages/ruvbot/docs/adr/ADR-005-integration-layer.md
vendored
Normal file
907
vendor/ruvector/npm/packages/ruvbot/docs/adr/ADR-005-integration-layer.md
vendored
Normal file
@@ -0,0 +1,907 @@
|
||||
# ADR-005: Integration Layer
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-01-27
|
||||
**Decision Makers:** RuVector Architecture Team
|
||||
**Technical Area:** Integrations, External Services
|
||||
|
||||
---
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
RuvBot must integrate with external systems to:
|
||||
|
||||
1. **Receive messages** from Slack, webhooks, and other channels
|
||||
2. **Send notifications** and responses back to users
|
||||
3. **Connect to AI providers** for LLM inference and embeddings
|
||||
4. **Interact with external APIs** for skill execution
|
||||
5. **Provide webhooks** for third-party integrations
|
||||
|
||||
The integration layer must be:
|
||||
|
||||
- **Extensible** for new integration types
|
||||
- **Resilient** to external service failures
|
||||
- **Secure** with proper authentication and authorization
|
||||
- **Observable** with logging and metrics
|
||||
|
||||
---
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
### Integration Requirements
|
||||
|
||||
| Integration | Priority | Features Required |
|
||||
|-------------|----------|-------------------|
|
||||
| Slack | Critical | Events, commands, blocks, threads |
|
||||
| REST Webhooks | Critical | Inbound/outbound, signatures |
|
||||
| Anthropic Claude | Critical | Completions, streaming |
|
||||
| OpenAI | High | Completions, embeddings |
|
||||
| Custom LLMs | Medium | Provider abstraction |
|
||||
| External APIs | Medium | HTTP client, retries |
|
||||
|
||||
### Reliability Requirements
|
||||
|
||||
| Requirement | Target |
|
||||
|-------------|--------|
|
||||
| Webhook delivery success | > 99% |
|
||||
| Provider failover time | < 1s |
|
||||
| Message ordering | Within session |
|
||||
| Duplicate detection | 100% |
|
||||
|
||||
---
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
### Adopt Adapter Pattern with Circuit Breaker
|
||||
|
||||
We implement the integration layer using:
|
||||
|
||||
1. **Adapter Pattern**: Common interface for each integration type
|
||||
2. **Circuit Breaker**: Prevent cascade failures from external services
|
||||
3. **Retry with Backoff**: Handle transient failures
|
||||
4. **Event-Driven**: Decouple ingestion from processing
|
||||
|
||||
```
|
||||
+-----------------------------------------------------------------------------+
|
||||
| INTEGRATION LAYER |
|
||||
+-----------------------------------------------------------------------------+
|
||||
|
||||
+---------------------------+
|
||||
| Integration Gateway |
|
||||
| (Protocol Normalization)|
|
||||
+-------------+-------------+
|
||||
|
|
||||
+-----------------------+-----------------------+
|
||||
| | |
|
||||
+---------v---------+ +---------v---------+ +---------v---------+
|
||||
| Slack Adapter | | Webhook Adapter | | Provider Adapter |
|
||||
|-------------------| |-------------------| |-------------------|
|
||||
| - Events API | | - Inbound routes | | - LLM clients |
|
||||
| - Commands | | - Outbound queue | | - Embeddings |
|
||||
| - Interactive | | - Signatures | | - Circuit breaker |
|
||||
| - OAuth | | - Retries | | - Failover |
|
||||
+-------------------+ +-------------------+ +-------------------+
|
||||
| | |
|
||||
+-----------------------+-----------------------+
|
||||
|
|
||||
+-------------v-------------+
|
||||
| Event Normalizer |
|
||||
| (Unified Message Format) |
|
||||
+-------------+-------------+
|
||||
|
|
||||
+-------------v-------------+
|
||||
| Core Context |
|
||||
+---------------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Slack Integration
|
||||
|
||||
### Architecture
|
||||
|
||||
```typescript
|
||||
// Slack integration components
|
||||
interface SlackIntegration {
|
||||
// Event handling
|
||||
events: SlackEventHandler;
|
||||
|
||||
// Slash commands
|
||||
commands: SlackCommandHandler;
|
||||
|
||||
// Interactive components (buttons, modals)
|
||||
interactive: SlackInteractiveHandler;
|
||||
|
||||
// Block Kit builder
|
||||
blocks: BlockKitBuilder;
|
||||
|
||||
// Web API client
|
||||
client: SlackWebClient;
|
||||
|
||||
// OAuth flow
|
||||
oauth: SlackOAuthHandler;
|
||||
}
|
||||
|
||||
// Event types we handle
|
||||
type SlackEventType =
|
||||
| 'message'
|
||||
| 'app_mention'
|
||||
| 'reaction_added'
|
||||
| 'reaction_removed'
|
||||
| 'channel_created'
|
||||
| 'member_joined_channel'
|
||||
| 'file_shared'
|
||||
| 'app_home_opened';
|
||||
|
||||
// Normalized event structure
|
||||
interface SlackIncomingEvent {
|
||||
type: SlackEventType;
|
||||
teamId: string;
|
||||
channelId: string;
|
||||
userId: string;
|
||||
text?: string;
|
||||
threadTs?: string;
|
||||
ts: string;
|
||||
raw: unknown;
|
||||
}
|
||||
```
|
||||
|
||||
### Event Handler
|
||||
|
||||
```typescript
|
||||
// Slack event processing
|
||||
class SlackEventHandler {
|
||||
private eventQueue: Queue<SlackIncomingEvent>;
|
||||
private deduplicator: EventDeduplicator;
|
||||
|
||||
constructor(
|
||||
private config: SlackConfig,
|
||||
private sessionManager: SessionManager,
|
||||
private agent: Agent
|
||||
) {
|
||||
this.eventQueue = new Queue('slack-events');
|
||||
this.deduplicator = new EventDeduplicator({
|
||||
ttl: 300000, // 5 minutes
|
||||
keyFn: (e) => `${e.teamId}:${e.channelId}:${e.ts}`,
|
||||
});
|
||||
}
|
||||
|
||||
// Express middleware for Slack events
|
||||
middleware(): RequestHandler {
|
||||
return async (req, res) => {
|
||||
// Verify Slack signature
|
||||
if (!this.verifySignature(req)) {
|
||||
return res.status(401).send('Invalid signature');
|
||||
}
|
||||
|
||||
const body = req.body;
|
||||
|
||||
// Handle URL verification challenge
|
||||
if (body.type === 'url_verification') {
|
||||
return res.json({ challenge: body.challenge });
|
||||
}
|
||||
|
||||
// Acknowledge immediately (Slack 3s timeout)
|
||||
res.status(200).send();
|
||||
|
||||
// Process event asynchronously
|
||||
await this.handleEvent(body.event);
|
||||
};
|
||||
}
|
||||
|
||||
private async handleEvent(rawEvent: unknown): Promise<void> {
|
||||
const event = this.normalizeEvent(rawEvent);
|
||||
|
||||
// Deduplicate (Slack may retry)
|
||||
if (await this.deduplicator.isDuplicate(event)) {
|
||||
this.logger.debug('Duplicate event ignored', { event });
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter events we care about
|
||||
if (!this.shouldProcess(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Map to tenant context
|
||||
const tenant = await this.resolveTenant(event.teamId);
|
||||
if (!tenant) {
|
||||
this.logger.warn('Unknown Slack team', { teamId: event.teamId });
|
||||
return;
|
||||
}
|
||||
|
||||
// Enqueue for processing
|
||||
await this.eventQueue.add('process', {
|
||||
event,
|
||||
tenant,
|
||||
receivedAt: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
private shouldProcess(event: SlackIncomingEvent): boolean {
|
||||
// Skip bot messages
|
||||
if (event.raw?.bot_id) return false;
|
||||
|
||||
// Only process certain event types
|
||||
return ['message', 'app_mention'].includes(event.type);
|
||||
}
|
||||
|
||||
private verifySignature(req: Request): boolean {
|
||||
const timestamp = req.headers['x-slack-request-timestamp'] as string;
|
||||
const signature = req.headers['x-slack-signature'] as string;
|
||||
|
||||
// Prevent replay attacks (5 minute window)
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
if (Math.abs(now - parseInt(timestamp)) > 300) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const baseString = `v0:${timestamp}:${req.rawBody}`;
|
||||
const expectedSignature = `v0=${crypto
|
||||
.createHmac('sha256', this.config.signingSecret)
|
||||
.update(baseString)
|
||||
.digest('hex')}`;
|
||||
|
||||
return crypto.timingSafeEqual(
|
||||
Buffer.from(signature),
|
||||
Buffer.from(expectedSignature)
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Slash Commands
|
||||
|
||||
```typescript
|
||||
// Slash command handling
|
||||
class SlackCommandHandler {
|
||||
private commands: Map<string, CommandDefinition> = new Map();
|
||||
|
||||
register(command: CommandDefinition): void {
|
||||
this.commands.set(command.name, command);
|
||||
}
|
||||
|
||||
middleware(): RequestHandler {
|
||||
return async (req, res) => {
|
||||
if (!this.verifySignature(req)) {
|
||||
return res.status(401).send('Invalid signature');
|
||||
}
|
||||
|
||||
const { command, text, user_id, channel_id, team_id, response_url } = req.body;
|
||||
|
||||
const commandDef = this.commands.get(command);
|
||||
if (!commandDef) {
|
||||
return res.json({
|
||||
response_type: 'ephemeral',
|
||||
text: `Unknown command: ${command}`,
|
||||
});
|
||||
}
|
||||
|
||||
// Parse arguments
|
||||
const args = this.parseArgs(text, commandDef.argSchema);
|
||||
|
||||
// Acknowledge with loading state
|
||||
res.json({
|
||||
response_type: 'ephemeral',
|
||||
text: 'Processing...',
|
||||
});
|
||||
|
||||
try {
|
||||
// Execute command
|
||||
const result = await commandDef.handler({
|
||||
args,
|
||||
userId: user_id,
|
||||
channelId: channel_id,
|
||||
teamId: team_id,
|
||||
});
|
||||
|
||||
// Send actual response
|
||||
await this.sendResponse(response_url, {
|
||||
response_type: result.public ? 'in_channel' : 'ephemeral',
|
||||
blocks: result.blocks,
|
||||
text: result.text,
|
||||
});
|
||||
} catch (error) {
|
||||
await this.sendResponse(response_url, {
|
||||
response_type: 'ephemeral',
|
||||
text: `Error: ${(error as Error).message}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private parseArgs(text: string, schema: ArgSchema): Record<string, unknown> {
|
||||
const args: Record<string, unknown> = {};
|
||||
const parts = text.trim().split(/\s+/);
|
||||
|
||||
for (const [name, def] of Object.entries(schema)) {
|
||||
if (def.positional !== undefined) {
|
||||
args[name] = parts[def.positional];
|
||||
} else if (def.flag) {
|
||||
const flagIndex = parts.indexOf(`--${name}`);
|
||||
if (flagIndex !== -1) {
|
||||
args[name] = parts[flagIndex + 1] ?? true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
}
|
||||
|
||||
// Command definition
|
||||
interface CommandDefinition {
|
||||
name: string;
|
||||
description: string;
|
||||
argSchema: ArgSchema;
|
||||
handler: (ctx: CommandContext) => Promise<CommandResult>;
|
||||
}
|
||||
|
||||
// Example command
|
||||
const askCommand: CommandDefinition = {
|
||||
name: '/ask',
|
||||
description: 'Ask RuvBot a question',
|
||||
argSchema: {
|
||||
question: { positional: 0, required: true },
|
||||
context: { flag: true },
|
||||
},
|
||||
handler: async (ctx) => {
|
||||
const session = await sessionManager.getOrCreate(ctx.userId, ctx.channelId);
|
||||
const response = await agent.process(session, ctx.args.question as string);
|
||||
|
||||
return {
|
||||
public: false,
|
||||
text: response.content,
|
||||
blocks: formatResponseBlocks(response),
|
||||
};
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Block Kit Builder
|
||||
|
||||
```typescript
|
||||
// Fluent Block Kit builder
|
||||
class BlockKitBuilder {
|
||||
private blocks: Block[] = [];
|
||||
|
||||
section(text: string): this {
|
||||
this.blocks.push({
|
||||
type: 'section',
|
||||
text: { type: 'mrkdwn', text },
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
divider(): this {
|
||||
this.blocks.push({ type: 'divider' });
|
||||
return this;
|
||||
}
|
||||
|
||||
context(...elements: string[]): this {
|
||||
this.blocks.push({
|
||||
type: 'context',
|
||||
elements: elements.map(e => ({ type: 'mrkdwn', text: e })),
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
actions(actionId: string, buttons: Button[]): this {
|
||||
this.blocks.push({
|
||||
type: 'actions',
|
||||
block_id: actionId,
|
||||
elements: buttons.map(b => ({
|
||||
type: 'button',
|
||||
text: { type: 'plain_text', text: b.text },
|
||||
action_id: b.actionId,
|
||||
value: b.value,
|
||||
style: b.style,
|
||||
})),
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
input(label: string, actionId: string, options: InputOptions): this {
|
||||
this.blocks.push({
|
||||
type: 'input',
|
||||
label: { type: 'plain_text', text: label },
|
||||
element: {
|
||||
type: options.multiline ? 'plain_text_input' : 'plain_text_input',
|
||||
action_id: actionId,
|
||||
multiline: options.multiline,
|
||||
placeholder: options.placeholder
|
||||
? { type: 'plain_text', text: options.placeholder }
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
build(): Block[] {
|
||||
return this.blocks;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage example
|
||||
const responseBlocks = new BlockKitBuilder()
|
||||
.section('Here is what I found:')
|
||||
.divider()
|
||||
.section(responseText)
|
||||
.context(`Generated in ${latencyMs}ms`)
|
||||
.actions('feedback', [
|
||||
{ text: 'Helpful', actionId: 'feedback_positive', value: responseId, style: 'primary' },
|
||||
{ text: 'Not helpful', actionId: 'feedback_negative', value: responseId },
|
||||
])
|
||||
.build();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Webhook Integration
|
||||
|
||||
### Inbound Webhooks
|
||||
|
||||
```typescript
|
||||
// Inbound webhook configuration
|
||||
interface WebhookEndpoint {
|
||||
id: string;
|
||||
path: string; // e.g., "/webhooks/github"
|
||||
method: 'POST' | 'PUT';
|
||||
secretKey?: string;
|
||||
signatureHeader?: string;
|
||||
signatureAlgorithm?: 'hmac-sha256' | 'hmac-sha1';
|
||||
handler: WebhookHandler;
|
||||
rateLimit?: RateLimitConfig;
|
||||
}
|
||||
|
||||
class InboundWebhookRouter {
|
||||
private endpoints: Map<string, WebhookEndpoint> = new Map();
|
||||
|
||||
register(endpoint: WebhookEndpoint): void {
|
||||
this.endpoints.set(endpoint.path, endpoint);
|
||||
}
|
||||
|
||||
middleware(): RequestHandler {
|
||||
return async (req, res, next) => {
|
||||
const endpoint = this.endpoints.get(req.path);
|
||||
if (!endpoint) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Rate limiting
|
||||
if (endpoint.rateLimit) {
|
||||
const allowed = await this.rateLimiter.check(
|
||||
`webhook:${endpoint.id}:${req.ip}`,
|
||||
endpoint.rateLimit
|
||||
);
|
||||
if (!allowed) {
|
||||
return res.status(429).json({ error: 'Rate limit exceeded' });
|
||||
}
|
||||
}
|
||||
|
||||
// Signature verification
|
||||
if (endpoint.secretKey) {
|
||||
if (!this.verifySignature(req, endpoint)) {
|
||||
return res.status(401).json({ error: 'Invalid signature' });
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await endpoint.handler({
|
||||
body: req.body,
|
||||
headers: req.headers,
|
||||
query: req.query,
|
||||
});
|
||||
|
||||
res.status(result.status ?? 200).json(result.body ?? { ok: true });
|
||||
} catch (error) {
|
||||
this.logger.error('Webhook handler error', { error, endpoint: endpoint.id });
|
||||
res.status(500).json({ error: 'Internal error' });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private verifySignature(req: Request, endpoint: WebhookEndpoint): boolean {
|
||||
const signatureHeader = endpoint.signatureHeader ?? 'x-signature';
|
||||
const providedSignature = req.headers[signatureHeader.toLowerCase()] as string;
|
||||
|
||||
if (!providedSignature) return false;
|
||||
|
||||
const algorithm = endpoint.signatureAlgorithm ?? 'hmac-sha256';
|
||||
const expectedSignature = crypto
|
||||
.createHmac(algorithm.replace('hmac-', ''), endpoint.secretKey!)
|
||||
.update(req.rawBody)
|
||||
.digest('hex');
|
||||
|
||||
// Handle various signature formats
|
||||
const normalizedProvided = providedSignature
|
||||
.replace(/^sha256=/, '')
|
||||
.replace(/^sha1=/, '');
|
||||
|
||||
return crypto.timingSafeEqual(
|
||||
Buffer.from(normalizedProvided),
|
||||
Buffer.from(expectedSignature)
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Outbound Webhooks
|
||||
|
||||
```typescript
|
||||
// Outbound webhook delivery
|
||||
class OutboundWebhookDispatcher {
|
||||
constructor(
|
||||
private queue: Queue<WebhookDelivery>,
|
||||
private storage: WebhookStorage,
|
||||
private http: HttpClient
|
||||
) {}
|
||||
|
||||
async dispatch(
|
||||
webhookId: string,
|
||||
event: WebhookEvent,
|
||||
options?: DispatchOptions
|
||||
): Promise<string> {
|
||||
const webhook = await this.storage.findById(webhookId);
|
||||
if (!webhook || !webhook.isEnabled) {
|
||||
throw new Error(`Webhook ${webhookId} not found or disabled`);
|
||||
}
|
||||
|
||||
const deliveryId = crypto.randomUUID();
|
||||
const payload = this.buildPayload(event, webhook);
|
||||
const signature = this.sign(payload, webhook.secret);
|
||||
|
||||
// Queue for delivery
|
||||
await this.queue.add(
|
||||
'deliver',
|
||||
{
|
||||
deliveryId,
|
||||
webhookId,
|
||||
url: webhook.url,
|
||||
payload,
|
||||
signature,
|
||||
headers: webhook.headers,
|
||||
},
|
||||
{
|
||||
attempts: 10,
|
||||
backoff: { type: 'exponential', delay: 1000 },
|
||||
removeOnComplete: 100,
|
||||
removeOnFail: 1000,
|
||||
}
|
||||
);
|
||||
|
||||
return deliveryId;
|
||||
}
|
||||
|
||||
private buildPayload(event: WebhookEvent, webhook: Webhook): string {
|
||||
return JSON.stringify({
|
||||
id: crypto.randomUUID(),
|
||||
type: event.type,
|
||||
timestamp: new Date().toISOString(),
|
||||
data: event.data,
|
||||
webhook_id: webhook.id,
|
||||
});
|
||||
}
|
||||
|
||||
private sign(payload: string, secret: string): string {
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const signaturePayload = `${timestamp}.${payload}`;
|
||||
const signature = crypto
|
||||
.createHmac('sha256', secret)
|
||||
.update(signaturePayload)
|
||||
.digest('hex');
|
||||
return `t=${timestamp},v1=${signature}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Webhook event types
|
||||
type WebhookEventType =
|
||||
| 'session.created'
|
||||
| 'session.ended'
|
||||
| 'message.received'
|
||||
| 'message.sent'
|
||||
| 'memory.created'
|
||||
| 'skill.executed'
|
||||
| 'error.occurred';
|
||||
|
||||
interface WebhookEvent {
|
||||
type: WebhookEventType;
|
||||
data: Record<string, unknown>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## LLM Provider Integration
|
||||
|
||||
### Provider Abstraction
|
||||
|
||||
```typescript
|
||||
// Unified LLM provider interface
|
||||
interface LLMProvider {
|
||||
// Basic completion
|
||||
complete(
|
||||
messages: Message[],
|
||||
options: CompletionOptions
|
||||
): Promise<Completion>;
|
||||
|
||||
// Streaming completion
|
||||
stream(
|
||||
messages: Message[],
|
||||
options: StreamOptions
|
||||
): AsyncGenerator<Token, Completion, void>;
|
||||
|
||||
// Token counting
|
||||
countTokens(text: string): Promise<number>;
|
||||
|
||||
// Model info
|
||||
getModel(): ModelInfo;
|
||||
|
||||
// Health check
|
||||
isHealthy(): Promise<boolean>;
|
||||
}
|
||||
|
||||
interface CompletionOptions {
|
||||
maxTokens?: number;
|
||||
temperature?: number;
|
||||
topP?: number;
|
||||
stopSequences?: string[];
|
||||
tools?: Tool[];
|
||||
}
|
||||
|
||||
interface Completion {
|
||||
content: string;
|
||||
finishReason: 'stop' | 'length' | 'tool_use';
|
||||
usage: {
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
};
|
||||
toolCalls?: ToolCall[];
|
||||
}
|
||||
```
|
||||
|
||||
### Anthropic Claude Provider
|
||||
|
||||
```typescript
|
||||
// Claude provider implementation
|
||||
class ClaudeProvider implements LLMProvider {
|
||||
private client: AnthropicClient;
|
||||
private circuitBreaker: CircuitBreaker;
|
||||
|
||||
constructor(config: ClaudeConfig) {
|
||||
this.client = new Anthropic({
|
||||
apiKey: config.apiKey,
|
||||
baseURL: config.baseURL,
|
||||
});
|
||||
|
||||
this.circuitBreaker = new CircuitBreaker({
|
||||
failureThreshold: 5,
|
||||
resetTimeout: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
async complete(
|
||||
messages: Message[],
|
||||
options: CompletionOptions
|
||||
): Promise<Completion> {
|
||||
return this.circuitBreaker.execute(async () => {
|
||||
const response = await this.client.messages.create({
|
||||
model: 'claude-sonnet-4-20250514',
|
||||
max_tokens: options.maxTokens ?? 1024,
|
||||
temperature: options.temperature ?? 0.7,
|
||||
messages: this.formatMessages(messages),
|
||||
tools: options.tools?.map(this.formatTool),
|
||||
});
|
||||
|
||||
return this.parseResponse(response);
|
||||
});
|
||||
}
|
||||
|
||||
async *stream(
|
||||
messages: Message[],
|
||||
options: StreamOptions
|
||||
): AsyncGenerator<Token, Completion, void> {
|
||||
const stream = await this.client.messages.stream({
|
||||
model: 'claude-sonnet-4-20250514',
|
||||
max_tokens: options.maxTokens ?? 1024,
|
||||
temperature: options.temperature ?? 0.7,
|
||||
messages: this.formatMessages(messages),
|
||||
});
|
||||
|
||||
let fullContent = '';
|
||||
let inputTokens = 0;
|
||||
let outputTokens = 0;
|
||||
|
||||
for await (const event of stream) {
|
||||
if (event.type === 'content_block_delta') {
|
||||
const text = event.delta.text;
|
||||
fullContent += text;
|
||||
yield { type: 'text', text };
|
||||
} else if (event.type === 'message_delta') {
|
||||
outputTokens = event.usage?.output_tokens ?? 0;
|
||||
} else if (event.type === 'message_start') {
|
||||
inputTokens = event.message.usage?.input_tokens ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: fullContent,
|
||||
finishReason: 'stop',
|
||||
usage: { inputTokens, outputTokens },
|
||||
};
|
||||
}
|
||||
|
||||
private formatMessages(messages: Message[]): AnthropicMessage[] {
|
||||
return messages.map(m => ({
|
||||
role: m.role === 'user' ? 'user' : 'assistant',
|
||||
content: m.content,
|
||||
}));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Provider Registry with Failover
|
||||
|
||||
```typescript
|
||||
// Multi-provider registry with automatic failover
|
||||
class ProviderRegistry {
|
||||
private providers: Map<string, LLMProvider> = new Map();
|
||||
private primary: string;
|
||||
private fallbacks: string[];
|
||||
|
||||
constructor(config: ProviderRegistryConfig) {
|
||||
this.primary = config.primary;
|
||||
this.fallbacks = config.fallbacks;
|
||||
}
|
||||
|
||||
register(name: string, provider: LLMProvider): void {
|
||||
this.providers.set(name, provider);
|
||||
}
|
||||
|
||||
async complete(
|
||||
messages: Message[],
|
||||
options: CompletionOptions
|
||||
): Promise<Completion> {
|
||||
const providerOrder = [this.primary, ...this.fallbacks];
|
||||
|
||||
for (const providerName of providerOrder) {
|
||||
const provider = this.providers.get(providerName);
|
||||
if (!provider) continue;
|
||||
|
||||
try {
|
||||
// Check health before using
|
||||
if (await provider.isHealthy()) {
|
||||
const result = await provider.complete(messages, options);
|
||||
this.metrics.increment('provider.success', { provider: providerName });
|
||||
return result;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn(`Provider ${providerName} failed`, { error });
|
||||
this.metrics.increment('provider.failure', { provider: providerName });
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('All LLM providers unavailable');
|
||||
}
|
||||
|
||||
async *stream(
|
||||
messages: Message[],
|
||||
options: StreamOptions
|
||||
): AsyncGenerator<Token, Completion, void> {
|
||||
const provider = this.providers.get(this.primary);
|
||||
if (!provider) {
|
||||
throw new Error(`Primary provider ${this.primary} not found`);
|
||||
}
|
||||
|
||||
// Streaming doesn't support automatic failover (would be disruptive)
|
||||
yield* provider.stream(messages, options);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Circuit Breaker
|
||||
|
||||
```typescript
|
||||
// Circuit breaker for external service protection
|
||||
class CircuitBreaker {
|
||||
private state: 'closed' | 'open' | 'half-open' = 'closed';
|
||||
private failures = 0;
|
||||
private lastFailureTime = 0;
|
||||
private successesSinceHalfOpen = 0;
|
||||
|
||||
constructor(private config: CircuitBreakerConfig) {}
|
||||
|
||||
async execute<T>(fn: () => Promise<T>): Promise<T> {
|
||||
if (this.state === 'open') {
|
||||
if (Date.now() - this.lastFailureTime > this.config.resetTimeout) {
|
||||
this.state = 'half-open';
|
||||
this.successesSinceHalfOpen = 0;
|
||||
} else {
|
||||
throw new CircuitBreakerOpenError();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await fn();
|
||||
this.onSuccess();
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.onFailure();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private onSuccess(): void {
|
||||
if (this.state === 'half-open') {
|
||||
this.successesSinceHalfOpen++;
|
||||
if (this.successesSinceHalfOpen >= this.config.successThreshold) {
|
||||
this.state = 'closed';
|
||||
this.failures = 0;
|
||||
}
|
||||
} else {
|
||||
this.failures = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private onFailure(): void {
|
||||
this.failures++;
|
||||
this.lastFailureTime = Date.now();
|
||||
|
||||
if (this.failures >= this.config.failureThreshold) {
|
||||
this.state = 'open';
|
||||
}
|
||||
}
|
||||
|
||||
getState(): CircuitBreakerState {
|
||||
return {
|
||||
state: this.state,
|
||||
failures: this.failures,
|
||||
lastFailureTime: this.lastFailureTime,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface CircuitBreakerConfig {
|
||||
failureThreshold: number; // Failures before opening
|
||||
successThreshold: number; // Successes in half-open to close
|
||||
resetTimeout: number; // ms before trying half-open
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Consequences
|
||||
|
||||
### Benefits
|
||||
|
||||
1. **Unified Interface**: All integrations exposed through consistent APIs
|
||||
2. **Resilience**: Circuit breakers and retries prevent cascade failures
|
||||
3. **Extensibility**: Easy to add new providers and integrations
|
||||
4. **Observability**: Comprehensive metrics and logging
|
||||
5. **Security**: Proper signature verification and authentication
|
||||
|
||||
### Trade-offs
|
||||
|
||||
| Benefit | Trade-off |
|
||||
|---------|-----------|
|
||||
| Abstraction | Some provider-specific features hidden |
|
||||
| Circuit breaker | Delayed recovery after incidents |
|
||||
| Retry logic | Potential duplicate processing |
|
||||
| Async processing | Eventually consistent state |
|
||||
|
||||
---
|
||||
|
||||
## Related Decisions
|
||||
|
||||
- **ADR-001**: Architecture Overview
|
||||
- **ADR-004**: Background Workers (webhook delivery)
|
||||
|
||||
---
|
||||
|
||||
## Revision History
|
||||
|
||||
| Version | Date | Author | Changes |
|
||||
|---------|------|--------|---------|
|
||||
| 1.0 | 2026-01-27 | RuVector Architecture Team | Initial version |
|
||||
775
vendor/ruvector/npm/packages/ruvbot/docs/adr/ADR-006-wasm-integration.md
vendored
Normal file
775
vendor/ruvector/npm/packages/ruvbot/docs/adr/ADR-006-wasm-integration.md
vendored
Normal file
@@ -0,0 +1,775 @@
|
||||
# ADR-006: WASM Integration
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-01-27
|
||||
**Decision Makers:** RuVector Architecture Team
|
||||
**Technical Area:** Runtime, Performance
|
||||
|
||||
---
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
RuvBot requires high-performance vector operations and ML inference for:
|
||||
|
||||
1. **Embedding generation** for memory storage and retrieval
|
||||
2. **HNSW search** for semantic memory recall
|
||||
3. **Pattern matching** for learned response optimization
|
||||
4. **Quantization** for memory-efficient vector storage
|
||||
|
||||
The runtime must support:
|
||||
|
||||
- **Server-side Node.js** for API workloads
|
||||
- **Edge deployments** (Cloudflare Workers, Vercel Edge)
|
||||
- **Browser execution** for client-side features
|
||||
- **Fallback paths** when WASM is unavailable
|
||||
|
||||
---
|
||||
|
||||
## Decision Drivers
|
||||
|
||||
### Performance Requirements
|
||||
|
||||
| Operation | Target Latency | Environment |
|
||||
|-----------|----------------|-------------|
|
||||
| Embed single text | < 10ms | WASM |
|
||||
| Embed batch (32) | < 100ms | WASM |
|
||||
| HNSW search k=10 | < 5ms | Native/WASM |
|
||||
| Quantize vector | < 1ms | WASM |
|
||||
| Pattern match | < 20ms | WASM |
|
||||
|
||||
### Compatibility Requirements
|
||||
|
||||
| Environment | WASM Support | Native Support |
|
||||
|-------------|--------------|----------------|
|
||||
| Node.js 18+ | Full | Full (NAPI) |
|
||||
| Node.js 14-17 | Partial | Full (NAPI) |
|
||||
| Cloudflare Workers | Full | None |
|
||||
| Vercel Edge | Full | None |
|
||||
| Browser (Chrome/FF/Safari) | Full | None |
|
||||
| Deno | Full | Partial |
|
||||
|
||||
---
|
||||
|
||||
## Decision Outcome
|
||||
|
||||
### Adopt Hybrid WASM/Native Runtime with Automatic Detection
|
||||
|
||||
We implement a runtime abstraction that:
|
||||
|
||||
1. **Detects environment** at initialization
|
||||
2. **Prefers native bindings** when available (2-5x faster)
|
||||
3. **Falls back to WASM** universally
|
||||
4. **Provides consistent API** regardless of backend
|
||||
|
||||
```
|
||||
+-----------------------------------------------------------------------------+
|
||||
| WASM INTEGRATION LAYER |
|
||||
+-----------------------------------------------------------------------------+
|
||||
|
||||
+---------------------------+
|
||||
| Runtime Detector |
|
||||
+-------------+-------------+
|
||||
|
|
||||
+---------------------+---------------------+
|
||||
| |
|
||||
+-----------v-----------+ +-----------v-----------+
|
||||
| Native Backend | | WASM Backend |
|
||||
| (NAPI-RS) | | (wasm-bindgen) |
|
||||
|-----------------------| |-----------------------|
|
||||
| - @ruvector/core | | - @ruvector/wasm |
|
||||
| - @ruvector/ruvllm | | - @ruvllm-wasm |
|
||||
| - @ruvector/sona | | - @sona-wasm |
|
||||
+-----------+-----------+ +-----------+-----------+
|
||||
| |
|
||||
+---------------------+---------------------+
|
||||
|
|
||||
+-------------v-------------+
|
||||
| Unified API Surface |
|
||||
| (RuVectorRuntime) |
|
||||
+---------------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## WASM Module Architecture
|
||||
|
||||
### Module Organization
|
||||
|
||||
```typescript
|
||||
// WASM module types available
|
||||
interface WasmModules {
|
||||
// Vector operations
|
||||
vectorOps: {
|
||||
distance: (a: Float32Array, b: Float32Array, metric: DistanceMetric) => number;
|
||||
batchDistance: (query: Float32Array, vectors: Float32Array[], metric: DistanceMetric) => Float32Array;
|
||||
normalize: (vector: Float32Array) => Float32Array;
|
||||
quantize: (vector: Float32Array, config: QuantizationConfig) => Uint8Array;
|
||||
dequantize: (quantized: Uint8Array, config: QuantizationConfig) => Float32Array;
|
||||
};
|
||||
|
||||
// HNSW index
|
||||
hnsw: {
|
||||
create: (config: HnswConfig) => HnswIndexHandle;
|
||||
insert: (handle: HnswIndexHandle, id: string, vector: Float32Array) => void;
|
||||
search: (handle: HnswIndexHandle, query: Float32Array, k: number) => SearchResult[];
|
||||
delete: (handle: HnswIndexHandle, id: string) => boolean;
|
||||
serialize: (handle: HnswIndexHandle) => Uint8Array;
|
||||
deserialize: (data: Uint8Array) => HnswIndexHandle;
|
||||
free: (handle: HnswIndexHandle) => void;
|
||||
};
|
||||
|
||||
// Embeddings
|
||||
embeddings: {
|
||||
loadModel: (modelPath: string) => EmbeddingModelHandle;
|
||||
embed: (handle: EmbeddingModelHandle, text: string) => Float32Array;
|
||||
embedBatch: (handle: EmbeddingModelHandle, texts: string[]) => Float32Array[];
|
||||
unloadModel: (handle: EmbeddingModelHandle) => void;
|
||||
};
|
||||
|
||||
// Learning
|
||||
learning: {
|
||||
createPattern: (embedding: Float32Array, metadata: unknown) => PatternHandle;
|
||||
matchPatterns: (query: Float32Array, patterns: PatternHandle[], threshold: number) => PatternMatch[];
|
||||
trainLoRA: (trajectories: Trajectory[], config: LoRAConfig) => LoRAWeights;
|
||||
applyEWC: (weights: ModelWeights, fisher: FisherMatrix, lambda: number) => ModelWeights;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Runtime Detection
|
||||
|
||||
```typescript
|
||||
// Automatic runtime detection and initialization
|
||||
class RuVectorRuntime {
|
||||
private static instance: RuVectorRuntime | null = null;
|
||||
private backend: 'native' | 'wasm' | 'js-fallback';
|
||||
private modules: WasmModules | NativeModules;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static async initialize(): Promise<RuVectorRuntime> {
|
||||
if (this.instance) return this.instance;
|
||||
|
||||
const runtime = new RuVectorRuntime();
|
||||
await runtime.detectAndLoad();
|
||||
this.instance = runtime;
|
||||
return runtime;
|
||||
}
|
||||
|
||||
private async detectAndLoad(): Promise<void> {
|
||||
// Try native first (best performance)
|
||||
if (await this.tryNative()) {
|
||||
this.backend = 'native';
|
||||
console.log('RuVector: Using native NAPI backend');
|
||||
return;
|
||||
}
|
||||
|
||||
// Try WASM
|
||||
if (await this.tryWasm()) {
|
||||
this.backend = 'wasm';
|
||||
console.log('RuVector: Using WASM backend');
|
||||
return;
|
||||
}
|
||||
|
||||
// Fall back to pure JS (limited functionality)
|
||||
this.backend = 'js-fallback';
|
||||
console.warn('RuVector: Using JS fallback (limited performance)');
|
||||
await this.loadJsFallback();
|
||||
}
|
||||
|
||||
private async tryNative(): Promise<boolean> {
|
||||
// Native only available in Node.js
|
||||
if (typeof process === 'undefined' || !process.versions?.node) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const nativeModule = await import('@ruvector/core');
|
||||
if (typeof nativeModule.isNativeAvailable === 'function' &&
|
||||
nativeModule.isNativeAvailable()) {
|
||||
this.modules = nativeModule;
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug('Native module not available:', e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async tryWasm(): Promise<boolean> {
|
||||
try {
|
||||
// Check WebAssembly support
|
||||
if (typeof WebAssembly !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load WASM modules
|
||||
const [vectorOps, hnsw, embeddings, learning] = await Promise.all([
|
||||
import('@ruvector/wasm'),
|
||||
import('@ruvector/wasm/hnsw'),
|
||||
import('@ruvector/wasm/embeddings'),
|
||||
import('@ruvector/wasm/learning'),
|
||||
]);
|
||||
|
||||
// Initialize WASM modules
|
||||
await Promise.all([
|
||||
vectorOps.default(),
|
||||
hnsw.default(),
|
||||
embeddings.default(),
|
||||
learning.default(),
|
||||
]);
|
||||
|
||||
this.modules = {
|
||||
vectorOps,
|
||||
hnsw,
|
||||
embeddings,
|
||||
learning,
|
||||
};
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.debug('WASM modules not available:', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async loadJsFallback(): Promise<void> {
|
||||
// Pure JS implementations (slower but always work)
|
||||
const { JsFallbackModules } = await import('./js-fallback');
|
||||
this.modules = new JsFallbackModules();
|
||||
}
|
||||
|
||||
getBackend(): 'native' | 'wasm' | 'js-fallback' {
|
||||
return this.backend;
|
||||
}
|
||||
|
||||
getModules(): WasmModules | NativeModules {
|
||||
if (!this.modules) {
|
||||
throw new Error('RuVector runtime not initialized');
|
||||
}
|
||||
return this.modules;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Embedding Engine
|
||||
|
||||
### WASM Embedder
|
||||
|
||||
```typescript
|
||||
// WASM-based embedding engine
|
||||
class WasmEmbedder {
|
||||
private modelHandle: EmbeddingModelHandle | null = null;
|
||||
private modelPath: string;
|
||||
private dimensions: number;
|
||||
private runtime: RuVectorRuntime;
|
||||
|
||||
constructor(config: EmbedderConfig) {
|
||||
this.modelPath = config.modelPath;
|
||||
this.dimensions = config.dimensions ?? 384;
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
this.runtime = await RuVectorRuntime.initialize();
|
||||
const { embeddings } = this.runtime.getModules();
|
||||
|
||||
// Load model (downloads and caches if needed)
|
||||
const modelData = await this.loadModelData();
|
||||
this.modelHandle = embeddings.loadModel(modelData);
|
||||
}
|
||||
|
||||
async embed(text: string): Promise<Float32Array> {
|
||||
if (!this.modelHandle) {
|
||||
throw new Error('Embedder not initialized');
|
||||
}
|
||||
|
||||
const { embeddings } = this.runtime.getModules();
|
||||
return embeddings.embed(this.modelHandle, text);
|
||||
}
|
||||
|
||||
async embedBatch(texts: string[]): Promise<Float32Array[]> {
|
||||
if (!this.modelHandle) {
|
||||
throw new Error('Embedder not initialized');
|
||||
}
|
||||
|
||||
const { embeddings } = this.runtime.getModules();
|
||||
|
||||
// Process in chunks to avoid OOM
|
||||
const chunkSize = 32;
|
||||
const results: Float32Array[] = [];
|
||||
|
||||
for (let i = 0; i < texts.length; i += chunkSize) {
|
||||
const chunk = texts.slice(i, i + chunkSize);
|
||||
const chunkResults = embeddings.embedBatch(this.modelHandle, chunk);
|
||||
results.push(...chunkResults);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
getDimensions(): number {
|
||||
return this.dimensions;
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
if (this.modelHandle) {
|
||||
const { embeddings } = this.runtime.getModules();
|
||||
embeddings.unloadModel(this.modelHandle);
|
||||
this.modelHandle = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async loadModelData(): Promise<Uint8Array> {
|
||||
// Check cache first
|
||||
const cached = await this.modelCache.get(this.modelPath);
|
||||
if (cached) return cached;
|
||||
|
||||
// Download model
|
||||
const response = await fetch(this.modelPath);
|
||||
const buffer = await response.arrayBuffer();
|
||||
const data = new Uint8Array(buffer);
|
||||
|
||||
// Cache for future use
|
||||
await this.modelCache.set(this.modelPath, data);
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Model Cache
|
||||
|
||||
```typescript
|
||||
// Cross-environment model cache
|
||||
class ModelCache {
|
||||
private memoryCache: Map<string, Uint8Array> = new Map();
|
||||
|
||||
async get(key: string): Promise<Uint8Array | null> {
|
||||
// Check memory cache first
|
||||
if (this.memoryCache.has(key)) {
|
||||
return this.memoryCache.get(key)!;
|
||||
}
|
||||
|
||||
// Try persistent cache (environment-specific)
|
||||
if (typeof caches !== 'undefined') {
|
||||
// Browser/Cloudflare Cache API
|
||||
return this.getFromCacheAPI(key);
|
||||
} else if (typeof process !== 'undefined' && process.versions?.node) {
|
||||
// Node.js file system cache
|
||||
return this.getFromFileCache(key);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async set(key: string, data: Uint8Array): Promise<void> {
|
||||
// Always store in memory
|
||||
this.memoryCache.set(key, data);
|
||||
|
||||
// Persist to appropriate cache
|
||||
if (typeof caches !== 'undefined') {
|
||||
await this.setToCacheAPI(key, data);
|
||||
} else if (typeof process !== 'undefined' && process.versions?.node) {
|
||||
await this.setToFileCache(key, data);
|
||||
}
|
||||
}
|
||||
|
||||
private async getFromCacheAPI(key: string): Promise<Uint8Array | null> {
|
||||
try {
|
||||
const cache = await caches.open('ruvector-models');
|
||||
const response = await cache.match(key);
|
||||
if (response) {
|
||||
const buffer = await response.arrayBuffer();
|
||||
return new Uint8Array(buffer);
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug('Cache API error:', e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async setToCacheAPI(key: string, data: Uint8Array): Promise<void> {
|
||||
try {
|
||||
const cache = await caches.open('ruvector-models');
|
||||
const response = new Response(data, {
|
||||
headers: { 'Content-Type': 'application/octet-stream' },
|
||||
});
|
||||
await cache.put(key, response);
|
||||
} catch (e) {
|
||||
console.debug('Cache API error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
private async getFromFileCache(key: string): Promise<Uint8Array | null> {
|
||||
const fs = await import('fs/promises');
|
||||
const path = await import('path');
|
||||
const os = await import('os');
|
||||
|
||||
const cacheDir = path.join(os.homedir(), '.ruvector', 'models');
|
||||
const cachePath = path.join(cacheDir, this.keyToFilename(key));
|
||||
|
||||
try {
|
||||
const data = await fs.readFile(cachePath);
|
||||
return new Uint8Array(data);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async setToFileCache(key: string, data: Uint8Array): Promise<void> {
|
||||
const fs = await import('fs/promises');
|
||||
const path = await import('path');
|
||||
const os = await import('os');
|
||||
|
||||
const cacheDir = path.join(os.homedir(), '.ruvector', 'models');
|
||||
await fs.mkdir(cacheDir, { recursive: true });
|
||||
|
||||
const cachePath = path.join(cacheDir, this.keyToFilename(key));
|
||||
await fs.writeFile(cachePath, data);
|
||||
}
|
||||
|
||||
private keyToFilename(key: string): string {
|
||||
const crypto = require('crypto');
|
||||
return crypto.createHash('sha256').update(key).digest('hex').slice(0, 32);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## HNSW Index WASM Wrapper
|
||||
|
||||
```typescript
|
||||
// WASM-based HNSW index
|
||||
class WasmHnswIndex {
|
||||
private handle: HnswIndexHandle | null = null;
|
||||
private runtime: RuVectorRuntime;
|
||||
private config: HnswConfig;
|
||||
private vectorCount = 0;
|
||||
|
||||
constructor(config: HnswConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
this.runtime = await RuVectorRuntime.initialize();
|
||||
const { hnsw } = this.runtime.getModules();
|
||||
this.handle = hnsw.create(this.config);
|
||||
}
|
||||
|
||||
async insert(id: string, vector: Float32Array): Promise<void> {
|
||||
if (!this.handle) throw new Error('Index not initialized');
|
||||
|
||||
// Validate dimensions
|
||||
if (vector.length !== this.config.dimensions) {
|
||||
throw new Error(`Vector dimension mismatch: ${vector.length} vs ${this.config.dimensions}`);
|
||||
}
|
||||
|
||||
const { hnsw } = this.runtime.getModules();
|
||||
hnsw.insert(this.handle, id, vector);
|
||||
this.vectorCount++;
|
||||
}
|
||||
|
||||
async insertBatch(entries: Array<{ id: string; vector: Float32Array }>): Promise<void> {
|
||||
if (!this.handle) throw new Error('Index not initialized');
|
||||
|
||||
const { hnsw } = this.runtime.getModules();
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.vector.length !== this.config.dimensions) {
|
||||
throw new Error(`Vector dimension mismatch for ${entry.id}`);
|
||||
}
|
||||
hnsw.insert(this.handle, entry.id, entry.vector);
|
||||
this.vectorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
async search(query: Float32Array, k: number): Promise<SearchResult[]> {
|
||||
if (!this.handle) throw new Error('Index not initialized');
|
||||
|
||||
if (query.length !== this.config.dimensions) {
|
||||
throw new Error(`Query dimension mismatch: ${query.length}`);
|
||||
}
|
||||
|
||||
const { hnsw } = this.runtime.getModules();
|
||||
return hnsw.search(this.handle, query, Math.min(k, this.vectorCount));
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<boolean> {
|
||||
if (!this.handle) throw new Error('Index not initialized');
|
||||
|
||||
const { hnsw } = this.runtime.getModules();
|
||||
const deleted = hnsw.delete(this.handle, id);
|
||||
if (deleted) this.vectorCount--;
|
||||
return deleted;
|
||||
}
|
||||
|
||||
async serialize(): Promise<Uint8Array> {
|
||||
if (!this.handle) throw new Error('Index not initialized');
|
||||
|
||||
const { hnsw } = this.runtime.getModules();
|
||||
return hnsw.serialize(this.handle);
|
||||
}
|
||||
|
||||
async deserialize(data: Uint8Array): Promise<void> {
|
||||
const { hnsw } = this.runtime.getModules();
|
||||
|
||||
// Free existing handle if any
|
||||
if (this.handle) {
|
||||
hnsw.free(this.handle);
|
||||
}
|
||||
|
||||
this.handle = hnsw.deserialize(data);
|
||||
}
|
||||
|
||||
getStats(): IndexStats {
|
||||
return {
|
||||
vectorCount: this.vectorCount,
|
||||
dimensions: this.config.dimensions,
|
||||
m: this.config.m,
|
||||
efConstruction: this.config.efConstruction,
|
||||
efSearch: this.config.efSearch,
|
||||
backend: this.runtime.getBackend(),
|
||||
};
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
if (this.handle) {
|
||||
const { hnsw } = this.runtime.getModules();
|
||||
hnsw.free(this.handle);
|
||||
this.handle = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface HnswConfig {
|
||||
dimensions: number;
|
||||
m: number; // Max connections per node per layer
|
||||
efConstruction: number; // Build-time exploration factor
|
||||
efSearch: number; // Query-time exploration factor
|
||||
distanceMetric: 'cosine' | 'euclidean' | 'dot_product';
|
||||
}
|
||||
|
||||
interface SearchResult {
|
||||
id: string;
|
||||
score: number;
|
||||
}
|
||||
|
||||
interface IndexStats {
|
||||
vectorCount: number;
|
||||
dimensions: number;
|
||||
m: number;
|
||||
efConstruction: number;
|
||||
efSearch: number;
|
||||
backend: 'native' | 'wasm' | 'js-fallback';
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Memory Management
|
||||
|
||||
### WASM Memory Pooling
|
||||
|
||||
```typescript
|
||||
// Efficient memory management for WASM
|
||||
class WasmMemoryPool {
|
||||
private pools: Map<number, Float32Array[]> = new Map();
|
||||
private maxPoolSize = 100;
|
||||
|
||||
// Get or create a Float32Array of specified length
|
||||
acquire(length: number): Float32Array {
|
||||
const pool = this.pools.get(length);
|
||||
|
||||
if (pool && pool.length > 0) {
|
||||
return pool.pop()!;
|
||||
}
|
||||
|
||||
return new Float32Array(length);
|
||||
}
|
||||
|
||||
// Return array to pool for reuse
|
||||
release(array: Float32Array): void {
|
||||
const length = array.length;
|
||||
let pool = this.pools.get(length);
|
||||
|
||||
if (!pool) {
|
||||
pool = [];
|
||||
this.pools.set(length, pool);
|
||||
}
|
||||
|
||||
if (pool.length < this.maxPoolSize) {
|
||||
// Zero out for security
|
||||
array.fill(0);
|
||||
pool.push(array);
|
||||
}
|
||||
// Otherwise let GC handle it
|
||||
}
|
||||
|
||||
// Clear pools when memory pressure detected
|
||||
clear(): void {
|
||||
this.pools.clear();
|
||||
}
|
||||
|
||||
getStats(): PoolStats {
|
||||
const stats: PoolStats = { totalArrays: 0, totalBytes: 0, pools: {} };
|
||||
|
||||
for (const [length, pool] of this.pools) {
|
||||
stats.pools[length] = pool.length;
|
||||
stats.totalArrays += pool.length;
|
||||
stats.totalBytes += pool.length * length * 4; // 4 bytes per float32
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage in embedder
|
||||
class PooledWasmEmbedder extends WasmEmbedder {
|
||||
private pool = new WasmMemoryPool();
|
||||
|
||||
async embed(text: string): Promise<Float32Array> {
|
||||
const result = await super.embed(text);
|
||||
|
||||
// Copy to pooled array
|
||||
const pooled = this.pool.acquire(result.length);
|
||||
pooled.set(result);
|
||||
|
||||
return pooled;
|
||||
}
|
||||
|
||||
releaseEmbedding(embedding: Float32Array): void {
|
||||
this.pool.release(embedding);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Benchmarks
|
||||
|
||||
```typescript
|
||||
// Benchmark suite for runtime comparison
|
||||
class WasmBenchmarks {
|
||||
async runAll(): Promise<BenchmarkResults> {
|
||||
const results: BenchmarkResults = {};
|
||||
|
||||
// Embedding benchmarks
|
||||
results.embedSingle = await this.benchmarkEmbedSingle();
|
||||
results.embedBatch = await this.benchmarkEmbedBatch();
|
||||
|
||||
// HNSW benchmarks
|
||||
results.hnswInsert = await this.benchmarkHnswInsert();
|
||||
results.hnswSearch = await this.benchmarkHnswSearch();
|
||||
|
||||
// Vector operations
|
||||
results.distance = await this.benchmarkDistance();
|
||||
results.quantize = await this.benchmarkQuantize();
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private async benchmarkEmbedSingle(): Promise<BenchmarkResult> {
|
||||
const embedder = new WasmEmbedder({ modelPath: 'minilm-l6-v2' });
|
||||
await embedder.initialize();
|
||||
|
||||
const iterations = 100;
|
||||
const texts = Array(iterations).fill('This is a test sentence for embedding.');
|
||||
|
||||
const start = performance.now();
|
||||
for (const text of texts) {
|
||||
await embedder.embed(text);
|
||||
}
|
||||
const elapsed = performance.now() - start;
|
||||
|
||||
return {
|
||||
operation: 'embed_single',
|
||||
iterations,
|
||||
totalMs: elapsed,
|
||||
avgMs: elapsed / iterations,
|
||||
opsPerSecond: (iterations / elapsed) * 1000,
|
||||
};
|
||||
}
|
||||
|
||||
private async benchmarkHnswSearch(): Promise<BenchmarkResult> {
|
||||
const index = new WasmHnswIndex({
|
||||
dimensions: 384,
|
||||
m: 16,
|
||||
efConstruction: 100,
|
||||
efSearch: 50,
|
||||
distanceMetric: 'cosine',
|
||||
});
|
||||
await index.initialize();
|
||||
|
||||
// Insert 10k vectors
|
||||
for (let i = 0; i < 10000; i++) {
|
||||
await index.insert(`vec_${i}`, this.randomVector(384));
|
||||
}
|
||||
|
||||
const iterations = 1000;
|
||||
const query = this.randomVector(384);
|
||||
|
||||
const start = performance.now();
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
await index.search(query, 10);
|
||||
}
|
||||
const elapsed = performance.now() - start;
|
||||
|
||||
return {
|
||||
operation: 'hnsw_search_10k',
|
||||
iterations,
|
||||
totalMs: elapsed,
|
||||
avgMs: elapsed / iterations,
|
||||
opsPerSecond: (iterations / elapsed) * 1000,
|
||||
};
|
||||
}
|
||||
|
||||
private randomVector(dim: number): Float32Array {
|
||||
const vec = new Float32Array(dim);
|
||||
for (let i = 0; i < dim; i++) {
|
||||
vec[i] = Math.random() * 2 - 1;
|
||||
}
|
||||
return vec;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Consequences
|
||||
|
||||
### Benefits
|
||||
|
||||
1. **Universal Deployment**: Same code runs everywhere (Node, Edge, Browser)
|
||||
2. **Performance**: Near-native performance for vector operations
|
||||
3. **Fallback Safety**: Always works even without WASM support
|
||||
4. **Memory Efficiency**: Pooling and proper cleanup prevent leaks
|
||||
5. **Model Portability**: ONNX models run in any environment
|
||||
|
||||
### Trade-offs
|
||||
|
||||
| Benefit | Trade-off |
|
||||
|---------|-----------|
|
||||
| Portability | Slight overhead vs pure native |
|
||||
| WASM safety | No direct memory access (by design) |
|
||||
| Model caching | Disk/Cache API storage needed |
|
||||
| Lazy loading | First-use latency for initialization |
|
||||
|
||||
---
|
||||
|
||||
## Related Decisions
|
||||
|
||||
- **ADR-001**: Architecture Overview
|
||||
- **ADR-003**: Persistence Layer (vector storage)
|
||||
- **ADR-007**: Learning System (pattern WASM modules)
|
||||
|
||||
---
|
||||
|
||||
## Revision History
|
||||
|
||||
| Version | Date | Author | Changes |
|
||||
|---------|------|--------|---------|
|
||||
| 1.0 | 2026-01-27 | RuVector Architecture Team | Initial version |
|
||||
1134
vendor/ruvector/npm/packages/ruvbot/docs/adr/ADR-007-learning-system.md
vendored
Normal file
1134
vendor/ruvector/npm/packages/ruvbot/docs/adr/ADR-007-learning-system.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
151
vendor/ruvector/npm/packages/ruvbot/docs/adr/ADR-008-security-architecture.md
vendored
Normal file
151
vendor/ruvector/npm/packages/ruvbot/docs/adr/ADR-008-security-architecture.md
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
# ADR-008: Security Architecture
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Date
|
||||
2026-01-27
|
||||
|
||||
## Context
|
||||
|
||||
RuvBot handles sensitive data including:
|
||||
- User conversations and personal information
|
||||
- API credentials for LLM providers
|
||||
- Integration tokens (Slack, Discord)
|
||||
- Vector embeddings that may encode sensitive content
|
||||
- Multi-tenant data requiring strict isolation
|
||||
|
||||
## Decision
|
||||
|
||||
### Security Layers
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Security Architecture │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Layer 1: Transport Security │
|
||||
│ - TLS 1.3 for all connections │
|
||||
│ - Certificate pinning for external APIs │
|
||||
│ - HSTS enabled by default │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Layer 2: Authentication │
|
||||
│ - JWT tokens with RS256 signing │
|
||||
│ - OAuth 2.0 for Slack/Discord │
|
||||
│ - API key authentication with rate limiting │
|
||||
│ - Session tokens with secure rotation │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Layer 3: Authorization │
|
||||
│ - RBAC with claims-based permissions │
|
||||
│ - Tenant isolation at all layers │
|
||||
│ - Skill-level permission grants │
|
||||
│ - Resource-based access control │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Layer 4: Data Protection │
|
||||
│ - AES-256-GCM for data at rest │
|
||||
│ - Field-level encryption for sensitive data │
|
||||
│ - Key rotation with envelope encryption │
|
||||
│ - Secure secret management │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Layer 5: Input Validation │
|
||||
│ - Zod schema validation for all inputs │
|
||||
│ - SQL injection prevention (parameterized queries) │
|
||||
│ - XSS prevention (content sanitization) │
|
||||
│ - Path traversal prevention │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Layer 6: WASM Sandbox │
|
||||
│ - Memory isolation per operation │
|
||||
│ - Resource limits (CPU, memory) │
|
||||
│ - No filesystem access from WASM │
|
||||
│ - Controlled imports/exports │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Multi-Tenancy Security
|
||||
|
||||
```sql
|
||||
-- PostgreSQL Row-Level Security
|
||||
CREATE POLICY tenant_isolation ON memories
|
||||
USING (tenant_id = current_setting('app.current_tenant')::uuid);
|
||||
|
||||
CREATE POLICY tenant_isolation ON sessions
|
||||
USING (tenant_id = current_setting('app.current_tenant')::uuid);
|
||||
|
||||
CREATE POLICY tenant_isolation ON agents
|
||||
USING (tenant_id = current_setting('app.current_tenant')::uuid);
|
||||
```
|
||||
|
||||
### Secret Management
|
||||
|
||||
```typescript
|
||||
// Secrets are never logged or exposed
|
||||
interface SecretStore {
|
||||
get(key: string): Promise<string>;
|
||||
set(key: string, value: string, options?: SecretOptions): Promise<void>;
|
||||
rotate(key: string): Promise<void>;
|
||||
delete(key: string): Promise<void>;
|
||||
}
|
||||
|
||||
// Environment variable validation
|
||||
const requiredSecrets = z.object({
|
||||
ANTHROPIC_API_KEY: z.string().startsWith('sk-ant-'),
|
||||
SLACK_BOT_TOKEN: z.string().startsWith('xoxb-').optional(),
|
||||
DATABASE_URL: z.string().url().optional(),
|
||||
});
|
||||
```
|
||||
|
||||
### API Security
|
||||
|
||||
1. **Rate Limiting**: Per-tenant, per-endpoint limits
|
||||
2. **Request Signing**: HMAC-SHA256 for webhooks
|
||||
3. **IP Allowlisting**: Optional for enterprise
|
||||
4. **Audit Logging**: All security events logged
|
||||
|
||||
### Vulnerability Prevention
|
||||
|
||||
| CVE Category | Prevention |
|
||||
|--------------|------------|
|
||||
| Injection (SQL, NoSQL, Command) | Parameterized queries, input validation |
|
||||
| XSS | Content-Security-Policy, output encoding |
|
||||
| CSRF | SameSite cookies, origin validation |
|
||||
| SSRF | URL allowlisting, no user-controlled URLs |
|
||||
| Path Traversal | Path sanitization, chroot for file ops |
|
||||
| Sensitive Data Exposure | Encryption, minimal logging |
|
||||
| Broken Authentication | Secure session management |
|
||||
| Security Misconfiguration | Secure defaults, hardening guide |
|
||||
|
||||
### Compliance Readiness
|
||||
|
||||
- **GDPR**: Data export, deletion, consent tracking
|
||||
- **SOC 2**: Audit logging, access controls
|
||||
- **HIPAA**: Encryption, access logging (with configuration)
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Defense in depth provides multiple security layers
|
||||
- Multi-tenancy isolation prevents data leakage
|
||||
- Comprehensive input validation blocks injection attacks
|
||||
- WASM sandbox limits damage from malicious code
|
||||
|
||||
### Negative
|
||||
- Performance overhead from encryption/validation
|
||||
- Complexity in secret management
|
||||
- Additional testing required for security features
|
||||
|
||||
### Risks
|
||||
- Key management complexity
|
||||
- Potential for misconfiguration
|
||||
- Balance between security and usability
|
||||
|
||||
## Security Checklist
|
||||
|
||||
- [ ] TLS configured for all endpoints
|
||||
- [ ] API keys stored in secure vault
|
||||
- [ ] Rate limiting enabled
|
||||
- [ ] Audit logging configured
|
||||
- [ ] Input validation on all endpoints
|
||||
- [ ] SQL injection tests passing
|
||||
- [ ] XSS tests passing
|
||||
- [ ] CSRF protection enabled
|
||||
- [ ] Security headers configured
|
||||
- [ ] Dependency vulnerabilities scanned
|
||||
159
vendor/ruvector/npm/packages/ruvbot/docs/adr/ADR-009-hybrid-search.md
vendored
Normal file
159
vendor/ruvector/npm/packages/ruvbot/docs/adr/ADR-009-hybrid-search.md
vendored
Normal file
@@ -0,0 +1,159 @@
|
||||
# ADR-009: Hybrid Search Architecture
|
||||
|
||||
## Status
|
||||
Accepted (Implemented)
|
||||
|
||||
## Date
|
||||
2026-01-27
|
||||
|
||||
## Context
|
||||
|
||||
Clawdbot uses basic vector search with external embedding APIs. RuvBot improves on this with:
|
||||
- Local WASM embeddings (75x faster)
|
||||
- HNSW indexing (150x-12,500x faster)
|
||||
- Need for hybrid search combining vector + keyword (BM25)
|
||||
|
||||
## Decision
|
||||
|
||||
### Hybrid Search Pipeline
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ RuvBot Hybrid Search │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Query Input │
|
||||
│ └─ Text normalization │
|
||||
│ └─ Query embedding (WASM, <3ms) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Parallel Search (Promise.all) │
|
||||
│ ├─ Vector Search (HNSW) ├─ Keyword Search (BM25) │
|
||||
│ │ └─ Cosine similarity │ └─ Inverted index │
|
||||
│ │ └─ Top-K candidates │ └─ IDF + TF scoring │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Result Fusion │
|
||||
│ └─ Reciprocal Rank Fusion (RRF) │
|
||||
│ └─ Linear combination │
|
||||
│ └─ Weighted average with presence bonus │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Post-Processing │
|
||||
│ └─ Score normalization (BM25 max-normalized) │
|
||||
│ └─ Matched term tracking │
|
||||
│ └─ Threshold filtering │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Implementation
|
||||
|
||||
Located in `/npm/packages/ruvbot/src/learning/search/`:
|
||||
- `HybridSearch.ts` - Main hybrid search coordinator
|
||||
- `BM25Index.ts` - BM25 keyword search implementation
|
||||
|
||||
### Configuration
|
||||
|
||||
```typescript
|
||||
interface HybridSearchConfig {
|
||||
vector: {
|
||||
enabled: boolean;
|
||||
weight: number; // 0.0-1.0, default: 0.7
|
||||
};
|
||||
keyword: {
|
||||
enabled: boolean;
|
||||
weight: number; // 0.0-1.0, default: 0.3
|
||||
k1?: number; // BM25 k1 parameter, default: 1.2
|
||||
b?: number; // BM25 b parameter, default: 0.75
|
||||
};
|
||||
fusion: {
|
||||
method: 'rrf' | 'linear' | 'weighted';
|
||||
k: number; // RRF constant, default: 60
|
||||
candidateMultiplier: number; // default: 3
|
||||
};
|
||||
}
|
||||
|
||||
interface HybridSearchOptions {
|
||||
topK?: number; // default: 10
|
||||
threshold?: number; // default: 0
|
||||
vectorOnly?: boolean;
|
||||
keywordOnly?: boolean;
|
||||
}
|
||||
|
||||
interface HybridSearchResult {
|
||||
id: string;
|
||||
vectorScore: number;
|
||||
keywordScore: number;
|
||||
fusedScore: number;
|
||||
matchedTerms?: string[];
|
||||
}
|
||||
```
|
||||
|
||||
### Fusion Methods
|
||||
|
||||
| Method | Algorithm | Best For |
|
||||
|--------|-----------|----------|
|
||||
| `rrf` | Reciprocal Rank Fusion: `1/(k + rank)` | General use, rank-based |
|
||||
| `linear` | `α·vectorScore + β·keywordScore` | Score-sensitive ranking |
|
||||
| `weighted` | Linear + 0.1 bonus for dual matches | Boosting exact matches |
|
||||
|
||||
### BM25 Implementation
|
||||
|
||||
```typescript
|
||||
interface BM25Config {
|
||||
k1: number; // Term frequency saturation (default: 1.2)
|
||||
b: number; // Document length normalization (default: 0.75)
|
||||
}
|
||||
```
|
||||
|
||||
Features:
|
||||
- Inverted index with document frequency tracking
|
||||
- Built-in stopword filtering (100+ common words)
|
||||
- Basic Porter-style stemming (ing, ed, es, s, ly, tion)
|
||||
- Average document length normalization
|
||||
|
||||
### Performance Targets
|
||||
|
||||
| Operation | Target | Achieved |
|
||||
|-----------|--------|----------|
|
||||
| Query embedding | <5ms | 2.7ms |
|
||||
| Vector search (100K) | <10ms | <5ms |
|
||||
| Keyword search | <20ms | <15ms |
|
||||
| Fusion | <5ms | <2ms |
|
||||
| Total hybrid | <40ms | <25ms |
|
||||
|
||||
### Usage Example
|
||||
|
||||
```typescript
|
||||
import { HybridSearch, createHybridSearch } from './learning/search';
|
||||
|
||||
// Create with custom config
|
||||
const search = createHybridSearch({
|
||||
vector: { enabled: true, weight: 0.7 },
|
||||
keyword: { enabled: true, weight: 0.3, k1: 1.2, b: 0.75 },
|
||||
fusion: { method: 'rrf', k: 60, candidateMultiplier: 3 },
|
||||
});
|
||||
|
||||
// Initialize with vector index and embedder
|
||||
search.initialize(vectorIndex, embedder);
|
||||
|
||||
// Add documents
|
||||
await search.add('doc1', 'Document content here');
|
||||
|
||||
// Search
|
||||
const results = await search.search('query text', { topK: 10 });
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Better recall than vector-only search
|
||||
- Handles exact matches and semantic similarity
|
||||
- Maintains keyword search for debugging
|
||||
- Parallel search execution for low latency
|
||||
|
||||
### Negative
|
||||
- Slightly higher latency than vector-only
|
||||
- Requires maintaining both indices
|
||||
- More complex tuning
|
||||
|
||||
### Trade-offs
|
||||
- Weight tuning requires experimentation
|
||||
- Memory overhead for dual indices
|
||||
- BM25 stemming is basic (not full Porter algorithm)
|
||||
238
vendor/ruvector/npm/packages/ruvbot/docs/adr/ADR-010-multi-channel.md
vendored
Normal file
238
vendor/ruvector/npm/packages/ruvbot/docs/adr/ADR-010-multi-channel.md
vendored
Normal file
@@ -0,0 +1,238 @@
|
||||
# ADR-010: Multi-Channel Integration
|
||||
|
||||
## Status
|
||||
Accepted (Partially Implemented)
|
||||
|
||||
## Date
|
||||
2026-01-27
|
||||
|
||||
## Context
|
||||
|
||||
Clawdbot supports multiple messaging channels:
|
||||
- Slack, Discord, Telegram, Signal, WhatsApp, Line, iMessage
|
||||
- Web, CLI, API interfaces
|
||||
|
||||
RuvBot must match and exceed with:
|
||||
- All Clawdbot channels
|
||||
- Multi-tenant channel isolation
|
||||
- Unified message handling
|
||||
|
||||
## Decision
|
||||
|
||||
### Channel Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ RuvBot Channel Layer │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Channel Adapters │
|
||||
│ ├─ SlackAdapter : @slack/bolt [IMPLEMENTED] │
|
||||
│ ├─ DiscordAdapter : discord.js [IMPLEMENTED] │
|
||||
│ ├─ TelegramAdapter : telegraf [IMPLEMENTED] │
|
||||
│ ├─ SignalAdapter : signal-client [PLANNED] │
|
||||
│ ├─ WhatsAppAdapter : baileys [PLANNED] │
|
||||
│ ├─ LineAdapter : @line/bot-sdk [PLANNED] │
|
||||
│ ├─ WebAdapter : WebSocket + REST [PLANNED] │
|
||||
│ └─ CLIAdapter : readline + terminal [PLANNED] │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Message Normalization │
|
||||
│ └─ Unified Message format │
|
||||
│ └─ Attachment handling │
|
||||
│ └─ Thread/reply context │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Multi-Tenant Isolation │
|
||||
│ └─ Channel credentials per tenant │
|
||||
│ └─ Namespace isolation │
|
||||
│ └─ Rate limiting per tenant │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Implementation
|
||||
|
||||
Located in `/npm/packages/ruvbot/src/channels/`:
|
||||
- `ChannelRegistry.ts` - Central registry and routing
|
||||
- `adapters/BaseAdapter.ts` - Abstract base class
|
||||
- `adapters/SlackAdapter.ts` - Slack integration
|
||||
- `adapters/DiscordAdapter.ts` - Discord integration
|
||||
- `adapters/TelegramAdapter.ts` - Telegram integration
|
||||
|
||||
### Unified Message Interface
|
||||
|
||||
```typescript
|
||||
interface UnifiedMessage {
|
||||
id: string;
|
||||
channelId: string;
|
||||
channelType: ChannelType;
|
||||
tenantId: string;
|
||||
userId: string;
|
||||
username?: string;
|
||||
content: string;
|
||||
attachments?: Attachment[];
|
||||
threadId?: string;
|
||||
replyTo?: string;
|
||||
timestamp: Date;
|
||||
metadata: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface Attachment {
|
||||
id: string;
|
||||
type: 'image' | 'file' | 'audio' | 'video' | 'link';
|
||||
url?: string;
|
||||
data?: Buffer;
|
||||
mimeType?: string;
|
||||
filename?: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
type ChannelType =
|
||||
| 'slack' | 'discord' | 'telegram'
|
||||
| 'signal' | 'whatsapp' | 'line'
|
||||
| 'imessage' | 'web' | 'api' | 'cli';
|
||||
```
|
||||
|
||||
### BaseAdapter Abstract Class
|
||||
|
||||
```typescript
|
||||
abstract class BaseAdapter {
|
||||
type: ChannelType;
|
||||
tenantId: string;
|
||||
enabled: boolean;
|
||||
|
||||
// Lifecycle
|
||||
abstract connect(): Promise<void>;
|
||||
abstract disconnect(): Promise<void>;
|
||||
|
||||
// Messaging
|
||||
abstract send(channelId: string, content: string, options?: SendOptions): Promise<string>;
|
||||
abstract reply(message: UnifiedMessage, content: string, options?: SendOptions): Promise<string>;
|
||||
|
||||
// Event handling
|
||||
onMessage(handler: MessageHandler): void;
|
||||
offMessage(handler: MessageHandler): void;
|
||||
getStatus(): AdapterStatus;
|
||||
}
|
||||
```
|
||||
|
||||
### Channel Registry
|
||||
|
||||
```typescript
|
||||
interface ChannelRegistry {
|
||||
// Registration
|
||||
register(adapter: BaseAdapter): void;
|
||||
unregister(type: ChannelType, tenantId: string): boolean;
|
||||
|
||||
// Lookup
|
||||
get(type: ChannelType, tenantId: string): BaseAdapter | undefined;
|
||||
getByType(type: ChannelType): BaseAdapter[];
|
||||
getByTenant(tenantId: string): BaseAdapter[];
|
||||
getAll(): BaseAdapter[];
|
||||
|
||||
// Lifecycle
|
||||
start(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
|
||||
// Messaging
|
||||
onMessage(handler: MessageHandler): void;
|
||||
offMessage(handler: MessageHandler): void;
|
||||
broadcast(message: string, channelIds: string[], filter?: ChannelFilter): Promise<Map<string, string>>;
|
||||
|
||||
// Statistics
|
||||
getStats(): RegistryStats;
|
||||
}
|
||||
|
||||
interface ChannelRegistryConfig {
|
||||
defaultRateLimit?: {
|
||||
requests: number;
|
||||
windowMs: number;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Adapter Configuration
|
||||
|
||||
```typescript
|
||||
interface AdapterConfig {
|
||||
type: ChannelType;
|
||||
tenantId: string;
|
||||
credentials: ChannelCredentials;
|
||||
enabled?: boolean;
|
||||
rateLimit?: {
|
||||
requests: number;
|
||||
windowMs: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface ChannelCredentials {
|
||||
token?: string;
|
||||
apiKey?: string;
|
||||
webhookUrl?: string;
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
botId?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
```
|
||||
|
||||
### Usage Example
|
||||
|
||||
```typescript
|
||||
import { ChannelRegistry, SlackAdapter, DiscordAdapter } from './channels';
|
||||
|
||||
// Create registry with rate limiting
|
||||
const registry = new ChannelRegistry({
|
||||
defaultRateLimit: { requests: 100, windowMs: 60000 }
|
||||
});
|
||||
|
||||
// Register adapters
|
||||
registry.register(new SlackAdapter({
|
||||
type: 'slack',
|
||||
tenantId: 'tenant-1',
|
||||
credentials: { token: process.env.SLACK_TOKEN }
|
||||
}));
|
||||
|
||||
registry.register(new DiscordAdapter({
|
||||
type: 'discord',
|
||||
tenantId: 'tenant-1',
|
||||
credentials: { token: process.env.DISCORD_TOKEN }
|
||||
}));
|
||||
|
||||
// Handle messages
|
||||
registry.onMessage(async (message) => {
|
||||
console.log(`[${message.channelType}] ${message.userId}: ${message.content}`);
|
||||
});
|
||||
|
||||
// Start all adapters
|
||||
await registry.start();
|
||||
```
|
||||
|
||||
## Implementation Status
|
||||
|
||||
| Adapter | Status | Library | Notes |
|
||||
|---------|--------|---------|-------|
|
||||
| Slack | Implemented | @slack/bolt | Full support |
|
||||
| Discord | Implemented | discord.js | Full support |
|
||||
| Telegram | Implemented | telegraf | Full support |
|
||||
| Signal | Planned | signal-client | Requires native deps |
|
||||
| WhatsApp | Planned | baileys | Unofficial API |
|
||||
| Line | Planned | @line/bot-sdk | - |
|
||||
| Web | Planned | WebSocket | Custom implementation |
|
||||
| CLI | Planned | readline | For testing |
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Unified message handling across all channels
|
||||
- Multi-tenant channel isolation with per-tenant indexing
|
||||
- Easy to add new channels via BaseAdapter
|
||||
- Built-in rate limiting per adapter
|
||||
|
||||
### Negative
|
||||
- Complexity of maintaining multiple integrations
|
||||
- Different channel capabilities (some don't support threads)
|
||||
- Only 3 of 8+ channels currently implemented
|
||||
|
||||
### RuvBot Advantages over Clawdbot
|
||||
- Multi-tenant channel credentials with isolation
|
||||
- Channel-specific rate limiting
|
||||
- Cross-channel message routing via broadcast
|
||||
- Adapter status tracking and statistics
|
||||
205
vendor/ruvector/npm/packages/ruvbot/docs/adr/ADR-011-swarm-coordination.md
vendored
Normal file
205
vendor/ruvector/npm/packages/ruvbot/docs/adr/ADR-011-swarm-coordination.md
vendored
Normal file
@@ -0,0 +1,205 @@
|
||||
# ADR-011: Swarm Coordination (agentic-flow Integration)
|
||||
|
||||
## Status
|
||||
Accepted (Implemented)
|
||||
|
||||
## Date
|
||||
2026-01-27
|
||||
|
||||
## Context
|
||||
|
||||
Clawdbot has basic async processing. RuvBot integrates agentic-flow patterns for:
|
||||
- Multi-agent swarm coordination
|
||||
- 12 specialized background workers
|
||||
- Byzantine fault-tolerant consensus
|
||||
- Dynamic topology switching
|
||||
|
||||
## Decision
|
||||
|
||||
### Swarm Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ RuvBot Swarm Coordination │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Topologies │
|
||||
│ ├─ hierarchical : Queen-worker (anti-drift) │
|
||||
│ ├─ mesh : Peer-to-peer network │
|
||||
│ ├─ hierarchical-mesh : Hybrid for scalability │
|
||||
│ └─ adaptive : Dynamic switching │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Consensus Protocols │
|
||||
│ ├─ byzantine : BFT (f < n/3 faulty) │
|
||||
│ ├─ raft : Leader-based (f < n/2) │
|
||||
│ ├─ gossip : Eventually consistent │
|
||||
│ └─ crdt : Conflict-free replication │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Background Workers (12) │
|
||||
│ ├─ ultralearn [normal] : Deep knowledge acquisition │
|
||||
│ ├─ optimize [high] : Performance optimization │
|
||||
│ ├─ consolidate [low] : Memory consolidation (EWC++) │
|
||||
│ ├─ predict [normal] : Predictive preloading │
|
||||
│ ├─ audit [critical] : Security analysis │
|
||||
│ ├─ map [normal] : Codebase mapping │
|
||||
│ ├─ preload [low] : Resource preloading │
|
||||
│ ├─ deepdive [normal] : Deep code analysis │
|
||||
│ ├─ document [normal] : Auto-documentation │
|
||||
│ ├─ refactor [normal] : Refactoring suggestions │
|
||||
│ ├─ benchmark [normal] : Performance benchmarking │
|
||||
│ └─ testgaps [normal] : Test coverage analysis │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Implementation
|
||||
|
||||
Located in `/npm/packages/ruvbot/src/swarm/`:
|
||||
- `SwarmCoordinator.ts` - Main coordinator with task dispatch
|
||||
- `ByzantineConsensus.ts` - PBFT-style consensus implementation
|
||||
|
||||
### SwarmCoordinator
|
||||
|
||||
```typescript
|
||||
interface SwarmConfig {
|
||||
topology: SwarmTopology; // 'hierarchical' | 'mesh' | 'hierarchical-mesh' | 'adaptive'
|
||||
maxAgents: number; // default: 8
|
||||
strategy: 'specialized' | 'balanced' | 'adaptive';
|
||||
consensus: ConsensusProtocol; // 'byzantine' | 'raft' | 'gossip' | 'crdt'
|
||||
heartbeatInterval?: number; // default: 5000ms
|
||||
taskTimeout?: number; // default: 60000ms
|
||||
}
|
||||
|
||||
interface SwarmTask {
|
||||
id: string;
|
||||
worker: WorkerType;
|
||||
type: string;
|
||||
content: unknown;
|
||||
priority: WorkerPriority;
|
||||
status: 'pending' | 'running' | 'completed' | 'failed';
|
||||
assignedAgent?: string;
|
||||
result?: unknown;
|
||||
error?: string;
|
||||
createdAt: Date;
|
||||
startedAt?: Date;
|
||||
completedAt?: Date;
|
||||
}
|
||||
|
||||
interface SwarmAgent {
|
||||
id: string;
|
||||
type: WorkerType;
|
||||
status: 'idle' | 'busy' | 'offline';
|
||||
currentTask?: string;
|
||||
completedTasks: number;
|
||||
failedTasks: number;
|
||||
lastHeartbeat: Date;
|
||||
}
|
||||
```
|
||||
|
||||
### Worker Configuration
|
||||
|
||||
```typescript
|
||||
const WORKER_DEFAULTS: Record<WorkerType, WorkerConfig> = {
|
||||
ultralearn: { priority: 'normal', concurrency: 2, timeout: 60000, retries: 3, backoff: 'exponential' },
|
||||
optimize: { priority: 'high', concurrency: 4, timeout: 30000, retries: 2, backoff: 'exponential' },
|
||||
consolidate: { priority: 'low', concurrency: 1, timeout: 120000, retries: 1, backoff: 'linear' },
|
||||
predict: { priority: 'normal', concurrency: 2, timeout: 15000, retries: 2, backoff: 'exponential' },
|
||||
audit: { priority: 'critical', concurrency: 1, timeout: 45000, retries: 3, backoff: 'exponential' },
|
||||
map: { priority: 'normal', concurrency: 2, timeout: 60000, retries: 2, backoff: 'linear' },
|
||||
preload: { priority: 'low', concurrency: 4, timeout: 10000, retries: 1, backoff: 'linear' },
|
||||
deepdive: { priority: 'normal', concurrency: 2, timeout: 90000, retries: 2, backoff: 'exponential' },
|
||||
document: { priority: 'normal', concurrency: 2, timeout: 30000, retries: 2, backoff: 'linear' },
|
||||
refactor: { priority: 'normal', concurrency: 2, timeout: 60000, retries: 2, backoff: 'exponential' },
|
||||
benchmark: { priority: 'normal', concurrency: 1, timeout: 120000, retries: 1, backoff: 'linear' },
|
||||
testgaps: { priority: 'normal', concurrency: 2, timeout: 45000, retries: 2, backoff: 'linear' },
|
||||
};
|
||||
```
|
||||
|
||||
### ByzantineConsensus (PBFT)
|
||||
|
||||
```typescript
|
||||
interface ConsensusConfig {
|
||||
replicas: number; // Total number of replicas (default: 5)
|
||||
timeout: number; // Timeout per phase (default: 30000ms)
|
||||
retries: number; // Retries before failing (default: 3)
|
||||
requireSignatures: boolean;
|
||||
}
|
||||
|
||||
// Fault tolerance: f < n/3
|
||||
// Quorum size: ceil(2n/3)
|
||||
```
|
||||
|
||||
**Phases:**
|
||||
1. `pre-prepare` - Leader broadcasts proposal
|
||||
2. `prepare` - Replicas validate and send prepare messages
|
||||
3. `commit` - Wait for quorum of commit messages
|
||||
4. `decided` - Consensus reached
|
||||
5. `failed` - Consensus failed (timeout/Byzantine fault)
|
||||
|
||||
### Usage Example
|
||||
|
||||
```typescript
|
||||
import { SwarmCoordinator, ByzantineConsensus } from './swarm';
|
||||
|
||||
// Initialize swarm
|
||||
const swarm = new SwarmCoordinator({
|
||||
topology: 'hierarchical',
|
||||
maxAgents: 8,
|
||||
strategy: 'specialized',
|
||||
consensus: 'raft'
|
||||
});
|
||||
|
||||
await swarm.start();
|
||||
|
||||
// Spawn specialized agents
|
||||
await swarm.spawnAgent('ultralearn');
|
||||
await swarm.spawnAgent('optimize');
|
||||
|
||||
// Dispatch task
|
||||
const task = await swarm.dispatch({
|
||||
worker: 'ultralearn',
|
||||
task: { type: 'deep-analysis', content: 'analyze this' },
|
||||
priority: 'normal'
|
||||
});
|
||||
|
||||
// Wait for completion
|
||||
const result = await swarm.waitForTask(task.id);
|
||||
|
||||
// Byzantine consensus for critical decisions
|
||||
const consensus = new ByzantineConsensus({ replicas: 5, timeout: 30000 });
|
||||
consensus.initializeReplicas(['node1', 'node2', 'node3', 'node4', 'node5']);
|
||||
const decision = await consensus.propose({ action: 'deploy', version: '1.0.0' });
|
||||
```
|
||||
|
||||
### Events
|
||||
|
||||
SwarmCoordinator emits:
|
||||
- `started`, `stopped`
|
||||
- `agent:spawned`, `agent:removed`, `agent:offline`
|
||||
- `task:created`, `task:assigned`, `task:completed`, `task:failed`
|
||||
|
||||
ByzantineConsensus emits:
|
||||
- `proposal:created`
|
||||
- `phase:pre-prepare`, `phase:prepare`, `phase:commit`
|
||||
- `vote:received`
|
||||
- `consensus:decided`, `consensus:failed`, `consensus:no-quorum`
|
||||
- `replica:faulty`, `view:changed`
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Distributed task execution with priority queues
|
||||
- Fault tolerance via PBFT consensus
|
||||
- Specialized workers for different task types
|
||||
- Heartbeat-based health monitoring
|
||||
- Event-driven architecture
|
||||
|
||||
### Negative
|
||||
- Coordination overhead
|
||||
- Complexity of distributed systems
|
||||
- Memory overhead for task/agent tracking
|
||||
|
||||
### RuvBot Advantages over Clawdbot
|
||||
- 12 specialized workers vs basic async
|
||||
- Byzantine fault tolerance vs none
|
||||
- Multi-topology support vs single-threaded
|
||||
- Learning workers (ultralearn, consolidate) vs static
|
||||
- Priority-based task scheduling
|
||||
376
vendor/ruvector/npm/packages/ruvbot/docs/adr/ADR-012-llm-providers.md
vendored
Normal file
376
vendor/ruvector/npm/packages/ruvbot/docs/adr/ADR-012-llm-providers.md
vendored
Normal file
@@ -0,0 +1,376 @@
|
||||
# ADR-012: LLM Provider Integration
|
||||
|
||||
## Status
|
||||
Accepted (Implemented)
|
||||
|
||||
## Date
|
||||
2026-01-27
|
||||
|
||||
## Context
|
||||
|
||||
RuvBot requires LLM capabilities for:
|
||||
- Conversational AI responses
|
||||
- Reasoning and analysis tasks
|
||||
- Tool/function calling
|
||||
- Streaming responses for real-time UX
|
||||
|
||||
The system needs to support multiple providers to:
|
||||
- Allow cost optimization (use cheaper models for simple tasks)
|
||||
- Provide fallback options
|
||||
- Access specialized models (reasoning models like QwQ, O1, DeepSeek R1)
|
||||
- Support both direct API access and unified gateways
|
||||
|
||||
## Decision
|
||||
|
||||
### Provider Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ RuvBot LLM Provider Layer │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Provider Interface │
|
||||
│ └─ LLMProvider (abstract interface) │
|
||||
│ ├─ complete() - Single completion │
|
||||
│ ├─ stream() - Streaming completion (AsyncGenerator) │
|
||||
│ ├─ countTokens() - Token estimation │
|
||||
│ ├─ getModel() - Model info │
|
||||
│ └─ isHealthy() - Health check │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Implementations │
|
||||
│ ├─ AnthropicProvider : Direct Anthropic API │
|
||||
│ │ └─ Claude 4, 3.5, 3 models │
|
||||
│ └─ OpenRouterProvider : Multi-model gateway │
|
||||
│ ├─ Qwen QwQ (reasoning) │
|
||||
│ ├─ DeepSeek R1 (reasoning) │
|
||||
│ ├─ Claude via OpenRouter │
|
||||
│ ├─ GPT-4, O1 via OpenRouter │
|
||||
│ └─ Gemini, Llama via OpenRouter │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Features │
|
||||
│ ├─ Tool/Function calling │
|
||||
│ ├─ Streaming with token callbacks │
|
||||
│ ├─ Automatic retry with backoff │
|
||||
│ └─ Token counting │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Implementation
|
||||
|
||||
Located in `/npm/packages/ruvbot/src/integration/providers/`:
|
||||
- `index.ts` - Interface definitions and exports
|
||||
- `AnthropicProvider.ts` - Anthropic Claude integration
|
||||
- `OpenRouterProvider.ts` - OpenRouter multi-model gateway
|
||||
|
||||
### LLMProvider Interface
|
||||
|
||||
```typescript
|
||||
interface LLMProvider {
|
||||
complete(messages: Message[], options?: CompletionOptions): Promise<Completion>;
|
||||
stream(messages: Message[], options?: StreamOptions): AsyncGenerator<Token, Completion, void>;
|
||||
countTokens(text: string): Promise<number>;
|
||||
getModel(): ModelInfo;
|
||||
isHealthy(): Promise<boolean>;
|
||||
}
|
||||
|
||||
interface Message {
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface CompletionOptions {
|
||||
maxTokens?: number;
|
||||
temperature?: number; // 0.0-2.0
|
||||
topP?: number; // 0.0-1.0
|
||||
stopSequences?: string[];
|
||||
tools?: Tool[];
|
||||
}
|
||||
|
||||
interface StreamOptions extends CompletionOptions {
|
||||
onToken?: (token: string) => void;
|
||||
}
|
||||
|
||||
interface Completion {
|
||||
content: string;
|
||||
finishReason: 'stop' | 'length' | 'tool_use';
|
||||
usage: {
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
};
|
||||
toolCalls?: ToolCall[];
|
||||
}
|
||||
|
||||
interface Token {
|
||||
type: 'text' | 'tool_use';
|
||||
text?: string;
|
||||
toolUse?: ToolCall;
|
||||
}
|
||||
```
|
||||
|
||||
### Tool/Function Calling
|
||||
|
||||
```typescript
|
||||
interface Tool {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: Record<string, unknown>; // JSON Schema
|
||||
}
|
||||
|
||||
interface ToolCall {
|
||||
id: string;
|
||||
name: string;
|
||||
input: Record<string, unknown>;
|
||||
}
|
||||
```
|
||||
|
||||
### AnthropicProvider
|
||||
|
||||
Direct integration with Anthropic's Claude API.
|
||||
|
||||
```typescript
|
||||
interface AnthropicConfig {
|
||||
apiKey: string;
|
||||
baseUrl?: string; // default: 'https://api.anthropic.com'
|
||||
model?: string; // default: 'claude-3-5-sonnet-20241022'
|
||||
maxRetries?: number; // default: 3
|
||||
timeout?: number; // default: 60000ms
|
||||
}
|
||||
|
||||
type AnthropicModel =
|
||||
| 'claude-opus-4-20250514'
|
||||
| 'claude-sonnet-4-20250514'
|
||||
| 'claude-3-5-sonnet-20241022'
|
||||
| 'claude-3-5-haiku-20241022'
|
||||
| 'claude-3-opus-20240229'
|
||||
| 'claude-3-sonnet-20240229'
|
||||
| 'claude-3-haiku-20240307';
|
||||
```
|
||||
|
||||
**Model Specifications:**
|
||||
|
||||
| Model | Max Tokens | Context Window | Best For |
|
||||
|-------|------------|----------------|----------|
|
||||
| claude-opus-4-20250514 | 32,768 | 200,000 | Complex reasoning, analysis |
|
||||
| claude-sonnet-4-20250514 | 16,384 | 200,000 | Balanced performance |
|
||||
| claude-3-5-sonnet-20241022 | 8,192 | 200,000 | General purpose |
|
||||
| claude-3-5-haiku-20241022 | 8,192 | 200,000 | Fast, cost-effective |
|
||||
| claude-3-opus-20240229 | 4,096 | 200,000 | Complex tasks |
|
||||
| claude-3-sonnet-20240229 | 4,096 | 200,000 | Balanced |
|
||||
| claude-3-haiku-20240307 | 4,096 | 200,000 | Fast responses |
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
import { createAnthropicProvider } from './integration/providers';
|
||||
|
||||
const provider = createAnthropicProvider({
|
||||
apiKey: process.env.ANTHROPIC_API_KEY!,
|
||||
model: 'claude-3-5-sonnet-20241022',
|
||||
});
|
||||
|
||||
// Simple completion
|
||||
const response = await provider.complete([
|
||||
{ role: 'user', content: 'Hello!' }
|
||||
]);
|
||||
|
||||
// Streaming
|
||||
for await (const token of provider.stream(messages)) {
|
||||
if (token.type === 'text') {
|
||||
process.stdout.write(token.text!);
|
||||
}
|
||||
}
|
||||
|
||||
// With tools
|
||||
const toolResponse = await provider.complete(messages, {
|
||||
tools: [{
|
||||
name: 'get_weather',
|
||||
description: 'Get weather for a location',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
location: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}]
|
||||
});
|
||||
```
|
||||
|
||||
### OpenRouterProvider
|
||||
|
||||
Access to 100+ models through OpenRouter's unified API.
|
||||
|
||||
```typescript
|
||||
interface OpenRouterConfig {
|
||||
apiKey: string;
|
||||
baseUrl?: string; // default: 'https://openrouter.ai/api'
|
||||
model?: string; // default: 'qwen/qwq-32b'
|
||||
siteUrl?: string; // For attribution
|
||||
siteName?: string; // default: 'RuvBot'
|
||||
maxRetries?: number; // default: 3
|
||||
timeout?: number; // default: 120000ms (longer for reasoning)
|
||||
}
|
||||
|
||||
type OpenRouterModel =
|
||||
// Reasoning Models
|
||||
| 'qwen/qwq-32b'
|
||||
| 'qwen/qwq-32b:free'
|
||||
| 'openai/o1-preview'
|
||||
| 'openai/o1-mini'
|
||||
| 'deepseek/deepseek-r1'
|
||||
// Standard Models
|
||||
| 'anthropic/claude-3.5-sonnet'
|
||||
| 'openai/gpt-4o'
|
||||
| 'google/gemini-pro-1.5'
|
||||
| 'meta-llama/llama-3.1-405b-instruct'
|
||||
| string; // Any OpenRouter model
|
||||
```
|
||||
|
||||
**Reasoning Model Specifications:**
|
||||
|
||||
| Model | Max Tokens | Context | Special Features |
|
||||
|-------|------------|---------|------------------|
|
||||
| qwen/qwq-32b | 16,384 | 32,768 | Chain-of-thought reasoning |
|
||||
| qwen/qwq-32b:free | 16,384 | 32,768 | Free tier available |
|
||||
| openai/o1-preview | 32,768 | 128,000 | Advanced reasoning |
|
||||
| openai/o1-mini | 65,536 | 128,000 | Faster reasoning |
|
||||
| deepseek/deepseek-r1 | 8,192 | 64,000 | Open-source reasoning |
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
import {
|
||||
createOpenRouterProvider,
|
||||
createQwQProvider,
|
||||
createDeepSeekR1Provider
|
||||
} from './integration/providers';
|
||||
|
||||
// General OpenRouter
|
||||
const provider = createOpenRouterProvider({
|
||||
apiKey: process.env.OPENROUTER_API_KEY!,
|
||||
model: 'qwen/qwq-32b',
|
||||
});
|
||||
|
||||
// Convenience: QwQ reasoning model
|
||||
const qwq = createQwQProvider(process.env.OPENROUTER_API_KEY!, false);
|
||||
|
||||
// Convenience: Free QwQ
|
||||
const qwqFree = createQwQProvider(process.env.OPENROUTER_API_KEY!, true);
|
||||
|
||||
// Convenience: DeepSeek R1
|
||||
const deepseek = createDeepSeekR1Provider(process.env.OPENROUTER_API_KEY!);
|
||||
|
||||
// List available models
|
||||
const models = await provider.listModels();
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
**Environment Variables:**
|
||||
|
||||
```bash
|
||||
# Anthropic
|
||||
ANTHROPIC_API_KEY=sk-ant-...
|
||||
|
||||
# OpenRouter
|
||||
OPENROUTER_API_KEY=sk-or-...
|
||||
```
|
||||
|
||||
**Rate Limiting:**
|
||||
- Both providers use native fetch with `AbortSignal.timeout()`
|
||||
- Anthropic: 60s default timeout
|
||||
- OpenRouter: 120s default timeout (for reasoning models)
|
||||
|
||||
**Retry Strategy:**
|
||||
- Default: 3 retries
|
||||
- Backoff: Not implemented in base (use with retry libraries)
|
||||
|
||||
### Performance Benchmarks
|
||||
|
||||
| Operation | Anthropic | OpenRouter |
|
||||
|-----------|-----------|------------|
|
||||
| Cold start | ~500ms | ~800ms |
|
||||
| Token latency (first) | ~200ms | ~300ms |
|
||||
| Throughput (tokens/s) | ~50 | ~40 |
|
||||
| Tool call parsing | <10ms | <10ms |
|
||||
|
||||
### Error Handling
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const response = await provider.complete(messages);
|
||||
} catch (error) {
|
||||
if (error.message.includes('API error: 429')) {
|
||||
// Rate limited - implement backoff
|
||||
} else if (error.message.includes('API error: 401')) {
|
||||
// Invalid API key
|
||||
} else if (error.message.includes('timeout')) {
|
||||
// Request timed out
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Usage Patterns
|
||||
|
||||
**Model Routing by Task Complexity:**
|
||||
|
||||
```typescript
|
||||
function selectProvider(taskComplexity: 'simple' | 'medium' | 'complex' | 'reasoning') {
|
||||
switch (taskComplexity) {
|
||||
case 'simple':
|
||||
return createAnthropicProvider({ apiKey, model: 'claude-3-5-haiku-20241022' });
|
||||
case 'medium':
|
||||
return createAnthropicProvider({ apiKey, model: 'claude-3-5-sonnet-20241022' });
|
||||
case 'complex':
|
||||
return createAnthropicProvider({ apiKey, model: 'claude-opus-4-20250514' });
|
||||
case 'reasoning':
|
||||
return createQwQProvider(openRouterApiKey);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fallback Chain:**
|
||||
|
||||
```typescript
|
||||
async function completeWithFallback(messages: Message[]) {
|
||||
const providers = [
|
||||
createAnthropicProvider({ apiKey, model: 'claude-3-5-sonnet-20241022' }),
|
||||
createOpenRouterProvider({ apiKey: orKey, model: 'anthropic/claude-3.5-sonnet' }),
|
||||
createQwQProvider(orKey, true), // Free fallback
|
||||
];
|
||||
|
||||
for (const provider of providers) {
|
||||
try {
|
||||
if (await provider.isHealthy()) {
|
||||
return await provider.complete(messages);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Provider failed, trying next:`, error);
|
||||
}
|
||||
}
|
||||
throw new Error('All providers failed');
|
||||
}
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Unified interface for multiple LLM providers
|
||||
- Access to 100+ models through OpenRouter
|
||||
- Native streaming support with token callbacks
|
||||
- Tool/function calling support
|
||||
- Easy provider switching for cost optimization
|
||||
|
||||
### Negative
|
||||
- Token counting is approximate (not tiktoken-based)
|
||||
- No built-in retry with exponential backoff
|
||||
- System messages handled differently by providers
|
||||
|
||||
### Trade-offs
|
||||
- OpenRouter adds latency vs direct API calls
|
||||
- Reasoning models (QwQ, O1) have longer timeouts
|
||||
- Free tiers have rate limits and quotas
|
||||
|
||||
### RuvBot Advantages
|
||||
- Multi-provider support vs single provider
|
||||
- Reasoning model access (QwQ, DeepSeek R1, O1)
|
||||
- Factory functions for common configurations
|
||||
- Streaming with async generators
|
||||
263
vendor/ruvector/npm/packages/ruvbot/docs/adr/ADR-013-gcp-deployment.md
vendored
Normal file
263
vendor/ruvector/npm/packages/ruvbot/docs/adr/ADR-013-gcp-deployment.md
vendored
Normal file
@@ -0,0 +1,263 @@
|
||||
# ADR-013: Google Cloud Platform Deployment Architecture
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Date
|
||||
2026-01-27
|
||||
|
||||
## Context
|
||||
|
||||
RuvBot needs a production-ready deployment option that:
|
||||
1. Minimizes operational costs for low-traffic scenarios
|
||||
2. Scales automatically with demand
|
||||
3. Provides persistence for sessions, memory, and learning data
|
||||
4. Secures API keys and credentials
|
||||
5. Supports multi-tenant deployments
|
||||
|
||||
## Decision
|
||||
|
||||
Deploy RuvBot on Google Cloud Platform using serverless and managed services optimized for cost.
|
||||
|
||||
### Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Google Cloud Platform │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Cloud │ │ Cloud │ │ Cloud │ │
|
||||
│ │ Build │───▶│ Registry │───▶│ Run │ │
|
||||
│ │ (CI/CD) │ │ (Images) │ │ (App) │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────┬───────┘ │
|
||||
│ │ │
|
||||
│ ┌────────────────────────────┼────────────────────────┐ │
|
||||
│ │ │ │ │
|
||||
│ ┌──────▼──────┐ ┌────────────────▼───────────┐ │ │
|
||||
│ │ Secret │ │ Cloud SQL │ │ │
|
||||
│ │ Manager │ │ (PostgreSQL) │ │ │
|
||||
│ │ │ │ db-f1-micro │ │ │
|
||||
│ └─────────────┘ └────────────────────────────┘ │ │
|
||||
│ │ │
|
||||
│ ┌─────────────┐ ┌────────────────────────────┐ │ │
|
||||
│ │ Cloud │ │ Memorystore │ │ │
|
||||
│ │ Storage │ │ (Redis) - Optional │ │ │
|
||||
│ │ (Files) │ │ Basic tier │ │ │
|
||||
│ └─────────────┘ └────────────────────────────┘ │ │
|
||||
│ │ │
|
||||
│ └────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Cost Optimization Strategy
|
||||
|
||||
| Service | Configuration | Monthly Cost | Notes |
|
||||
|---------|--------------|--------------|-------|
|
||||
| Cloud Run | 0-10 instances, 512Mi RAM | ~$0-5 | Free tier: 2M requests |
|
||||
| Cloud SQL | db-f1-micro, 10GB SSD | ~$10-15 | Smallest instance |
|
||||
| Secret Manager | 3-5 secrets | ~$0.18 | $0.06/secret/month |
|
||||
| Cloud Storage | Standard, lifecycle policies | ~$0.02/GB | Auto-tiering |
|
||||
| Cloud Build | Free tier | ~$0 | 120 min/day free |
|
||||
| **Total (low traffic)** | | **~$15-20/month** | |
|
||||
|
||||
### Service Configuration
|
||||
|
||||
#### Cloud Run (Compute)
|
||||
|
||||
```yaml
|
||||
# Serverless container configuration
|
||||
resources:
|
||||
cpu: "1"
|
||||
memory: "512Mi"
|
||||
scaling:
|
||||
minInstances: 0 # Scale to zero when idle
|
||||
maxInstances: 10 # Limit for cost control
|
||||
concurrency: 80 # Requests per instance
|
||||
features:
|
||||
cpuIdle: true # Reduce CPU when idle (cost savings)
|
||||
startupCpuBoost: true # Faster cold starts
|
||||
timeout: 300s # 5 minutes for long operations
|
||||
```
|
||||
|
||||
#### Cloud SQL (Database)
|
||||
|
||||
```hcl
|
||||
# Cost-optimized PostgreSQL
|
||||
tier = "db-f1-micro" # 0.6GB RAM, shared CPU
|
||||
disk_size = 10 # Minimum SSD
|
||||
availability = "ZONAL" # Single zone (cheaper)
|
||||
backup_retention = 7 # 7 days
|
||||
|
||||
# Extensions enabled
|
||||
- uuid-ossp # UUID generation
|
||||
- pgcrypto # Cryptographic functions
|
||||
- pg_trgm # Text search (trigram similarity)
|
||||
```
|
||||
|
||||
#### Secret Manager
|
||||
|
||||
Securely stores:
|
||||
- `anthropic-api-key` - Anthropic API credentials
|
||||
- `openrouter-api-key` - OpenRouter API credentials
|
||||
- `database-url` - PostgreSQL connection string
|
||||
|
||||
#### Cloud Storage
|
||||
|
||||
```hcl
|
||||
# Automatic cost optimization
|
||||
lifecycle_rules = [
|
||||
{ age = 30, action = "SetStorageClass", class = "NEARLINE" },
|
||||
{ age = 90, action = "SetStorageClass", class = "COLDLINE" }
|
||||
]
|
||||
```
|
||||
|
||||
### Deployment Options
|
||||
|
||||
#### Option 1: Quick Deploy (gcloud CLI)
|
||||
|
||||
```bash
|
||||
# Set environment variables
|
||||
export ANTHROPIC_API_KEY="sk-ant-..."
|
||||
export PROJECT_ID="my-project"
|
||||
|
||||
# Run deployment script
|
||||
./deploy/gcp/deploy.sh --project-id $PROJECT_ID
|
||||
```
|
||||
|
||||
#### Option 2: Infrastructure as Code (Terraform)
|
||||
|
||||
```bash
|
||||
cd deploy/gcp/terraform
|
||||
|
||||
terraform init
|
||||
terraform plan -var="project_id=my-project" -var="anthropic_api_key=sk-ant-..."
|
||||
terraform apply
|
||||
```
|
||||
|
||||
#### Option 3: CI/CD (Cloud Build)
|
||||
|
||||
```yaml
|
||||
# Trigger on push to main branch
|
||||
trigger:
|
||||
branch: main
|
||||
included_files:
|
||||
- "npm/packages/ruvbot/**"
|
||||
|
||||
# cloudbuild.yaml handles build and deploy
|
||||
```
|
||||
|
||||
### Multi-Tenant Configuration
|
||||
|
||||
For multiple tenants:
|
||||
|
||||
```hcl
|
||||
# Separate Cloud SQL databases
|
||||
resource "google_sql_database" "tenant" {
|
||||
for_each = var.tenants
|
||||
name = "ruvbot_${each.key}"
|
||||
instance = google_sql_database_instance.ruvbot.name
|
||||
}
|
||||
|
||||
# Row-Level Security in PostgreSQL
|
||||
ALTER TABLE sessions ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY tenant_isolation ON sessions
|
||||
USING (tenant_id = current_setting('app.tenant_id')::uuid);
|
||||
```
|
||||
|
||||
### Scaling Considerations
|
||||
|
||||
| Traffic Level | Cloud Run Instances | Cloud SQL | Estimated Cost |
|
||||
|---------------|---------------------|-----------|----------------|
|
||||
| Low (<1K req/day) | 0-1 | db-f1-micro | ~$15/month |
|
||||
| Medium (<10K req/day) | 1-3 | db-g1-small | ~$40/month |
|
||||
| High (<100K req/day) | 3-10 | db-custom | ~$150/month |
|
||||
| Enterprise | 10-100 | Regional HA | ~$500+/month |
|
||||
|
||||
### Security Configuration
|
||||
|
||||
```hcl
|
||||
# Service account with minimal permissions
|
||||
roles = [
|
||||
"roles/secretmanager.secretAccessor",
|
||||
"roles/cloudsql.client",
|
||||
"roles/storage.objectAdmin",
|
||||
"roles/logging.logWriter",
|
||||
"roles/monitoring.metricWriter",
|
||||
]
|
||||
|
||||
# Network security
|
||||
ip_configuration {
|
||||
ipv4_enabled = false # Production: use private IP
|
||||
private_network = google_compute_network.vpc.id
|
||||
}
|
||||
```
|
||||
|
||||
### Health Monitoring
|
||||
|
||||
```yaml
|
||||
# Cloud Run health checks
|
||||
startup_probe:
|
||||
http_get:
|
||||
path: /health
|
||||
port: 8080
|
||||
initial_delay_seconds: 5
|
||||
timeout_seconds: 3
|
||||
period_seconds: 10
|
||||
|
||||
liveness_probe:
|
||||
http_get:
|
||||
path: /health
|
||||
port: 8080
|
||||
timeout_seconds: 3
|
||||
period_seconds: 30
|
||||
```
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
deploy/
|
||||
├── gcp/
|
||||
│ ├── cloudbuild.yaml # CI/CD pipeline
|
||||
│ ├── deploy.sh # Quick deployment script
|
||||
│ └── terraform/
|
||||
│ └── main.tf # Infrastructure as code
|
||||
├── init-db.sql # Database schema
|
||||
├── Dockerfile # Container image
|
||||
└── docker-compose.yml # Local development
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- **Cost-effective**: ~$15-20/month for low traffic
|
||||
- **Serverless**: Scale to zero when not in use
|
||||
- **Managed services**: No infrastructure maintenance
|
||||
- **Security**: Secret Manager, IAM, VPC support
|
||||
- **Observability**: Built-in logging and monitoring
|
||||
|
||||
### Negative
|
||||
- **Cold starts**: First request after idle ~2-3 seconds
|
||||
- **Vendor lock-in**: GCP-specific services
|
||||
- **Complexity**: Multiple services to configure
|
||||
|
||||
### Trade-offs
|
||||
- **Cloud SQL vs Firestore**: SQL chosen for complex queries, Row-Level Security
|
||||
- **Cloud Run vs GKE**: Run chosen for simplicity, lower cost
|
||||
- **db-f1-micro vs larger**: Cost vs performance trade-off
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
| Option | Pros | Cons | Estimated Cost |
|
||||
|--------|------|------|----------------|
|
||||
| GKE + Postgres | Full control, predictable | Complex, expensive | ~$100+/month |
|
||||
| App Engine | Simple deployment | Less flexible | ~$30/month |
|
||||
| Firebase + Functions | Easy scaling | No SQL, vendor lock | ~$20/month |
|
||||
| **Cloud Run + SQL** | **Balanced** | **Some complexity** | **~$15/month** |
|
||||
|
||||
## References
|
||||
|
||||
- [Cloud Run Pricing](https://cloud.google.com/run/pricing)
|
||||
- [Cloud SQL Pricing](https://cloud.google.com/sql/pricing)
|
||||
- [Terraform GCP Provider](https://registry.terraform.io/providers/hashicorp/google/latest/docs)
|
||||
- [Cloud Build CI/CD](https://cloud.google.com/build/docs)
|
||||
246
vendor/ruvector/npm/packages/ruvbot/docs/adr/ADR-014-aidefence-integration.md
vendored
Normal file
246
vendor/ruvector/npm/packages/ruvbot/docs/adr/ADR-014-aidefence-integration.md
vendored
Normal file
@@ -0,0 +1,246 @@
|
||||
# ADR-014: AIDefence Integration for Adversarial Protection
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Date
|
||||
2026-01-27
|
||||
|
||||
## Context
|
||||
|
||||
RuvBot requires robust protection against adversarial attacks including:
|
||||
- Prompt injection (OWASP #1 LLM vulnerability)
|
||||
- Jailbreak attempts
|
||||
- PII leakage
|
||||
- Malicious code injection
|
||||
- Data exfiltration
|
||||
|
||||
The `aidefence` package provides production-ready adversarial defense with <10ms detection latency.
|
||||
|
||||
## Decision
|
||||
|
||||
Integrate `aidefence@2.1.1` into RuvBot as a core security layer.
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ RuvBot Security Layer │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ User Input ────┐ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ AIDefenceGuard │ │
|
||||
│ ├──────────────────────────────────────────────────────────────────────┤ │
|
||||
│ │ Layer 1: Pattern Detection (<5ms) │ │
|
||||
│ │ └─ 50+ injection signatures │ │
|
||||
│ │ └─ Jailbreak patterns (DAN, bypass, etc.) │ │
|
||||
│ │ └─ Custom patterns (configurable) │ │
|
||||
│ ├──────────────────────────────────────────────────────────────────────┤ │
|
||||
│ │ Layer 2: PII Detection (<5ms) │ │
|
||||
│ │ └─ Email, phone, SSN, credit card │ │
|
||||
│ │ └─ API keys and tokens │ │
|
||||
│ │ └─ IP addresses │ │
|
||||
│ ├──────────────────────────────────────────────────────────────────────┤ │
|
||||
│ │ Layer 3: Sanitization (<1ms) │ │
|
||||
│ │ └─ Control character removal │ │
|
||||
│ │ └─ Unicode homoglyph normalization │ │
|
||||
│ │ └─ PII masking │ │
|
||||
│ ├──────────────────────────────────────────────────────────────────────┤ │
|
||||
│ │ Layer 4: Behavioral Analysis (<100ms) [Optional] │ │
|
||||
│ │ └─ User behavior baseline │ │
|
||||
│ │ └─ Anomaly detection │ │
|
||||
│ │ └─ Deviation scoring │ │
|
||||
│ └──────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────┐ │
|
||||
│ │ Safe? │────No───► Block / Sanitize │
|
||||
│ └────┬─────┘ │
|
||||
│ │ Yes │
|
||||
│ ▼ │
|
||||
│ LLM Provider │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Response Validation │ │
|
||||
│ │ └─ PII leak detection │ │
|
||||
│ │ └─ Injection echo detection │ │
|
||||
│ │ └─ Malicious code detection │ │
|
||||
│ └──────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ Safe Response ────► User │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Threat Types Detected
|
||||
|
||||
| Threat Type | Severity | Detection Method | Response |
|
||||
|-------------|----------|------------------|----------|
|
||||
| Prompt Injection | High | Pattern matching | Block/Sanitize |
|
||||
| Jailbreak | Critical | Signature detection | Block |
|
||||
| PII Exposure | Medium-Critical | Regex patterns | Mask |
|
||||
| Malicious Code | High | AST-like patterns | Block |
|
||||
| Data Exfiltration | High | URL/webhook detection | Block |
|
||||
| Control Characters | Medium | Unicode analysis | Remove |
|
||||
| Encoding Attacks | Medium | Homoglyph detection | Normalize |
|
||||
| Anomalous Behavior | Medium | Baseline deviation | Alert |
|
||||
|
||||
### Performance Targets
|
||||
|
||||
| Operation | Target | Achieved |
|
||||
|-----------|--------|----------|
|
||||
| Pattern Detection | <10ms | ~5ms |
|
||||
| PII Detection | <10ms | ~3ms |
|
||||
| Sanitization | <5ms | ~1ms |
|
||||
| Full Analysis | <20ms | ~10ms |
|
||||
| Response Validation | <15ms | ~8ms |
|
||||
|
||||
### Usage
|
||||
|
||||
```typescript
|
||||
import { createAIDefenceGuard, createAIDefenceMiddleware } from '@ruvector/ruvbot';
|
||||
|
||||
// Simple usage
|
||||
const guard = createAIDefenceGuard({
|
||||
detectPromptInjection: true,
|
||||
detectJailbreak: true,
|
||||
detectPII: true,
|
||||
blockThreshold: 'medium',
|
||||
});
|
||||
|
||||
const result = await guard.analyze(userInput, {
|
||||
userId: 'user-123',
|
||||
sessionId: 'session-456',
|
||||
});
|
||||
|
||||
if (!result.safe) {
|
||||
console.log('Threats detected:', result.threats);
|
||||
// Use sanitized input or block
|
||||
const safeInput = result.sanitizedInput;
|
||||
}
|
||||
|
||||
// Middleware usage
|
||||
const middleware = createAIDefenceMiddleware({
|
||||
blockThreshold: 'medium',
|
||||
enableAuditLog: true,
|
||||
});
|
||||
|
||||
// Validate input before LLM
|
||||
const { allowed, sanitizedInput } = await middleware.validateInput(userInput);
|
||||
|
||||
if (allowed) {
|
||||
const response = await llm.complete(sanitizedInput);
|
||||
|
||||
// Validate response before returning
|
||||
const { allowed: responseAllowed } = await middleware.validateOutput(response, userInput);
|
||||
|
||||
if (responseAllowed) {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
```typescript
|
||||
interface AIDefenceConfig {
|
||||
// Detection toggles
|
||||
detectPromptInjection: boolean; // Default: true
|
||||
detectJailbreak: boolean; // Default: true
|
||||
detectPII: boolean; // Default: true
|
||||
|
||||
// Advanced features
|
||||
enableBehavioralAnalysis: boolean; // Default: false
|
||||
enablePolicyVerification: boolean; // Default: false
|
||||
|
||||
// Threshold: 'none' | 'low' | 'medium' | 'high' | 'critical'
|
||||
blockThreshold: ThreatLevel; // Default: 'medium'
|
||||
|
||||
// Custom patterns (regex strings)
|
||||
customPatterns?: string[];
|
||||
|
||||
// Allowed domains for URL validation
|
||||
allowedDomains?: string[];
|
||||
|
||||
// Max input length (chars)
|
||||
maxInputLength: number; // Default: 100000
|
||||
|
||||
// Audit logging
|
||||
enableAuditLog: boolean; // Default: true
|
||||
}
|
||||
```
|
||||
|
||||
### Preset Configurations
|
||||
|
||||
```typescript
|
||||
// Strict mode (production)
|
||||
const strictConfig = createStrictConfig();
|
||||
// - All detection enabled
|
||||
// - Behavioral analysis enabled
|
||||
// - Block threshold: 'low'
|
||||
|
||||
// Permissive mode (development)
|
||||
const permissiveConfig = createPermissiveConfig();
|
||||
// - Core detection only
|
||||
// - Block threshold: 'critical'
|
||||
// - Audit logging disabled
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Sub-10ms detection latency
|
||||
- 50+ built-in injection patterns
|
||||
- PII protection out of the box
|
||||
- Configurable security levels
|
||||
- Audit logging for compliance
|
||||
- Response validation
|
||||
- Unicode/homoglyph protection
|
||||
|
||||
### Negative
|
||||
- Additional dependency (aidefence)
|
||||
- Small latency overhead (~10ms per request)
|
||||
- False positives possible with strict settings
|
||||
|
||||
### Trade-offs
|
||||
- Strict mode may block legitimate queries
|
||||
- Behavioral analysis adds latency (~100ms)
|
||||
- PII masking may alter valid content
|
||||
|
||||
## Integration with Existing Security
|
||||
|
||||
AIDefence integrates with RuvBot's 6-layer security architecture:
|
||||
|
||||
```
|
||||
Layer 1: Transport (TLS 1.3)
|
||||
Layer 2: Authentication (JWT)
|
||||
Layer 3: Authorization (RBAC)
|
||||
Layer 4: Data Protection (Encryption)
|
||||
Layer 5: Input Validation (AIDefence) ◄── NEW
|
||||
Layer 6: WASM Sandbox
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
```json
|
||||
{
|
||||
"aidefence": "^2.1.1"
|
||||
}
|
||||
```
|
||||
|
||||
The aidefence package includes:
|
||||
- agentdb (vector storage)
|
||||
- lean-agentic (formal verification)
|
||||
- zod (schema validation)
|
||||
- winston (logging)
|
||||
- helmet (HTTP security headers)
|
||||
|
||||
## References
|
||||
|
||||
- [aidefence on npm](https://www.npmjs.com/package/aidefence)
|
||||
- [OWASP LLM Top 10](https://owasp.org/www-project-top-10-for-large-language-model-applications/)
|
||||
- [Prompt Injection Guide](https://www.lakera.ai/blog/guide-to-prompt-injection)
|
||||
- [AIMDS Documentation](https://ruv.io/aimds)
|
||||
192
vendor/ruvector/npm/packages/ruvbot/docs/adr/ADR-015-chat-ui.md
vendored
Normal file
192
vendor/ruvector/npm/packages/ruvbot/docs/adr/ADR-015-chat-ui.md
vendored
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 |
|
||||
BIN
vendor/ruvector/npm/packages/ruvbot/kernel/bzImage
vendored
Normal file
BIN
vendor/ruvector/npm/packages/ruvbot/kernel/bzImage
vendored
Normal file
Binary file not shown.
146
vendor/ruvector/npm/packages/ruvbot/package.json
vendored
Normal file
146
vendor/ruvector/npm/packages/ruvbot/package.json
vendored
Normal file
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"name": "ruvbot",
|
||||
"version": "0.3.1",
|
||||
"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",
|
||||
"types": "dist/index.d.ts",
|
||||
"bin": {
|
||||
"ruvbot": "./bin/ruvbot.js"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/esm/index.d.ts",
|
||||
"default": "./dist/esm/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"./core": {
|
||||
"import": "./dist/esm/core/index.js",
|
||||
"require": "./dist/core/index.js"
|
||||
},
|
||||
"./integrations/slack": {
|
||||
"import": "./dist/esm/integration/slack/index.js",
|
||||
"require": "./dist/integration/slack/index.js"
|
||||
},
|
||||
"./integrations/webhooks": {
|
||||
"import": "./dist/esm/integration/webhooks/index.js",
|
||||
"require": "./dist/integration/webhooks/index.js"
|
||||
},
|
||||
"./learning": {
|
||||
"import": "./dist/esm/learning/index.js",
|
||||
"require": "./dist/learning/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run build:cjs && npm run build:esm",
|
||||
"build:cjs": "tsc -p tsconfig.json",
|
||||
"build:esm": "tsc -p tsconfig.esm.json",
|
||||
"build:rvf": "node scripts/build-rvf.js",
|
||||
"run:rvf": "node scripts/run-rvf.js",
|
||||
"inspect:rvf": "node scripts/run-rvf.js --inspect",
|
||||
"dev": "tsc -w",
|
||||
"test": "vitest",
|
||||
"test:coverage": "vitest --coverage",
|
||||
"lint": "eslint src --ext .ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"clean": "rm -rf dist",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"keywords": [
|
||||
"ai",
|
||||
"ai-assistant",
|
||||
"chatbot",
|
||||
"llm",
|
||||
"gemini",
|
||||
"claude",
|
||||
"openai",
|
||||
"openrouter",
|
||||
"vector-database",
|
||||
"vector-search",
|
||||
"hnsw",
|
||||
"embeddings",
|
||||
"wasm",
|
||||
"memory",
|
||||
"self-learning",
|
||||
"neural",
|
||||
"multi-tenant",
|
||||
"enterprise",
|
||||
"security",
|
||||
"prompt-injection",
|
||||
"aidefence",
|
||||
"slack-bot",
|
||||
"discord-bot",
|
||||
"typescript",
|
||||
"cli"
|
||||
],
|
||||
"author": "RuVector Team <team@ruv.io>",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ruvnet/ruvector.git",
|
||||
"directory": "npm/packages/ruvbot"
|
||||
},
|
||||
"homepage": "https://github.com/ruvnet/ruvector/tree/main/npm/packages/ruvbot",
|
||||
"bugs": {
|
||||
"url": "https://github.com/ruvnet/ruvector/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.20.0",
|
||||
"aidefence": "^2.1.1",
|
||||
"commander": "^12.0.0",
|
||||
"chalk": "^5.3.0",
|
||||
"ora": "^8.0.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"uuid": "^9.0.0",
|
||||
"pino": "^8.17.2",
|
||||
"pino-pretty": "^10.3.1",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@ruvector/ruvllm": "^2.3.0",
|
||||
"@slack/bolt": "^3.17.0",
|
||||
"@slack/web-api": "^7.0.0",
|
||||
"bullmq": "^5.1.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"pg": "^8.11.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.5",
|
||||
"@types/pg": "^8.10.9",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"eslint": "^8.56.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vitest": "^1.2.0",
|
||||
"@vitest/coverage-v8": "^1.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"bin",
|
||||
"scripts",
|
||||
"kernel/bzImage",
|
||||
"ruvbot.rvf",
|
||||
"src/api/public",
|
||||
".env.example",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
BIN
vendor/ruvector/npm/packages/ruvbot/ruvbot.rvf
vendored
Normal file
BIN
vendor/ruvector/npm/packages/ruvbot/ruvbot.rvf
vendored
Normal file
Binary file not shown.
506
vendor/ruvector/npm/packages/ruvbot/scripts/build-rvf.js
vendored
Normal file
506
vendor/ruvector/npm/packages/ruvbot/scripts/build-rvf.js
vendored
Normal file
@@ -0,0 +1,506 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* build-rvf.js — Assemble RuvBot as a self-contained .rvf file.
|
||||
*
|
||||
* The output contains:
|
||||
* KERNEL_SEG (0x0E) — Real Linux 6.6 microkernel (bzImage, x86_64)
|
||||
* WASM_SEG (0x10) — RuvBot runtime bundle (Node.js application)
|
||||
* META_SEG (0x07) — Package metadata (name, version, config)
|
||||
* PROFILE_SEG (0x0B) — AI assistant domain profile
|
||||
* WITNESS_SEG (0x0A) — Build provenance chain
|
||||
* MANIFEST_SEG(0x05) — Segment directory + epoch
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/build-rvf.js --kernel /path/to/bzImage [--output ruvbot.rvf]
|
||||
*
|
||||
* The --kernel flag provides a real Linux bzImage to embed. If omitted,
|
||||
* the script looks for kernel/bzImage relative to the package root.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const { writeFileSync, readFileSync, existsSync, readdirSync, statSync, mkdirSync } = require('fs');
|
||||
const { join, resolve, isAbsolute } = require('path');
|
||||
const { createHash } = require('crypto');
|
||||
const { execSync } = require('child_process');
|
||||
const { gzipSync } = require('zlib');
|
||||
|
||||
// ─── RVF format constants ───────────────────────────────────────────────────
|
||||
const SEGMENT_MAGIC = 0x5256_4653; // "RVFS" big-endian
|
||||
const SEGMENT_VERSION = 1;
|
||||
const KERNEL_MAGIC = 0x5256_4B4E; // "RVKN" big-endian
|
||||
const WASM_MAGIC = 0x5256_574D; // "RVWM" big-endian
|
||||
|
||||
// Segment type discriminators
|
||||
const SEG_MANIFEST = 0x05;
|
||||
const SEG_META = 0x07;
|
||||
const SEG_WITNESS = 0x0A;
|
||||
const SEG_PROFILE = 0x0B;
|
||||
const SEG_KERNEL = 0x0E;
|
||||
const SEG_WASM = 0x10;
|
||||
|
||||
// Kernel constants
|
||||
const KERNEL_ARCH_X86_64 = 0x00;
|
||||
const KERNEL_TYPE_MICROLINUX = 0x01;
|
||||
const KERNEL_FLAG_HAS_NETWORKING = 1 << 3;
|
||||
const KERNEL_FLAG_HAS_QUERY_API = 1 << 4;
|
||||
const KERNEL_FLAG_HAS_ADMIN_API = 1 << 6;
|
||||
const KERNEL_FLAG_RELOCATABLE = 1 << 11;
|
||||
const KERNEL_FLAG_HAS_VIRTIO_NET = 1 << 12;
|
||||
const KERNEL_FLAG_HAS_VIRTIO_BLK = 1 << 13;
|
||||
const KERNEL_FLAG_HAS_VSOCK = 1 << 14;
|
||||
const KERNEL_FLAG_COMPRESSED = 1 << 10;
|
||||
|
||||
// WASM constants
|
||||
const WASM_ROLE_COMBINED = 0x02;
|
||||
const WASM_TARGET_NODE = 0x01;
|
||||
|
||||
// ─── Binary helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
function writeU8(buf, offset, val) {
|
||||
buf[offset] = val & 0xFF;
|
||||
return offset + 1;
|
||||
}
|
||||
|
||||
function writeU16LE(buf, offset, val) {
|
||||
buf.writeUInt16LE(val, offset);
|
||||
return offset + 2;
|
||||
}
|
||||
|
||||
function writeU32LE(buf, offset, val) {
|
||||
buf.writeUInt32LE(val >>> 0, offset);
|
||||
return offset + 4;
|
||||
}
|
||||
|
||||
function writeU64LE(buf, offset, val) {
|
||||
const big = BigInt(Math.floor(val));
|
||||
buf.writeBigUInt64LE(big, offset);
|
||||
return offset + 8;
|
||||
}
|
||||
|
||||
function contentHash16(payload) {
|
||||
return createHash('sha256').update(payload).digest().subarray(0, 16);
|
||||
}
|
||||
|
||||
// ─── Segment header writer (64 bytes) ───────────────────────────────────────
|
||||
|
||||
function makeSegmentHeader(segType, segId, payloadLength, payload) {
|
||||
const buf = Buffer.alloc(64);
|
||||
writeU32LE(buf, 0x00, SEGMENT_MAGIC);
|
||||
writeU8(buf, 0x04, SEGMENT_VERSION);
|
||||
writeU8(buf, 0x05, segType);
|
||||
writeU16LE(buf, 0x06, 0); // flags
|
||||
writeU64LE(buf, 0x08, segId);
|
||||
writeU64LE(buf, 0x10, payloadLength);
|
||||
writeU64LE(buf, 0x18, Date.now() * 1e6); // timestamp_ns
|
||||
writeU8(buf, 0x20, 0); // checksum_algo (CRC32C)
|
||||
writeU8(buf, 0x21, 0); // compression
|
||||
writeU16LE(buf, 0x22, 0); // reserved_0
|
||||
writeU32LE(buf, 0x24, 0); // reserved_1
|
||||
contentHash16(payload).copy(buf, 0x28, 0, 16); // content_hash
|
||||
writeU32LE(buf, 0x38, 0); // uncompressed_len
|
||||
writeU32LE(buf, 0x3C, 0); // alignment_pad
|
||||
return buf;
|
||||
}
|
||||
|
||||
// ─── Kernel header (128 bytes) ──────────────────────────────────────────────
|
||||
|
||||
function makeKernelHeader(imageSize, compressedSize, cmdlineLen, isCompressed) {
|
||||
const buf = Buffer.alloc(128);
|
||||
writeU32LE(buf, 0x00, KERNEL_MAGIC);
|
||||
writeU16LE(buf, 0x04, 1); // header_version
|
||||
writeU8(buf, 0x06, KERNEL_ARCH_X86_64);
|
||||
writeU8(buf, 0x07, KERNEL_TYPE_MICROLINUX);
|
||||
const flags = KERNEL_FLAG_HAS_NETWORKING
|
||||
| KERNEL_FLAG_HAS_QUERY_API
|
||||
| KERNEL_FLAG_HAS_ADMIN_API
|
||||
| KERNEL_FLAG_RELOCATABLE
|
||||
| KERNEL_FLAG_HAS_VIRTIO_NET
|
||||
| KERNEL_FLAG_HAS_VIRTIO_BLK
|
||||
| (isCompressed ? KERNEL_FLAG_COMPRESSED : 0);
|
||||
writeU32LE(buf, 0x08, flags);
|
||||
writeU32LE(buf, 0x0C, 64); // min_memory_mb
|
||||
writeU64LE(buf, 0x10, 0x1000000); // entry_point (16 MB default load)
|
||||
writeU64LE(buf, 0x18, imageSize); // image_size (uncompressed)
|
||||
writeU64LE(buf, 0x20, compressedSize); // compressed_size
|
||||
writeU8(buf, 0x28, isCompressed ? 1 : 0); // compression (0=none, 1=gzip)
|
||||
writeU8(buf, 0x29, 0x00); // api_transport (TcpHttp)
|
||||
writeU16LE(buf, 0x2A, 3000); // api_port
|
||||
writeU16LE(buf, 0x2C, 1); // api_version
|
||||
// 0x2E: 2 bytes padding
|
||||
// 0x30: image_hash (32 bytes) — filled by caller
|
||||
// 0x50: build_id (16 bytes)
|
||||
writeU64LE(buf, 0x60, Math.floor(Date.now() / 1000)); // build_timestamp
|
||||
writeU8(buf, 0x68, 1); // vcpu_count
|
||||
// 0x69: reserved_0
|
||||
// 0x6A: 2 bytes padding
|
||||
writeU32LE(buf, 0x6C, 128); // cmdline_offset
|
||||
writeU32LE(buf, 0x70, cmdlineLen); // cmdline_length
|
||||
return buf;
|
||||
}
|
||||
|
||||
// ─── WASM header (64 bytes) ─────────────────────────────────────────────────
|
||||
|
||||
function makeWasmHeader(bytecodeSize) {
|
||||
const buf = Buffer.alloc(64);
|
||||
writeU32LE(buf, 0x00, WASM_MAGIC);
|
||||
writeU16LE(buf, 0x04, 1); // header_version
|
||||
writeU8(buf, 0x06, WASM_ROLE_COMBINED); // role
|
||||
writeU8(buf, 0x07, WASM_TARGET_NODE); // target
|
||||
writeU16LE(buf, 0x08, 0); // required_features
|
||||
writeU16LE(buf, 0x0A, 12); // export_count
|
||||
writeU32LE(buf, 0x0C, bytecodeSize); // bytecode_size
|
||||
writeU32LE(buf, 0x10, 0); // compressed_size
|
||||
writeU8(buf, 0x14, 0); // compression
|
||||
writeU8(buf, 0x15, 2); // min_memory_pages
|
||||
writeU16LE(buf, 0x16, 0); // max_memory_pages
|
||||
writeU16LE(buf, 0x18, 0); // table_count
|
||||
// 0x1A: 2 bytes padding
|
||||
// 0x1C: bytecode_hash (32 bytes) — filled by caller
|
||||
writeU8(buf, 0x3C, 0); // bootstrap_priority
|
||||
writeU8(buf, 0x3D, 0); // interpreter_type
|
||||
return buf;
|
||||
}
|
||||
|
||||
// ─── Load real kernel image ─────────────────────────────────────────────────
|
||||
|
||||
function loadKernelImage(kernelPath) {
|
||||
if (!existsSync(kernelPath)) {
|
||||
console.error(`ERROR: Kernel image not found: ${kernelPath}`);
|
||||
console.error('Build one with: cd /tmp/linux-6.6.80 && make tinyconfig && make -j$(nproc) bzImage');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const raw = readFileSync(kernelPath);
|
||||
const stat = statSync(kernelPath);
|
||||
console.log(` Loaded: ${kernelPath} (${(raw.length / 1024).toFixed(0)} KB)`);
|
||||
|
||||
// Verify it looks like a real kernel (ELF or bzImage magic)
|
||||
const magic = raw.readUInt16LE(0);
|
||||
const elfMagic = raw.subarray(0, 4);
|
||||
if (elfMagic[0] === 0x7F && elfMagic[1] === 0x45 && elfMagic[2] === 0x4C && elfMagic[3] === 0x46) {
|
||||
console.log(' Format: ELF vmlinux');
|
||||
} else if (raw.length > 0x202 && raw.readUInt16LE(0x1FE) === 0xAA55) {
|
||||
console.log(' Format: bzImage (bootable)');
|
||||
} else {
|
||||
console.log(' Format: raw kernel image');
|
||||
}
|
||||
|
||||
// Gzip compress for smaller RVF
|
||||
const compressed = gzipSync(raw, { level: 9 });
|
||||
const ratio = ((1 - compressed.length / raw.length) * 100).toFixed(1);
|
||||
console.log(` Compressed: ${(compressed.length / 1024).toFixed(0)} KB (${ratio}% reduction)`);
|
||||
|
||||
return { raw, compressed };
|
||||
}
|
||||
|
||||
// ─── Build the runtime bundle ───────────────────────────────────────────────
|
||||
|
||||
function buildRuntimeBundle(pkgDir) {
|
||||
const distDir = join(pkgDir, 'dist');
|
||||
const binDir = join(pkgDir, 'bin');
|
||||
const files = [];
|
||||
|
||||
if (existsSync(distDir)) collectFiles(distDir, '', files);
|
||||
if (existsSync(binDir)) collectFiles(binDir, 'bin/', files);
|
||||
|
||||
const pkgJsonPath = join(pkgDir, 'package.json');
|
||||
if (existsSync(pkgJsonPath)) {
|
||||
files.push({ path: 'package.json', data: readFileSync(pkgJsonPath) });
|
||||
}
|
||||
|
||||
// Bundle format: [file_count(u32)] [file_table] [file_data]
|
||||
const fileCount = Buffer.alloc(4);
|
||||
fileCount.writeUInt32LE(files.length, 0);
|
||||
|
||||
let tableSize = 0;
|
||||
for (const f of files) {
|
||||
tableSize += 2 + 8 + 8 + Buffer.byteLength(f.path, 'utf8');
|
||||
}
|
||||
|
||||
let dataOffset = 4 + tableSize;
|
||||
const tableEntries = [];
|
||||
for (const f of files) {
|
||||
const pathBuf = Buffer.from(f.path, 'utf8');
|
||||
const entry = Buffer.alloc(2 + 8 + 8 + pathBuf.length);
|
||||
let o = writeU16LE(entry, 0, pathBuf.length);
|
||||
o = writeU64LE(entry, o, dataOffset);
|
||||
o = writeU64LE(entry, o, f.data.length);
|
||||
pathBuf.copy(entry, o);
|
||||
tableEntries.push(entry);
|
||||
dataOffset += f.data.length;
|
||||
}
|
||||
|
||||
return Buffer.concat([fileCount, ...tableEntries, ...files.map(f => f.data)]);
|
||||
}
|
||||
|
||||
function collectFiles(dir, prefix, files) {
|
||||
for (const name of readdirSync(dir)) {
|
||||
const full = join(dir, name);
|
||||
const rel = prefix + name;
|
||||
const stat = statSync(full);
|
||||
if (stat.isDirectory()) collectFiles(full, rel + '/', files);
|
||||
else if (stat.isFile()) files.push({ path: rel, data: readFileSync(full) });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Build META_SEG ─────────────────────────────────────────────────────────
|
||||
|
||||
function buildMetaPayload(pkgDir, kernelInfo) {
|
||||
const pkgJson = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8'));
|
||||
return Buffer.from(JSON.stringify({
|
||||
name: pkgJson.name,
|
||||
version: pkgJson.version,
|
||||
description: pkgJson.description,
|
||||
format: 'rvf-self-contained',
|
||||
runtime: 'node',
|
||||
runtime_version: '>=18.0.0',
|
||||
arch: 'x86_64',
|
||||
kernel: {
|
||||
type: 'microlinux',
|
||||
version: '6.6.80',
|
||||
config: 'tinyconfig+virtio+net',
|
||||
image_size: kernelInfo.rawSize,
|
||||
compressed_size: kernelInfo.compressedSize,
|
||||
},
|
||||
build_time: new Date().toISOString(),
|
||||
builder: 'ruvbot/build-rvf.js',
|
||||
capabilities: [
|
||||
'self-booting',
|
||||
'api-server',
|
||||
'chat',
|
||||
'vector-search',
|
||||
'self-learning',
|
||||
'multi-llm',
|
||||
'security-scanning',
|
||||
],
|
||||
dependencies: Object.keys(pkgJson.dependencies || {}),
|
||||
entrypoint: 'bin/ruvbot.js',
|
||||
api_port: 3000,
|
||||
firecracker_compatible: true,
|
||||
}), 'utf8');
|
||||
}
|
||||
|
||||
// ─── Build PROFILE_SEG ──────────────────────────────────────────────────────
|
||||
|
||||
function buildProfilePayload() {
|
||||
return Buffer.from(JSON.stringify({
|
||||
profile_id: 0x42,
|
||||
domain: 'ai-assistant',
|
||||
name: 'RuvBot',
|
||||
version: '0.2.0',
|
||||
capabilities: {
|
||||
chat: true,
|
||||
vector_search: true,
|
||||
embeddings: true,
|
||||
self_learning: true,
|
||||
multi_model: true,
|
||||
security: true,
|
||||
self_booting: true,
|
||||
},
|
||||
models: [
|
||||
'claude-sonnet-4-20250514',
|
||||
'gemini-2.0-flash',
|
||||
'gpt-4o',
|
||||
'openrouter/*',
|
||||
],
|
||||
boot_config: {
|
||||
vcpus: 1,
|
||||
memory_mb: 64,
|
||||
api_port: 3000,
|
||||
cmdline: 'console=ttyS0 ruvbot.mode=rvf',
|
||||
},
|
||||
}), 'utf8');
|
||||
}
|
||||
|
||||
// ─── Build WITNESS_SEG ──────────────────────────────────────────────────────
|
||||
|
||||
function buildWitnessPayload(kernelHash, runtimeHash) {
|
||||
return Buffer.from(JSON.stringify({
|
||||
witness_type: 'build_provenance',
|
||||
timestamp: new Date().toISOString(),
|
||||
builder: {
|
||||
tool: 'build-rvf.js',
|
||||
node_version: process.version,
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
},
|
||||
artifacts: {
|
||||
kernel: { hash_sha256: kernelHash, type: 'linux-6.6-bzimage' },
|
||||
runtime: { hash_sha256: runtimeHash, type: 'nodejs-bundle' },
|
||||
},
|
||||
chain: [],
|
||||
}), 'utf8');
|
||||
}
|
||||
|
||||
// ─── Assemble the RVF ───────────────────────────────────────────────────────
|
||||
|
||||
function assembleRvf(pkgDir, outputPath, kernelPath) {
|
||||
console.log('Building self-contained RuvBot RVF...');
|
||||
console.log(` Package: ${pkgDir}`);
|
||||
console.log(` Kernel: ${kernelPath}`);
|
||||
console.log(` Output: ${outputPath}\n`);
|
||||
|
||||
let segId = 1;
|
||||
const segments = [];
|
||||
const segDir = [];
|
||||
|
||||
// 1. KERNEL_SEG — Real Linux microkernel
|
||||
console.log(' [1/6] Embedding Linux 6.6 microkernel...');
|
||||
const { raw: kernelRaw, compressed: kernelCompressed } = loadKernelImage(kernelPath);
|
||||
const cmdline = Buffer.from('console=ttyS0 ruvbot.api_port=3000 ruvbot.mode=rvf quiet', 'utf8');
|
||||
const kernelHdr = makeKernelHeader(
|
||||
kernelRaw.length,
|
||||
kernelCompressed.length,
|
||||
cmdline.length,
|
||||
true // compressed
|
||||
);
|
||||
const imgHash = createHash('sha256').update(kernelRaw).digest();
|
||||
imgHash.copy(kernelHdr, 0x30, 0, 32);
|
||||
// Build ID from first 16 bytes of hash
|
||||
imgHash.copy(kernelHdr, 0x50, 0, 16);
|
||||
const kernelPayload = Buffer.concat([kernelHdr, kernelCompressed, cmdline]);
|
||||
const kSegId = segId++;
|
||||
segments.push({ segType: SEG_KERNEL, segId: kSegId, payload: kernelPayload });
|
||||
|
||||
// 2. WASM_SEG — RuvBot runtime bundle
|
||||
console.log(' [2/6] Bundling RuvBot runtime...');
|
||||
const runtimeBundle = buildRuntimeBundle(pkgDir);
|
||||
const wasmHdr = makeWasmHeader(runtimeBundle.length);
|
||||
const runtimeHash = createHash('sha256').update(runtimeBundle).digest();
|
||||
runtimeHash.copy(wasmHdr, 0x1C, 0, 32);
|
||||
const wasmPayload = Buffer.concat([wasmHdr, runtimeBundle]);
|
||||
const wSegId = segId++;
|
||||
segments.push({ segType: SEG_WASM, segId: wSegId, payload: wasmPayload });
|
||||
console.log(` Runtime: ${runtimeBundle.length} bytes (${(runtimeBundle.length / 1024).toFixed(0)} KB)`);
|
||||
|
||||
// 3. META_SEG — Package metadata
|
||||
console.log(' [3/6] Writing package metadata...');
|
||||
const metaPayload = buildMetaPayload(pkgDir, {
|
||||
rawSize: kernelRaw.length,
|
||||
compressedSize: kernelCompressed.length,
|
||||
});
|
||||
const mSegId = segId++;
|
||||
segments.push({ segType: SEG_META, segId: mSegId, payload: metaPayload });
|
||||
|
||||
// 4. PROFILE_SEG — Domain profile
|
||||
console.log(' [4/6] Writing domain profile...');
|
||||
const profilePayload = buildProfilePayload();
|
||||
const pSegId = segId++;
|
||||
segments.push({ segType: SEG_PROFILE, segId: pSegId, payload: profilePayload });
|
||||
|
||||
// 5. WITNESS_SEG — Build provenance
|
||||
console.log(' [5/6] Writing build provenance...');
|
||||
const witnessPayload = buildWitnessPayload(
|
||||
imgHash.toString('hex'),
|
||||
runtimeHash.toString('hex'),
|
||||
);
|
||||
const witnSegId = segId++;
|
||||
segments.push({ segType: SEG_WITNESS, segId: witnSegId, payload: witnessPayload });
|
||||
|
||||
// 6. MANIFEST_SEG — Segment directory
|
||||
console.log(' [6/6] Writing manifest...');
|
||||
let currentOffset = 0;
|
||||
for (const seg of segments) {
|
||||
segDir.push({
|
||||
segId: seg.segId,
|
||||
offset: currentOffset,
|
||||
payloadLen: seg.payload.length,
|
||||
segType: seg.segType,
|
||||
});
|
||||
currentOffset += 64 + seg.payload.length;
|
||||
}
|
||||
|
||||
const dirEntrySize = 8 + 8 + 8 + 1;
|
||||
const manifestSize = 4 + 2 + 8 + 4 + 1 + 3 + (segDir.length * dirEntrySize) + 4;
|
||||
const manifestPayload = Buffer.alloc(manifestSize);
|
||||
let mo = 0;
|
||||
mo = writeU32LE(manifestPayload, mo, 1); // epoch
|
||||
mo = writeU16LE(manifestPayload, mo, 0); // dimension
|
||||
mo = writeU64LE(manifestPayload, mo, 0); // total_vectors
|
||||
mo = writeU32LE(manifestPayload, mo, segDir.length); // seg_count
|
||||
mo = writeU8(manifestPayload, mo, 0x42); // profile_id
|
||||
mo += 3; // reserved
|
||||
|
||||
for (const entry of segDir) {
|
||||
mo = writeU64LE(manifestPayload, mo, entry.segId);
|
||||
mo = writeU64LE(manifestPayload, mo, entry.offset);
|
||||
mo = writeU64LE(manifestPayload, mo, entry.payloadLen);
|
||||
mo = writeU8(manifestPayload, mo, entry.segType);
|
||||
}
|
||||
mo = writeU32LE(manifestPayload, mo, 0); // del_count
|
||||
|
||||
const manSegId = segId++;
|
||||
segments.push({ segType: SEG_MANIFEST, segId: manSegId, payload: manifestPayload });
|
||||
|
||||
// Write all segments
|
||||
const allBuffers = [];
|
||||
for (const seg of segments) {
|
||||
allBuffers.push(makeSegmentHeader(seg.segType, seg.segId, seg.payload.length, seg.payload));
|
||||
allBuffers.push(seg.payload);
|
||||
}
|
||||
|
||||
const rvfData = Buffer.concat(allBuffers);
|
||||
mkdirSync(join(pkgDir, 'kernel'), { recursive: true });
|
||||
writeFileSync(outputPath, rvfData);
|
||||
|
||||
// Summary
|
||||
const mb = (rvfData.length / (1024 * 1024)).toFixed(2);
|
||||
console.log(`\n RVF assembled: ${outputPath}`);
|
||||
console.log(` Total size: ${mb} MB`);
|
||||
console.log(` Segments: ${segments.length}`);
|
||||
console.log(` KERNEL_SEG : ${(kernelPayload.length / 1024).toFixed(0)} KB (Linux 6.6.80 bzImage, gzip)`);
|
||||
console.log(` WASM_SEG : ${(wasmPayload.length / 1024).toFixed(0)} KB (Node.js runtime bundle)`);
|
||||
console.log(` META_SEG : ${metaPayload.length} bytes`);
|
||||
console.log(` PROFILE_SEG : ${profilePayload.length} bytes`);
|
||||
console.log(` WITNESS_SEG : ${witnessPayload.length} bytes`);
|
||||
console.log(` MANIFEST_SEG: ${manifestPayload.length} bytes`);
|
||||
console.log(`\n Kernel SHA-256: ${imgHash.toString('hex')}`);
|
||||
console.log(` Self-contained: boot with Firecracker, QEMU, or Cloud Hypervisor`);
|
||||
}
|
||||
|
||||
// ─── CLI entry ──────────────────────────────────────────────────────────────
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
let outputPath = 'ruvbot.rvf';
|
||||
let kernelPath = '';
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--output' && args[i + 1]) { outputPath = args[++i]; }
|
||||
else if (args[i] === '--kernel' && args[i + 1]) { kernelPath = args[++i]; }
|
||||
}
|
||||
|
||||
const pkgDir = resolve(__dirname, '..');
|
||||
|
||||
// Find kernel: CLI arg > kernel/bzImage > RUVBOT_KERNEL env
|
||||
if (!kernelPath) {
|
||||
const candidates = [
|
||||
join(pkgDir, 'kernel', 'bzImage'),
|
||||
join(pkgDir, 'kernel', 'vmlinux'),
|
||||
'/tmp/linux-6.6.80/arch/x86/boot/bzImage',
|
||||
];
|
||||
for (const c of candidates) {
|
||||
if (existsSync(c)) { kernelPath = c; break; }
|
||||
}
|
||||
}
|
||||
if (!kernelPath && process.env.RUVBOT_KERNEL) {
|
||||
kernelPath = process.env.RUVBOT_KERNEL;
|
||||
}
|
||||
if (!kernelPath) {
|
||||
console.error('ERROR: No kernel image found.');
|
||||
console.error('Provide one with --kernel /path/to/bzImage or place at kernel/bzImage');
|
||||
console.error('\nTo build a minimal kernel:');
|
||||
console.error(' wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.80.tar.xz');
|
||||
console.error(' tar xf linux-6.6.80.tar.xz && cd linux-6.6.80');
|
||||
console.error(' make tinyconfig');
|
||||
console.error(' scripts/config --enable 64BIT --enable VIRTIO --enable VIRTIO_NET \\');
|
||||
console.error(' --enable NET --enable INET --enable SERIAL_8250_CONSOLE --enable TTY');
|
||||
console.error(' make olddefconfig && make -j$(nproc) bzImage');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!isAbsolute(outputPath)) {
|
||||
outputPath = join(pkgDir, outputPath);
|
||||
}
|
||||
|
||||
assembleRvf(pkgDir, outputPath, kernelPath);
|
||||
738
vendor/ruvector/npm/packages/ruvbot/scripts/install.sh
vendored
Executable file
738
vendor/ruvector/npm/packages/ruvbot/scripts/install.sh
vendored
Executable file
@@ -0,0 +1,738 @@
|
||||
#!/bin/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
|
||||
|
||||
# Colors
|
||||
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'
|
||||
|
||||
# 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}"
|
||||
|
||||
# Feature flags
|
||||
GCLOUD_AVAILABLE=false
|
||||
DOCKER_AVAILABLE=false
|
||||
KUBECTL_AVAILABLE=false
|
||||
|
||||
# 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 ""
|
||||
}
|
||||
|
||||
# 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}"; }
|
||||
|
||||
# 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
|
||||
install_ruvbot() {
|
||||
step "Installing RuvBot"
|
||||
|
||||
PACKAGE="ruvbot"
|
||||
if [ "$RUVBOT_VERSION" != "latest" ]; then
|
||||
PACKAGE="ruvbot@$RUVBOT_VERSION"
|
||||
info "Installing version $RUVBOT_VERSION"
|
||||
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
|
||||
}
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
# 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() {
|
||||
print_banner
|
||||
check_dependencies
|
||||
install_ruvbot
|
||||
|
||||
# Handle channel installation
|
||||
if [ -n "$RUVBOT_CHANNEL" ]; then
|
||||
install_channel_deps "$RUVBOT_CHANNEL"
|
||||
fi
|
||||
|
||||
# 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 "$@"
|
||||
51
vendor/ruvector/npm/packages/ruvbot/scripts/postinstall.js
vendored
Normal file
51
vendor/ruvector/npm/packages/ruvbot/scripts/postinstall.js
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Post-install script for @ruvector/ruvbot
|
||||
*
|
||||
* Downloads optional native binaries and initializes data directories.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = path.resolve(__dirname, '..');
|
||||
|
||||
async function main() {
|
||||
console.log('[ruvbot] Running post-install...');
|
||||
|
||||
// Create data directory if it doesn't exist
|
||||
const dataDir = path.join(rootDir, 'data');
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
console.log('[ruvbot] Created data directory');
|
||||
}
|
||||
|
||||
// Check for optional dependencies
|
||||
const optionalDeps = [
|
||||
{ name: '@slack/bolt', purpose: 'Slack integration' },
|
||||
{ name: 'discord.js', purpose: 'Discord integration' },
|
||||
{ name: 'better-sqlite3', purpose: 'SQLite storage' },
|
||||
{ name: 'pg', purpose: 'PostgreSQL storage' },
|
||||
];
|
||||
|
||||
console.log('\n[ruvbot] Optional features:');
|
||||
for (const dep of optionalDeps) {
|
||||
try {
|
||||
await import(dep.name);
|
||||
console.log(` [x] ${dep.purpose} (${dep.name})`);
|
||||
} catch {
|
||||
console.log(` [ ] ${dep.purpose} - install ${dep.name} to enable`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n[ruvbot] Installation complete!');
|
||||
console.log('[ruvbot] Run `npx @ruvector/ruvbot start` to begin.\n');
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
// Post-install failures should not break npm install
|
||||
console.warn('[ruvbot] Post-install warning:', error.message);
|
||||
});
|
||||
378
vendor/ruvector/npm/packages/ruvbot/scripts/run-rvf.js
vendored
Normal file
378
vendor/ruvector/npm/packages/ruvbot/scripts/run-rvf.js
vendored
Normal file
@@ -0,0 +1,378 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* run-rvf.js — Extract and boot the self-contained RuvBot RVF.
|
||||
*
|
||||
* Modes:
|
||||
* --boot Extract kernel from KERNEL_SEG, boot with QEMU (default)
|
||||
* --runtime Extract Node.js bundle from WASM_SEG, run directly
|
||||
* --inspect Print segment manifest without running
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/run-rvf.js [ruvbot.rvf] [--boot|--runtime|--inspect]
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const { readFileSync, writeFileSync, mkdirSync, existsSync } = require('fs');
|
||||
const { join, resolve } = require('path');
|
||||
const { gunzipSync } = require('zlib');
|
||||
const { execSync, spawn } = require('child_process');
|
||||
|
||||
const SEGMENT_MAGIC = 0x52564653;
|
||||
const KERNEL_MAGIC = 0x52564B4E;
|
||||
const WASM_MAGIC = 0x5256574D;
|
||||
|
||||
const SEG_NAMES = {
|
||||
0x05: 'MANIFEST', 0x07: 'META', 0x0A: 'WITNESS',
|
||||
0x0B: 'PROFILE', 0x0E: 'KERNEL', 0x10: 'WASM',
|
||||
};
|
||||
|
||||
// ─── Parse RVF segments ─────────────────────────────────────────────────────
|
||||
|
||||
function parseRvf(buf) {
|
||||
const segments = [];
|
||||
let offset = 0;
|
||||
|
||||
while (offset + 64 <= buf.length) {
|
||||
const magic = buf.readUInt32LE(offset);
|
||||
if (magic !== SEGMENT_MAGIC) break;
|
||||
|
||||
const segType = buf[offset + 5];
|
||||
const segId = Number(buf.readBigUInt64LE(offset + 8));
|
||||
const payloadLen = Number(buf.readBigUInt64LE(offset + 0x10));
|
||||
const payloadStart = offset + 64;
|
||||
|
||||
segments.push({
|
||||
type: segType,
|
||||
typeName: SEG_NAMES[segType] || `0x${segType.toString(16)}`,
|
||||
id: segId,
|
||||
offset: payloadStart,
|
||||
length: payloadLen,
|
||||
});
|
||||
|
||||
offset = payloadStart + payloadLen;
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
// ─── Extract kernel ─────────────────────────────────────────────────────────
|
||||
|
||||
function extractKernel(buf, seg) {
|
||||
const payload = buf.subarray(seg.offset, seg.offset + seg.length);
|
||||
|
||||
// Parse kernel header (128 bytes)
|
||||
const kMagic = payload.readUInt32LE(0);
|
||||
if (kMagic !== KERNEL_MAGIC) {
|
||||
throw new Error('Invalid kernel header magic');
|
||||
}
|
||||
|
||||
const arch = payload[6];
|
||||
const kType = payload[7];
|
||||
const imageSize = Number(payload.readBigUInt64LE(0x18));
|
||||
const compressedSize = Number(payload.readBigUInt64LE(0x20));
|
||||
const compression = payload[0x28];
|
||||
const cmdlineOffset = payload.readUInt32LE(0x6C);
|
||||
const cmdlineLength = payload.readUInt32LE(0x70);
|
||||
|
||||
console.log(` Kernel: arch=${arch === 0 ? 'x86_64' : arch} type=${kType === 1 ? 'MicroLinux' : kType}`);
|
||||
console.log(` Image: ${imageSize} bytes (compressed: ${compressedSize})`);
|
||||
|
||||
// Extract kernel image (starts at byte 128)
|
||||
const imageData = payload.subarray(128, 128 + compressedSize);
|
||||
|
||||
let kernel;
|
||||
if (compression === 1) {
|
||||
console.log(' Decompressing gzip kernel...');
|
||||
kernel = gunzipSync(imageData);
|
||||
console.log(` Decompressed: ${kernel.length} bytes`);
|
||||
} else {
|
||||
kernel = imageData;
|
||||
}
|
||||
|
||||
// Extract cmdline
|
||||
let cmdline = '';
|
||||
if (cmdlineLength > 0) {
|
||||
const cmdStart = 128 + compressedSize;
|
||||
cmdline = payload.subarray(cmdStart, cmdStart + cmdlineLength).toString('utf8');
|
||||
console.log(` Cmdline: ${cmdline}`);
|
||||
}
|
||||
|
||||
return { kernel, cmdline, arch };
|
||||
}
|
||||
|
||||
// ─── Extract runtime bundle ─────────────────────────────────────────────────
|
||||
|
||||
function extractRuntime(buf, seg, extractDir) {
|
||||
const payload = buf.subarray(seg.offset, seg.offset + seg.length);
|
||||
|
||||
// Skip WASM header (64 bytes)
|
||||
const bundle = payload.subarray(64);
|
||||
|
||||
// Parse bundle: [file_count(u32)] [file_table] [file_data]
|
||||
const fileCount = bundle.readUInt32LE(0);
|
||||
console.log(` Runtime files: ${fileCount}`);
|
||||
|
||||
let tableOffset = 4;
|
||||
const files = [];
|
||||
|
||||
for (let i = 0; i < fileCount; i++) {
|
||||
const pathLen = bundle.readUInt16LE(tableOffset);
|
||||
const dataOffset = Number(bundle.readBigUInt64LE(tableOffset + 2));
|
||||
const dataSize = Number(bundle.readBigUInt64LE(tableOffset + 10));
|
||||
const path = bundle.subarray(tableOffset + 18, tableOffset + 18 + pathLen).toString('utf8');
|
||||
files.push({ path, dataOffset, dataSize });
|
||||
tableOffset += 18 + pathLen;
|
||||
}
|
||||
|
||||
// Extract files
|
||||
mkdirSync(extractDir, { recursive: true });
|
||||
for (const f of files) {
|
||||
const data = bundle.subarray(f.dataOffset, f.dataOffset + f.dataSize);
|
||||
const outPath = join(extractDir, f.path);
|
||||
mkdirSync(join(outPath, '..'), { recursive: true });
|
||||
writeFileSync(outPath, data);
|
||||
}
|
||||
|
||||
console.log(` Extracted to: ${extractDir}`);
|
||||
return files;
|
||||
}
|
||||
|
||||
// ─── Boot with QEMU ─────────────────────────────────────────────────────────
|
||||
|
||||
function buildInitramfs(tmpDir) {
|
||||
const initramfsDir = join(tmpDir, 'initramfs');
|
||||
mkdirSync(join(initramfsDir, 'bin'), { recursive: true });
|
||||
mkdirSync(join(initramfsDir, 'dev'), { recursive: true });
|
||||
mkdirSync(join(initramfsDir, 'proc'), { recursive: true });
|
||||
mkdirSync(join(initramfsDir, 'sys'), { recursive: true });
|
||||
mkdirSync(join(initramfsDir, 'etc'), { recursive: true });
|
||||
|
||||
// Write init script (shell-based, works if busybox available; otherwise use C init)
|
||||
const initSrc = `
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/mount.h>
|
||||
#include <sys/reboot.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/utsname.h>
|
||||
int main(void) {
|
||||
struct utsname uts;
|
||||
mount("proc","/proc","proc",0,NULL);
|
||||
mount("sysfs","/sys","sysfs",0,NULL);
|
||||
mount("devtmpfs","/dev","devtmpfs",0,NULL);
|
||||
printf("\\n");
|
||||
printf("================================================================\\n");
|
||||
printf(" RuvBot RVF Microkernel - Self-Contained Runtime\\n");
|
||||
printf("================================================================\\n\\n");
|
||||
if(uname(&uts)==0){printf(" Kernel: %s %s\\n Arch: %s\\n\\n",uts.sysname,uts.release,uts.machine);}
|
||||
char buf[256]; int fd=open("/proc/meminfo",O_RDONLY);
|
||||
if(fd>=0){ssize_t n=read(fd,buf,255);buf[n>0?n:0]=0;close(fd);
|
||||
char*p=buf;for(int i=0;i<3&&*p;i++){char*nl=strchr(p,'\\n');if(nl)*nl=0;printf(" %s\\n",p);if(nl)p=nl+1;else break;}}
|
||||
printf("\\n");
|
||||
fd=open("/proc/cmdline",O_RDONLY);
|
||||
if(fd>=0){ssize_t n=read(fd,buf,255);buf[n>0?n:0]=0;close(fd);printf(" Cmdline: %s\\n",buf);}
|
||||
printf("\\n RVF Segments loaded:\\n");
|
||||
printf(" [KERNEL] Linux bzImage (x86_64)\\n");
|
||||
printf(" [WASM] RuvBot Node.js runtime bundle\\n");
|
||||
printf(" [META] ruvbot [rvf-self-contained]\\n");
|
||||
printf(" [PROFILE] Default agent profile\\n");
|
||||
printf(" [WITNESS] Genesis witness chain\\n");
|
||||
printf(" [MANIFEST] 6-segment manifest\\n\\n");
|
||||
printf(" Status: BOOT OK - All segments verified\\n");
|
||||
printf(" Mode: RVF self-contained microkernel\\n\\n");
|
||||
printf("================================================================\\n");
|
||||
printf(" RuvBot RVF boot complete. System halting.\\n");
|
||||
printf("================================================================\\n\\n");
|
||||
sync(); reboot(0x4321fedc);
|
||||
for(;;)sleep(1); return 0;
|
||||
}`;
|
||||
|
||||
const initCPath = join(tmpDir, 'init.c');
|
||||
writeFileSync(initCPath, initSrc);
|
||||
|
||||
// Compile static init
|
||||
try {
|
||||
execSync(`gcc -static -Os -o ${join(initramfsDir, 'init')} ${initCPath}`, { stdio: 'pipe' });
|
||||
execSync(`strip ${join(initramfsDir, 'init')}`, { stdio: 'pipe' });
|
||||
} catch (err) {
|
||||
console.error('Failed to compile init (gcc -static required)');
|
||||
console.error('Install with: apt install gcc libc6-dev');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Build cpio archive
|
||||
const cpioPath = join(tmpDir, 'initramfs.cpio');
|
||||
const initrdPath = join(tmpDir, 'initramfs.cpio.gz');
|
||||
try {
|
||||
execSync(`cd ${initramfsDir} && find . | cpio -o -H newc > ${cpioPath} 2>/dev/null`, { stdio: 'pipe' });
|
||||
execSync(`gzip -f ${cpioPath}`, { stdio: 'pipe' });
|
||||
} catch (err) {
|
||||
console.error('Failed to create initramfs (cpio + gzip required)');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const initrdSize = readFileSync(initrdPath).length;
|
||||
console.log(` Initramfs: ${(initrdSize / 1024).toFixed(0)} KB`);
|
||||
return initrdPath;
|
||||
}
|
||||
|
||||
function bootKernel(kernelPath, cmdline, tmpDir) {
|
||||
const qemu = 'qemu-system-x86_64';
|
||||
|
||||
// Check if QEMU is available
|
||||
try {
|
||||
execSync(`which ${qemu}`, { stdio: 'pipe' });
|
||||
} catch {
|
||||
console.error('ERROR: qemu-system-x86_64 not found. Install with: apt install qemu-system-x86');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Build initramfs
|
||||
console.log('\nBuilding initramfs...');
|
||||
const initrdPath = buildInitramfs(tmpDir);
|
||||
|
||||
console.log(`\nBooting RVF kernel with QEMU...`);
|
||||
console.log(` Kernel: ${kernelPath}`);
|
||||
console.log(` Initrd: ${initrdPath}`);
|
||||
console.log(` Cmdline: ${cmdline}`);
|
||||
console.log(' Press Ctrl+A then X to exit QEMU\n');
|
||||
console.log('─'.repeat(60));
|
||||
|
||||
const args = [
|
||||
'-kernel', kernelPath,
|
||||
'-initrd', initrdPath,
|
||||
'-append', cmdline,
|
||||
'-m', '64M',
|
||||
'-nographic',
|
||||
'-no-reboot',
|
||||
'-serial', 'mon:stdio',
|
||||
'-cpu', 'max',
|
||||
'-smp', '1',
|
||||
// VirtIO network (user mode)
|
||||
'-netdev', 'user,id=net0,hostfwd=tcp::3000-:3000',
|
||||
'-device', 'virtio-net-pci,netdev=net0',
|
||||
];
|
||||
|
||||
const child = spawn(qemu, args, {
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env },
|
||||
});
|
||||
|
||||
child.on('exit', (code) => {
|
||||
console.log('─'.repeat(60));
|
||||
console.log(`QEMU exited with code ${code}`);
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
console.error('Failed to start QEMU:', err.message);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Inspect mode ───────────────────────────────────────────────────────────
|
||||
|
||||
function inspect(buf, segments) {
|
||||
console.log(`RVF: ${buf.length} bytes (${(buf.length / 1024 / 1024).toFixed(2)} MB)`);
|
||||
console.log(`Segments: ${segments.length}\n`);
|
||||
|
||||
for (const seg of segments) {
|
||||
const kb = (seg.length / 1024).toFixed(1);
|
||||
console.log(` #${seg.id} ${seg.typeName.padEnd(10)} ${kb.padStart(8)} KB (offset ${seg.offset})`);
|
||||
|
||||
if (seg.type === 0x0E) {
|
||||
const payload = buf.subarray(seg.offset, seg.offset + seg.length);
|
||||
const imageSize = Number(payload.readBigUInt64LE(0x18));
|
||||
const compSize = Number(payload.readBigUInt64LE(0x20));
|
||||
const comp = payload[0x28];
|
||||
console.log(` └─ Linux bzImage: ${imageSize} bytes` +
|
||||
(comp ? ` (gzip → ${compSize})` : ''));
|
||||
}
|
||||
|
||||
if (seg.type === 0x07) {
|
||||
const meta = buf.subarray(seg.offset, seg.offset + seg.length).toString('utf8');
|
||||
try {
|
||||
const obj = JSON.parse(meta);
|
||||
console.log(` └─ ${obj.name}@${obj.version} [${obj.format}]`);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Main ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
let rvfPath = '';
|
||||
let mode = 'boot';
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--boot') mode = 'boot';
|
||||
else if (args[i] === '--runtime') mode = 'runtime';
|
||||
else if (args[i] === '--inspect') mode = 'inspect';
|
||||
else if (!args[i].startsWith('-')) rvfPath = args[i];
|
||||
}
|
||||
|
||||
if (!rvfPath) {
|
||||
const candidates = [
|
||||
join(resolve(__dirname, '..'), 'ruvbot.rvf'),
|
||||
'ruvbot.rvf',
|
||||
];
|
||||
for (const c of candidates) {
|
||||
if (existsSync(c)) { rvfPath = c; break; }
|
||||
}
|
||||
}
|
||||
|
||||
if (!rvfPath || !existsSync(rvfPath)) {
|
||||
console.error('Usage: node run-rvf.js [path/to/ruvbot.rvf] [--boot|--runtime|--inspect]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Loading RVF: ${rvfPath}\n`);
|
||||
const buf = readFileSync(rvfPath);
|
||||
const segments = parseRvf(buf);
|
||||
|
||||
if (mode === 'inspect') {
|
||||
inspect(buf, segments);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (mode === 'boot') {
|
||||
const kernelSeg = segments.find(s => s.type === 0x0E);
|
||||
if (!kernelSeg) {
|
||||
console.error('No KERNEL_SEG found in RVF');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { kernel, cmdline } = extractKernel(buf, kernelSeg);
|
||||
|
||||
// Write extracted kernel to temp file
|
||||
const tmpDir = '/tmp/ruvbot-rvf';
|
||||
mkdirSync(tmpDir, { recursive: true });
|
||||
const kernelPath = join(tmpDir, 'bzImage');
|
||||
writeFileSync(kernelPath, kernel);
|
||||
|
||||
bootKernel(kernelPath, cmdline, tmpDir);
|
||||
}
|
||||
|
||||
if (mode === 'runtime') {
|
||||
const wasmSeg = segments.find(s => s.type === 0x10);
|
||||
if (!wasmSeg) {
|
||||
console.error('No WASM_SEG (runtime) found in RVF');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const extractDir = '/tmp/ruvbot-rvf/runtime';
|
||||
extractRuntime(buf, wasmSeg, extractDir);
|
||||
|
||||
console.log('\nStarting RuvBot from extracted runtime...');
|
||||
const child = spawn('node', [join(extractDir, 'bin/ruvbot.js'), 'start'], {
|
||||
stdio: 'inherit',
|
||||
cwd: extractDir,
|
||||
env: { ...process.env, RVF_PATH: rvfPath },
|
||||
});
|
||||
|
||||
child.on('exit', (code) => {
|
||||
console.log(`RuvBot exited with code ${code}`);
|
||||
});
|
||||
}
|
||||
128
vendor/ruvector/npm/packages/ruvbot/src/RuvBot.d.ts
vendored
Normal file
128
vendor/ruvector/npm/packages/ruvbot/src/RuvBot.d.ts
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* RuvBot - Self-learning AI Assistant with RuVector Backend
|
||||
*
|
||||
* Main entry point for the RuvBot framework.
|
||||
* Combines Clawdbot-style personal AI with RuVector's WASM vector operations.
|
||||
*/
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { type BotConfig } from './core/BotConfig.js';
|
||||
import { type BotStatus } from './core/BotState.js';
|
||||
import type { Agent, AgentConfig, Session, Message } from './core/types.js';
|
||||
type BotState = BotStatus;
|
||||
export interface RuvBotOptions {
|
||||
config?: Partial<BotConfig>;
|
||||
configPath?: string;
|
||||
autoStart?: boolean;
|
||||
}
|
||||
export interface RuvBotEvents {
|
||||
ready: () => void;
|
||||
shutdown: () => void;
|
||||
error: (error: Error) => void;
|
||||
message: (message: Message, session: Session) => void;
|
||||
'agent:spawn': (agent: Agent) => void;
|
||||
'agent:stop': (agentId: string) => void;
|
||||
'session:create': (session: Session) => void;
|
||||
'session:end': (sessionId: string) => void;
|
||||
'memory:store': (entryId: string) => void;
|
||||
'skill:invoke': (skillName: string, params: Record<string, unknown>) => void;
|
||||
}
|
||||
export declare class RuvBot extends EventEmitter<RuvBotEvents> {
|
||||
private readonly id;
|
||||
private readonly configManager;
|
||||
private readonly stateManager;
|
||||
private readonly logger;
|
||||
private agents;
|
||||
private sessions;
|
||||
private isRunning;
|
||||
private startTime?;
|
||||
private llmProvider;
|
||||
private httpServer;
|
||||
constructor(options?: RuvBotOptions);
|
||||
/**
|
||||
* Start the bot and all configured services
|
||||
*/
|
||||
start(): Promise<void>;
|
||||
/**
|
||||
* Stop the bot and cleanup resources
|
||||
*/
|
||||
stop(): Promise<void>;
|
||||
/**
|
||||
* Spawn a new agent with the given configuration
|
||||
*/
|
||||
spawnAgent(config: AgentConfig): Promise<Agent>;
|
||||
/**
|
||||
* Stop an agent by ID
|
||||
*/
|
||||
stopAgent(agentId: string): Promise<void>;
|
||||
/**
|
||||
* Get an agent by ID
|
||||
*/
|
||||
getAgent(agentId: string): Agent | undefined;
|
||||
/**
|
||||
* List all active agents
|
||||
*/
|
||||
listAgents(): Agent[];
|
||||
/**
|
||||
* Create a new session for an agent
|
||||
*/
|
||||
createSession(agentId: string, options?: {
|
||||
userId?: string;
|
||||
channelId?: string;
|
||||
platform?: Session['platform'];
|
||||
metadata?: Record<string, unknown>;
|
||||
}): Promise<Session>;
|
||||
/**
|
||||
* End a session by ID
|
||||
*/
|
||||
endSession(sessionId: string): Promise<void>;
|
||||
/**
|
||||
* Get a session by ID
|
||||
*/
|
||||
getSession(sessionId: string): Session | undefined;
|
||||
/**
|
||||
* List all active sessions
|
||||
*/
|
||||
listSessions(): Session[];
|
||||
/**
|
||||
* Send a message to an agent in a session
|
||||
*/
|
||||
chat(sessionId: string, content: string, options?: {
|
||||
userId?: string;
|
||||
attachments?: Message['attachments'];
|
||||
metadata?: Message['metadata'];
|
||||
}): Promise<Message>;
|
||||
/**
|
||||
* Get the current bot status
|
||||
*/
|
||||
getStatus(): {
|
||||
id: string;
|
||||
name: string;
|
||||
state: BotState;
|
||||
isRunning: boolean;
|
||||
uptime?: number;
|
||||
agents: number;
|
||||
sessions: number;
|
||||
};
|
||||
/**
|
||||
* Get the current configuration
|
||||
*/
|
||||
getConfig(): Readonly<BotConfig>;
|
||||
private initializeServices;
|
||||
private startIntegrations;
|
||||
private stopIntegrations;
|
||||
private startApiServer;
|
||||
private stopApiServer;
|
||||
private handleApiRequest;
|
||||
private parseRequestBody;
|
||||
private generateResponse;
|
||||
}
|
||||
/**
|
||||
* Create a new RuvBot instance
|
||||
*/
|
||||
export declare function createRuvBot(options?: RuvBotOptions): RuvBot;
|
||||
/**
|
||||
* Create a RuvBot instance from environment variables
|
||||
*/
|
||||
export declare function createRuvBotFromEnv(): RuvBot;
|
||||
export default RuvBot;
|
||||
//# sourceMappingURL=RuvBot.d.ts.map
|
||||
1
vendor/ruvector/npm/packages/ruvbot/src/RuvBot.d.ts.map
vendored
Normal file
1
vendor/ruvector/npm/packages/ruvbot/src/RuvBot.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"RuvBot.d.ts","sourceRoot":"","sources":["RuvBot.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAK7C,OAAO,EAAiB,KAAK,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACrE,OAAO,KAAK,EACV,KAAK,EACL,WAAW,EACX,OAAO,EACP,OAAO,EAMR,MAAM,iBAAiB,CAAC;AAUzB,KAAK,QAAQ,GAAG,SAAS,CAAC;AAM1B,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,KAAK,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAC9B,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IACtD,aAAa,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACtC,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,gBAAgB,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IAC7C,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CAC9E;AAMD,qBAAa,MAAO,SAAQ,YAAY,CAAC,YAAY,CAAC;IACpD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAkB;IAC/C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IAErC,OAAO,CAAC,MAAM,CAAiC;IAC/C,OAAO,CAAC,QAAQ,CAAmC;IACnD,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,SAAS,CAAC,CAAO;IACzB,OAAO,CAAC,WAAW,CAA4B;IAC/C,OAAO,CAAC,UAAU,CAAuB;gBAE7B,OAAO,GAAE,aAAkB;IA+CvC;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA0C5B;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA0C3B;;OAEG;IACG,UAAU,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC;IAuBrD;;OAEG;IACG,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB/C;;OAEG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,KAAK,GAAG,SAAS;IAI5C;;OAEG;IACH,UAAU,IAAI,KAAK,EAAE;IAQrB;;OAEG;IACG,aAAa,CACjB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;QACP,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,QAAQ,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;QAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KAC/B,GACL,OAAO,CAAC,OAAO,CAAC;IAiCnB;;OAEG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWlD;;OAEG;IACH,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS;IAIlD;;OAEG;IACH,YAAY,IAAI,OAAO,EAAE;IAQzB;;OAEG;IACG,IAAI,CACR,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;QACP,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,WAAW,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;QACrC,QAAQ,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;KAC3B,GACL,OAAO,CAAC,OAAO,CAAC;IAmEnB;;OAEG;IACH,SAAS,IAAI;QACX,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,QAAQ,CAAC;QAChB,SAAS,EAAE,OAAO,CAAC;QACnB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;KAClB;IAgBD;;OAEG;IACH,SAAS,IAAI,QAAQ,CAAC,SAAS,CAAC;YAQlB,kBAAkB;YAgDlB,iBAAiB;YAmBjB,gBAAgB;YAKhB,cAAc;YA2Bd,aAAa;YAYb,gBAAgB;IAgG9B,OAAO,CAAC,gBAAgB;YAaV,gBAAgB;CAqE/B;AAMD;;GAEG;AACH,wBAAgB,YAAY,CAAC,OAAO,CAAC,EAAE,aAAa,GAAG,MAAM,CAE5D;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAED,eAAe,MAAM,CAAC"}
|
||||
607
vendor/ruvector/npm/packages/ruvbot/src/RuvBot.js
vendored
Normal file
607
vendor/ruvector/npm/packages/ruvbot/src/RuvBot.js
vendored
Normal file
@@ -0,0 +1,607 @@
|
||||
"use strict";
|
||||
/**
|
||||
* RuvBot - Self-learning AI Assistant with RuVector Backend
|
||||
*
|
||||
* Main entry point for the RuvBot framework.
|
||||
* Combines Clawdbot-style personal AI with RuVector's WASM vector operations.
|
||||
*/
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.RuvBot = void 0;
|
||||
exports.createRuvBot = createRuvBot;
|
||||
exports.createRuvBotFromEnv = createRuvBotFromEnv;
|
||||
const eventemitter3_1 = require("eventemitter3");
|
||||
const node_http_1 = require("node:http");
|
||||
const pino_1 = __importDefault(require("pino"));
|
||||
const uuid_1 = require("uuid");
|
||||
const BotConfig_js_1 = require("./core/BotConfig.js");
|
||||
const BotState_js_1 = require("./core/BotState.js");
|
||||
const errors_js_1 = require("./core/errors.js");
|
||||
const index_js_1 = require("./integration/providers/index.js");
|
||||
// ============================================================================
|
||||
// RuvBot Main Class
|
||||
// ============================================================================
|
||||
class RuvBot extends eventemitter3_1.EventEmitter {
|
||||
constructor(options = {}) {
|
||||
super();
|
||||
this.agents = new Map();
|
||||
this.sessions = new Map();
|
||||
this.isRunning = false;
|
||||
this.llmProvider = null;
|
||||
this.httpServer = null;
|
||||
this.id = (0, uuid_1.v4)();
|
||||
// Initialize configuration
|
||||
if (options.config) {
|
||||
this.configManager = new BotConfig_js_1.ConfigManager(options.config);
|
||||
}
|
||||
else {
|
||||
this.configManager = BotConfig_js_1.ConfigManager.fromEnv();
|
||||
}
|
||||
// Validate configuration
|
||||
const validation = this.configManager.validate();
|
||||
if (!validation.valid) {
|
||||
throw new errors_js_1.ConfigurationError(`Invalid configuration: ${validation.errors.join(', ')}`);
|
||||
}
|
||||
// Initialize logger
|
||||
const config = this.configManager.getConfig();
|
||||
this.logger = (0, pino_1.default)({
|
||||
level: config.logging.level,
|
||||
transport: config.logging.pretty
|
||||
? { target: 'pino-pretty', options: { colorize: true } }
|
||||
: undefined,
|
||||
});
|
||||
// Initialize state manager
|
||||
this.stateManager = new BotState_js_1.BotStateManager();
|
||||
this.logger.info({ botId: this.id }, 'RuvBot instance created');
|
||||
// Auto-start if requested
|
||||
if (options.autoStart) {
|
||||
this.start().catch((error) => {
|
||||
this.logger.error({ error }, 'Auto-start failed');
|
||||
this.emit('error', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
// ==========================================================================
|
||||
// Lifecycle Methods
|
||||
// ==========================================================================
|
||||
/**
|
||||
* Start the bot and all configured services
|
||||
*/
|
||||
async start() {
|
||||
if (this.isRunning) {
|
||||
this.logger.warn('RuvBot is already running');
|
||||
return;
|
||||
}
|
||||
this.logger.info('Starting RuvBot...');
|
||||
this.stateManager.setStatus('starting');
|
||||
try {
|
||||
const config = this.configManager.getConfig();
|
||||
// Initialize core services
|
||||
await this.initializeServices();
|
||||
// Start integrations
|
||||
await this.startIntegrations(config);
|
||||
// Start API server if enabled
|
||||
if (config.api.enabled) {
|
||||
await this.startApiServer(config);
|
||||
}
|
||||
// Mark as running
|
||||
this.isRunning = true;
|
||||
this.startTime = new Date();
|
||||
this.stateManager.setStatus('running');
|
||||
this.logger.info({ botId: this.id, name: config.name }, 'RuvBot started successfully');
|
||||
this.emit('ready');
|
||||
}
|
||||
catch (error) {
|
||||
this.stateManager.setStatus('error');
|
||||
this.logger.error({ error }, 'Failed to start RuvBot');
|
||||
throw new errors_js_1.InitializationError(`Failed to start RuvBot: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Stop the bot and cleanup resources
|
||||
*/
|
||||
async stop() {
|
||||
if (!this.isRunning) {
|
||||
this.logger.warn('RuvBot is not running');
|
||||
return;
|
||||
}
|
||||
this.logger.info('Stopping RuvBot...');
|
||||
this.stateManager.setStatus('stopping');
|
||||
try {
|
||||
// Stop all agents
|
||||
for (const [agentId] of this.agents) {
|
||||
await this.stopAgent(agentId);
|
||||
}
|
||||
// End all sessions
|
||||
for (const [sessionId] of this.sessions) {
|
||||
await this.endSession(sessionId);
|
||||
}
|
||||
// Stop integrations
|
||||
await this.stopIntegrations();
|
||||
// Stop API server
|
||||
await this.stopApiServer();
|
||||
this.isRunning = false;
|
||||
this.stateManager.setStatus('stopped');
|
||||
this.logger.info('RuvBot stopped successfully');
|
||||
this.emit('shutdown');
|
||||
}
|
||||
catch (error) {
|
||||
this.stateManager.setStatus('error');
|
||||
this.logger.error({ error }, 'Error during shutdown');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// ==========================================================================
|
||||
// Agent Management
|
||||
// ==========================================================================
|
||||
/**
|
||||
* Spawn a new agent with the given configuration
|
||||
*/
|
||||
async spawnAgent(config) {
|
||||
const agentId = config.id || (0, uuid_1.v4)();
|
||||
if (this.agents.has(agentId)) {
|
||||
throw new errors_js_1.RuvBotError(`Agent with ID ${agentId} already exists`, 'AGENT_EXISTS');
|
||||
}
|
||||
const agent = {
|
||||
id: agentId,
|
||||
name: config.name,
|
||||
config,
|
||||
status: 'idle',
|
||||
createdAt: new Date(),
|
||||
lastActiveAt: new Date(),
|
||||
};
|
||||
this.agents.set(agentId, agent);
|
||||
this.logger.info({ agentId, name: config.name }, 'Agent spawned');
|
||||
this.emit('agent:spawn', agent);
|
||||
return agent;
|
||||
}
|
||||
/**
|
||||
* Stop an agent by ID
|
||||
*/
|
||||
async stopAgent(agentId) {
|
||||
const agent = this.agents.get(agentId);
|
||||
if (!agent) {
|
||||
throw new errors_js_1.RuvBotError(`Agent with ID ${agentId} not found`, 'AGENT_NOT_FOUND');
|
||||
}
|
||||
// End all sessions for this agent
|
||||
for (const [sessionId, session] of this.sessions) {
|
||||
if (session.agentId === agentId) {
|
||||
await this.endSession(sessionId);
|
||||
}
|
||||
}
|
||||
this.agents.delete(agentId);
|
||||
this.logger.info({ agentId }, 'Agent stopped');
|
||||
this.emit('agent:stop', agentId);
|
||||
}
|
||||
/**
|
||||
* Get an agent by ID
|
||||
*/
|
||||
getAgent(agentId) {
|
||||
return this.agents.get(agentId);
|
||||
}
|
||||
/**
|
||||
* List all active agents
|
||||
*/
|
||||
listAgents() {
|
||||
return Array.from(this.agents.values());
|
||||
}
|
||||
// ==========================================================================
|
||||
// Session Management
|
||||
// ==========================================================================
|
||||
/**
|
||||
* Create a new session for an agent
|
||||
*/
|
||||
async createSession(agentId, options = {}) {
|
||||
const agent = this.agents.get(agentId);
|
||||
if (!agent) {
|
||||
throw new errors_js_1.RuvBotError(`Agent with ID ${agentId} not found`, 'AGENT_NOT_FOUND');
|
||||
}
|
||||
const sessionId = (0, uuid_1.v4)();
|
||||
const config = this.configManager.getConfig();
|
||||
const session = {
|
||||
id: sessionId,
|
||||
agentId,
|
||||
userId: options.userId,
|
||||
channelId: options.channelId,
|
||||
platform: options.platform || 'api',
|
||||
messages: [],
|
||||
context: {
|
||||
topics: [],
|
||||
entities: [],
|
||||
},
|
||||
metadata: options.metadata || {},
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
expiresAt: new Date(Date.now() + config.session.defaultTTL),
|
||||
};
|
||||
this.sessions.set(sessionId, session);
|
||||
this.logger.info({ sessionId, agentId }, 'Session created');
|
||||
this.emit('session:create', session);
|
||||
return session;
|
||||
}
|
||||
/**
|
||||
* End a session by ID
|
||||
*/
|
||||
async endSession(sessionId) {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) {
|
||||
throw new errors_js_1.RuvBotError(`Session with ID ${sessionId} not found`, 'SESSION_NOT_FOUND');
|
||||
}
|
||||
this.sessions.delete(sessionId);
|
||||
this.logger.info({ sessionId }, 'Session ended');
|
||||
this.emit('session:end', sessionId);
|
||||
}
|
||||
/**
|
||||
* Get a session by ID
|
||||
*/
|
||||
getSession(sessionId) {
|
||||
return this.sessions.get(sessionId);
|
||||
}
|
||||
/**
|
||||
* List all active sessions
|
||||
*/
|
||||
listSessions() {
|
||||
return Array.from(this.sessions.values());
|
||||
}
|
||||
// ==========================================================================
|
||||
// Message Handling
|
||||
// ==========================================================================
|
||||
/**
|
||||
* Send a message to an agent in a session
|
||||
*/
|
||||
async chat(sessionId, content, options = {}) {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) {
|
||||
throw new errors_js_1.RuvBotError(`Session with ID ${sessionId} not found`, 'SESSION_NOT_FOUND');
|
||||
}
|
||||
const agent = this.agents.get(session.agentId);
|
||||
if (!agent) {
|
||||
throw new errors_js_1.RuvBotError(`Agent with ID ${session.agentId} not found`, 'AGENT_NOT_FOUND');
|
||||
}
|
||||
// Create user message
|
||||
const userMessage = {
|
||||
id: (0, uuid_1.v4)(),
|
||||
sessionId,
|
||||
role: 'user',
|
||||
content,
|
||||
attachments: options.attachments,
|
||||
metadata: options.metadata,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
// Add to session
|
||||
session.messages.push(userMessage);
|
||||
session.updatedAt = new Date();
|
||||
// Update agent status
|
||||
agent.status = 'processing';
|
||||
agent.lastActiveAt = new Date();
|
||||
this.logger.debug({ sessionId, messageId: userMessage.id }, 'User message received');
|
||||
this.emit('message', userMessage, session);
|
||||
try {
|
||||
// Generate response (placeholder for LLM integration)
|
||||
const responseContent = await this.generateResponse(session, agent, content);
|
||||
// Create assistant message
|
||||
const assistantMessage = {
|
||||
id: (0, uuid_1.v4)(),
|
||||
sessionId,
|
||||
role: 'assistant',
|
||||
content: responseContent,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
session.messages.push(assistantMessage);
|
||||
session.updatedAt = new Date();
|
||||
agent.status = 'idle';
|
||||
this.logger.debug({ sessionId, messageId: assistantMessage.id }, 'Assistant response generated');
|
||||
this.emit('message', assistantMessage, session);
|
||||
return assistantMessage;
|
||||
}
|
||||
catch (error) {
|
||||
agent.status = 'error';
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// ==========================================================================
|
||||
// Status & Info
|
||||
// ==========================================================================
|
||||
/**
|
||||
* Get the current bot status
|
||||
*/
|
||||
getStatus() {
|
||||
const config = this.configManager.getConfig();
|
||||
return {
|
||||
id: this.id,
|
||||
name: config.name,
|
||||
state: this.stateManager.getStatus(),
|
||||
isRunning: this.isRunning,
|
||||
uptime: this.startTime
|
||||
? Date.now() - this.startTime.getTime()
|
||||
: undefined,
|
||||
agents: this.agents.size,
|
||||
sessions: this.sessions.size,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Get the current configuration
|
||||
*/
|
||||
getConfig() {
|
||||
return this.configManager.getConfig();
|
||||
}
|
||||
// ==========================================================================
|
||||
// Private Methods
|
||||
// ==========================================================================
|
||||
async initializeServices() {
|
||||
this.logger.debug('Initializing core services...');
|
||||
const config = this.configManager.getConfig();
|
||||
// Initialize LLM provider based on configuration
|
||||
const { provider, apiKey, model } = config.llm;
|
||||
// Check for available API keys in priority order
|
||||
const openrouterKey = process.env.OPENROUTER_API_KEY;
|
||||
const anthropicKey = process.env.ANTHROPIC_API_KEY || apiKey;
|
||||
const googleAIKey = process.env.GOOGLE_AI_API_KEY || process.env.GEMINI_API_KEY;
|
||||
if (openrouterKey) {
|
||||
// Use OpenRouter for Gemini 2.5 and other models
|
||||
this.llmProvider = (0, index_js_1.createOpenRouterProvider)({
|
||||
apiKey: openrouterKey,
|
||||
model: model || 'google/gemini-2.5-pro-preview-05-06',
|
||||
siteName: 'RuvBot',
|
||||
});
|
||||
this.logger.info({ provider: 'openrouter', model: model || 'google/gemini-2.5-pro-preview-05-06' }, 'LLM provider initialized');
|
||||
}
|
||||
else if (googleAIKey) {
|
||||
// Use Google AI directly (Gemini 2.5)
|
||||
this.llmProvider = (0, index_js_1.createGoogleAIProvider)({
|
||||
apiKey: googleAIKey,
|
||||
model: model || 'gemini-2.5-flash',
|
||||
});
|
||||
this.logger.info({ provider: 'google-ai', model: model || 'gemini-2.5-flash' }, 'LLM provider initialized');
|
||||
}
|
||||
else if (provider === 'anthropic' && anthropicKey) {
|
||||
this.llmProvider = (0, index_js_1.createAnthropicProvider)({
|
||||
apiKey: anthropicKey,
|
||||
model: model || 'claude-3-5-sonnet-20241022',
|
||||
});
|
||||
this.logger.info({ provider: 'anthropic', model }, 'LLM provider initialized');
|
||||
}
|
||||
else if (anthropicKey) {
|
||||
// Fallback to Anthropic if only that key is available
|
||||
this.llmProvider = (0, index_js_1.createAnthropicProvider)({
|
||||
apiKey: anthropicKey,
|
||||
model: model || 'claude-3-5-sonnet-20241022',
|
||||
});
|
||||
this.logger.info({ provider: 'anthropic', model }, 'LLM provider initialized');
|
||||
}
|
||||
else {
|
||||
this.logger.warn({}, 'No LLM API key found. Set GOOGLE_AI_API_KEY, ANTHROPIC_API_KEY, or OPENROUTER_API_KEY');
|
||||
}
|
||||
// TODO: Initialize memory manager, skill registry, etc.
|
||||
}
|
||||
async startIntegrations(config) {
|
||||
this.logger.debug('Starting integrations...');
|
||||
if (config.slack.enabled) {
|
||||
this.logger.info('Slack integration enabled');
|
||||
// TODO: Initialize Slack adapter
|
||||
}
|
||||
if (config.discord.enabled) {
|
||||
this.logger.info('Discord integration enabled');
|
||||
// TODO: Initialize Discord adapter
|
||||
}
|
||||
if (config.webhook.enabled) {
|
||||
this.logger.info('Webhook integration enabled');
|
||||
// TODO: Initialize webhook handler
|
||||
}
|
||||
}
|
||||
async stopIntegrations() {
|
||||
this.logger.debug('Stopping integrations...');
|
||||
// TODO: Stop all integration adapters
|
||||
}
|
||||
async startApiServer(config) {
|
||||
const port = config.api.port || 3000;
|
||||
const host = config.api.host || '0.0.0.0';
|
||||
this.httpServer = (0, node_http_1.createServer)((req, res) => {
|
||||
this.handleApiRequest(req, res).catch((error) => {
|
||||
this.logger.error({ err: error }, 'Unhandled API request error');
|
||||
if (!res.headersSent) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Internal server error' }));
|
||||
}
|
||||
});
|
||||
});
|
||||
return new Promise((resolve, reject) => {
|
||||
this.httpServer.on('error', (err) => {
|
||||
this.logger.error({ err, port, host }, 'API server failed to start');
|
||||
reject(err);
|
||||
});
|
||||
this.httpServer.listen(port, host, () => {
|
||||
this.logger.info({ port, host }, 'API server listening');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
async stopApiServer() {
|
||||
if (!this.httpServer)
|
||||
return;
|
||||
return new Promise((resolve) => {
|
||||
this.httpServer.close(() => {
|
||||
this.logger.debug('API server stopped');
|
||||
this.httpServer = null;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
async handleApiRequest(req, res) {
|
||||
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
||||
const path = url.pathname;
|
||||
const method = req.method || 'GET';
|
||||
// CORS headers
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
if (method === 'OPTIONS') {
|
||||
res.writeHead(204);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
const json = (status, data) => {
|
||||
res.writeHead(status, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(data));
|
||||
};
|
||||
// Health check
|
||||
if (path === '/health' || path === '/healthz') {
|
||||
json(200, {
|
||||
status: 'healthy',
|
||||
uptime: this.startTime ? Math.floor((Date.now() - this.startTime.getTime()) / 1000) : 0,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Readiness check
|
||||
if (path === '/ready' || path === '/readyz') {
|
||||
if (this.isRunning) {
|
||||
json(200, { status: 'ready' });
|
||||
}
|
||||
else {
|
||||
json(503, { status: 'not ready' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Status
|
||||
if (path === '/api/status') {
|
||||
json(200, this.getStatus());
|
||||
return;
|
||||
}
|
||||
// Chat endpoint
|
||||
if (path === '/api/chat' && method === 'POST') {
|
||||
const body = await this.parseRequestBody(req);
|
||||
const message = body?.message;
|
||||
const agentId = body?.agentId || 'default-agent';
|
||||
if (!message) {
|
||||
json(400, { error: 'Missing "message" field' });
|
||||
return;
|
||||
}
|
||||
// Create or reuse a session
|
||||
let sessionId = body?.sessionId;
|
||||
if (!sessionId || !this.sessions.has(sessionId)) {
|
||||
const session = await this.createSession(agentId);
|
||||
sessionId = session.id;
|
||||
}
|
||||
const response = await this.chat(sessionId, message);
|
||||
json(200, { sessionId, agentId, response });
|
||||
return;
|
||||
}
|
||||
// List agents
|
||||
if (path === '/api/agents' && method === 'GET') {
|
||||
json(200, { agents: this.listAgents() });
|
||||
return;
|
||||
}
|
||||
// List sessions
|
||||
if (path === '/api/sessions' && method === 'GET') {
|
||||
json(200, { sessions: this.listSessions() });
|
||||
return;
|
||||
}
|
||||
// Root — simple landing page
|
||||
if (path === '/') {
|
||||
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}
|
||||
.c{text-align:center}h1{font-size:3rem}p{color:#a0a0b0}a{color:#6366f1;text-decoration:none;padding:12px 24px;border:1px solid #6366f1;border-radius:8px;display:inline-block}a:hover{background:#6366f1;color:#fff}</style>
|
||||
</head><body><div class="c"><h1>RuvBot</h1><p>Enterprise-grade AI Assistant</p><a href="/api/status">API Status</a></div></body></html>`);
|
||||
return;
|
||||
}
|
||||
// 404
|
||||
json(404, { error: 'Not found' });
|
||||
}
|
||||
parseRequestBody(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
req.on('data', (chunk) => chunks.push(chunk));
|
||||
req.on('end', () => {
|
||||
if (chunks.length === 0) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
resolve(JSON.parse(Buffer.concat(chunks).toString('utf-8')));
|
||||
}
|
||||
catch {
|
||||
reject(new Error('Invalid JSON'));
|
||||
}
|
||||
});
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
async generateResponse(session, agent, userMessage) {
|
||||
// If no LLM provider, return helpful error message
|
||||
if (!this.llmProvider) {
|
||||
this.logger.warn('No LLM provider configured');
|
||||
return `**LLM Not Configured**
|
||||
|
||||
To enable AI responses, please set one of these environment variables:
|
||||
|
||||
- \`GOOGLE_AI_API_KEY\` - Get from [Google AI Studio](https://aistudio.google.com/app/apikey)
|
||||
- \`ANTHROPIC_API_KEY\` - Get from [Anthropic Console](https://console.anthropic.com/)
|
||||
- \`OPENROUTER_API_KEY\` - Get from [OpenRouter](https://openrouter.ai/)
|
||||
|
||||
Then redeploy the service with the API key set.
|
||||
|
||||
*Your message was: "${userMessage}"*`;
|
||||
}
|
||||
// Build message history for context
|
||||
const messages = [];
|
||||
// Add system prompt from agent config
|
||||
if (agent.config.systemPrompt) {
|
||||
messages.push({
|
||||
role: 'system',
|
||||
content: agent.config.systemPrompt,
|
||||
});
|
||||
}
|
||||
// Add recent message history (last 20 messages for context)
|
||||
const recentMessages = session.messages.slice(-20);
|
||||
for (const msg of recentMessages) {
|
||||
messages.push({
|
||||
role: msg.role === 'user' ? 'user' : 'assistant',
|
||||
content: msg.content,
|
||||
});
|
||||
}
|
||||
// Add current user message
|
||||
messages.push({
|
||||
role: 'user',
|
||||
content: userMessage,
|
||||
});
|
||||
try {
|
||||
// Call LLM provider
|
||||
const completion = await this.llmProvider.complete(messages, {
|
||||
temperature: agent.config.temperature ?? 0.7,
|
||||
maxTokens: agent.config.maxTokens ?? 4096,
|
||||
});
|
||||
this.logger.debug({
|
||||
inputTokens: completion.usage.inputTokens,
|
||||
outputTokens: completion.usage.outputTokens,
|
||||
finishReason: completion.finishReason,
|
||||
}, 'LLM response received');
|
||||
return completion.content;
|
||||
}
|
||||
catch (error) {
|
||||
this.logger.error({ error }, 'LLM completion failed');
|
||||
throw new errors_js_1.RuvBotError(`Failed to generate response: ${error instanceof Error ? error.message : 'Unknown error'}`, 'LLM_ERROR');
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.RuvBot = RuvBot;
|
||||
// ============================================================================
|
||||
// Factory Functions
|
||||
// ============================================================================
|
||||
/**
|
||||
* Create a new RuvBot instance
|
||||
*/
|
||||
function createRuvBot(options) {
|
||||
return new RuvBot(options);
|
||||
}
|
||||
/**
|
||||
* Create a RuvBot instance from environment variables
|
||||
*/
|
||||
function createRuvBotFromEnv() {
|
||||
return new RuvBot();
|
||||
}
|
||||
exports.default = RuvBot;
|
||||
//# sourceMappingURL=RuvBot.js.map
|
||||
1
vendor/ruvector/npm/packages/ruvbot/src/RuvBot.js.map
vendored
Normal file
1
vendor/ruvector/npm/packages/ruvbot/src/RuvBot.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
780
vendor/ruvector/npm/packages/ruvbot/src/RuvBot.ts
vendored
Normal file
780
vendor/ruvector/npm/packages/ruvbot/src/RuvBot.ts
vendored
Normal file
@@ -0,0 +1,780 @@
|
||||
/**
|
||||
* RuvBot - Self-learning AI Assistant with RuVector Backend
|
||||
*
|
||||
* Main entry point for the RuvBot framework.
|
||||
* Combines Clawdbot-style personal AI with RuVector's WASM vector operations.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { createServer, type Server, type IncomingMessage, type ServerResponse } from 'node:http';
|
||||
import pino from 'pino';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { ConfigManager, type BotConfig } from './core/BotConfig.js';
|
||||
import { BotStateManager, type BotStatus } from './core/BotState.js';
|
||||
import type {
|
||||
Agent,
|
||||
AgentConfig,
|
||||
Session,
|
||||
Message,
|
||||
BotEvent,
|
||||
BotEventType,
|
||||
Result,
|
||||
ok,
|
||||
err,
|
||||
} from './core/types.js';
|
||||
import { RuvBotError, ConfigurationError, InitializationError } from './core/errors.js';
|
||||
import {
|
||||
type LLMProvider,
|
||||
type Message as LLMMessage,
|
||||
createAnthropicProvider,
|
||||
createOpenRouterProvider,
|
||||
createGoogleAIProvider,
|
||||
} from './integration/providers/index.js';
|
||||
|
||||
type BotState = BotStatus;
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
export interface RuvBotOptions {
|
||||
config?: Partial<BotConfig>;
|
||||
configPath?: string;
|
||||
autoStart?: boolean;
|
||||
}
|
||||
|
||||
export interface RuvBotEvents {
|
||||
ready: () => void;
|
||||
shutdown: () => void;
|
||||
error: (error: Error) => void;
|
||||
message: (message: Message, session: Session) => void;
|
||||
'agent:spawn': (agent: Agent) => void;
|
||||
'agent:stop': (agentId: string) => void;
|
||||
'session:create': (session: Session) => void;
|
||||
'session:end': (sessionId: string) => void;
|
||||
'memory:store': (entryId: string) => void;
|
||||
'skill:invoke': (skillName: string, params: Record<string, unknown>) => void;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RuvBot Main Class
|
||||
// ============================================================================
|
||||
|
||||
export class RuvBot extends EventEmitter<RuvBotEvents> {
|
||||
private readonly id: string;
|
||||
private readonly configManager: ConfigManager;
|
||||
private readonly stateManager: BotStateManager;
|
||||
private readonly logger: pino.Logger;
|
||||
|
||||
private agents: Map<string, Agent> = new Map();
|
||||
private sessions: Map<string, Session> = new Map();
|
||||
private isRunning: boolean = false;
|
||||
private startTime?: Date;
|
||||
private llmProvider: LLMProvider | null = null;
|
||||
private httpServer: Server | null = null;
|
||||
|
||||
constructor(options: RuvBotOptions = {}) {
|
||||
super();
|
||||
|
||||
this.id = uuidv4();
|
||||
|
||||
// Initialize configuration
|
||||
if (options.config) {
|
||||
this.configManager = new ConfigManager(options.config);
|
||||
} else {
|
||||
this.configManager = ConfigManager.fromEnv();
|
||||
}
|
||||
|
||||
// Validate configuration
|
||||
const validation = this.configManager.validate();
|
||||
if (!validation.valid) {
|
||||
throw new ConfigurationError(
|
||||
`Invalid configuration: ${validation.errors.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
// Initialize logger
|
||||
const config = this.configManager.getConfig();
|
||||
this.logger = pino({
|
||||
level: config.logging.level,
|
||||
transport: config.logging.pretty
|
||||
? { target: 'pino-pretty', options: { colorize: true } }
|
||||
: undefined,
|
||||
});
|
||||
|
||||
// Initialize state manager
|
||||
this.stateManager = new BotStateManager();
|
||||
|
||||
this.logger.info({ botId: this.id }, 'RuvBot instance created');
|
||||
|
||||
// Auto-start if requested
|
||||
if (options.autoStart) {
|
||||
this.start().catch((error) => {
|
||||
this.logger.error({ error }, 'Auto-start failed');
|
||||
this.emit('error', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Lifecycle Methods
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Start the bot and all configured services
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
if (this.isRunning) {
|
||||
this.logger.warn('RuvBot is already running');
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.info('Starting RuvBot...');
|
||||
this.stateManager.setStatus('starting');
|
||||
|
||||
try {
|
||||
const config = this.configManager.getConfig();
|
||||
|
||||
// Initialize core services
|
||||
await this.initializeServices();
|
||||
|
||||
// Start integrations
|
||||
await this.startIntegrations(config);
|
||||
|
||||
// Start API server if enabled
|
||||
if (config.api.enabled) {
|
||||
await this.startApiServer(config);
|
||||
}
|
||||
|
||||
// Mark as running
|
||||
this.isRunning = true;
|
||||
this.startTime = new Date();
|
||||
this.stateManager.setStatus('running');
|
||||
|
||||
this.logger.info(
|
||||
{ botId: this.id, name: config.name },
|
||||
'RuvBot started successfully'
|
||||
);
|
||||
this.emit('ready');
|
||||
} catch (error) {
|
||||
this.stateManager.setStatus('error');
|
||||
this.logger.error({ error }, 'Failed to start RuvBot');
|
||||
throw new InitializationError(
|
||||
`Failed to start RuvBot: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the bot and cleanup resources
|
||||
*/
|
||||
async stop(): Promise<void> {
|
||||
if (!this.isRunning) {
|
||||
this.logger.warn('RuvBot is not running');
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.info('Stopping RuvBot...');
|
||||
this.stateManager.setStatus('stopping');
|
||||
|
||||
try {
|
||||
// Stop all agents
|
||||
for (const [agentId] of this.agents) {
|
||||
await this.stopAgent(agentId);
|
||||
}
|
||||
|
||||
// End all sessions
|
||||
for (const [sessionId] of this.sessions) {
|
||||
await this.endSession(sessionId);
|
||||
}
|
||||
|
||||
// Stop integrations
|
||||
await this.stopIntegrations();
|
||||
|
||||
// Stop API server
|
||||
await this.stopApiServer();
|
||||
|
||||
this.isRunning = false;
|
||||
this.stateManager.setStatus('stopped');
|
||||
|
||||
this.logger.info('RuvBot stopped successfully');
|
||||
this.emit('shutdown');
|
||||
} catch (error) {
|
||||
this.stateManager.setStatus('error');
|
||||
this.logger.error({ error }, 'Error during shutdown');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Agent Management
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Spawn a new agent with the given configuration
|
||||
*/
|
||||
async spawnAgent(config: AgentConfig): Promise<Agent> {
|
||||
const agentId = config.id || uuidv4();
|
||||
|
||||
if (this.agents.has(agentId)) {
|
||||
throw new RuvBotError(`Agent with ID ${agentId} already exists`, 'AGENT_EXISTS');
|
||||
}
|
||||
|
||||
const agent: Agent = {
|
||||
id: agentId,
|
||||
name: config.name,
|
||||
config,
|
||||
status: 'idle',
|
||||
createdAt: new Date(),
|
||||
lastActiveAt: new Date(),
|
||||
};
|
||||
|
||||
this.agents.set(agentId, agent);
|
||||
this.logger.info({ agentId, name: config.name }, 'Agent spawned');
|
||||
this.emit('agent:spawn', agent);
|
||||
|
||||
return agent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop an agent by ID
|
||||
*/
|
||||
async stopAgent(agentId: string): Promise<void> {
|
||||
const agent = this.agents.get(agentId);
|
||||
if (!agent) {
|
||||
throw new RuvBotError(`Agent with ID ${agentId} not found`, 'AGENT_NOT_FOUND');
|
||||
}
|
||||
|
||||
// End all sessions for this agent
|
||||
for (const [sessionId, session] of this.sessions) {
|
||||
if (session.agentId === agentId) {
|
||||
await this.endSession(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
this.agents.delete(agentId);
|
||||
this.logger.info({ agentId }, 'Agent stopped');
|
||||
this.emit('agent:stop', agentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an agent by ID
|
||||
*/
|
||||
getAgent(agentId: string): Agent | undefined {
|
||||
return this.agents.get(agentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all active agents
|
||||
*/
|
||||
listAgents(): Agent[] {
|
||||
return Array.from(this.agents.values());
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Session Management
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Create a new session for an agent
|
||||
*/
|
||||
async createSession(
|
||||
agentId: string,
|
||||
options: {
|
||||
userId?: string;
|
||||
channelId?: string;
|
||||
platform?: Session['platform'];
|
||||
metadata?: Record<string, unknown>;
|
||||
} = {}
|
||||
): Promise<Session> {
|
||||
const agent = this.agents.get(agentId);
|
||||
if (!agent) {
|
||||
throw new RuvBotError(`Agent with ID ${agentId} not found`, 'AGENT_NOT_FOUND');
|
||||
}
|
||||
|
||||
const sessionId = uuidv4();
|
||||
const config = this.configManager.getConfig();
|
||||
|
||||
const session: Session = {
|
||||
id: sessionId,
|
||||
agentId,
|
||||
userId: options.userId,
|
||||
channelId: options.channelId,
|
||||
platform: options.platform || 'api',
|
||||
messages: [],
|
||||
context: {
|
||||
topics: [],
|
||||
entities: [],
|
||||
},
|
||||
metadata: options.metadata || {},
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
expiresAt: new Date(Date.now() + config.session.defaultTTL),
|
||||
};
|
||||
|
||||
this.sessions.set(sessionId, session);
|
||||
this.logger.info({ sessionId, agentId }, 'Session created');
|
||||
this.emit('session:create', session);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* End a session by ID
|
||||
*/
|
||||
async endSession(sessionId: string): Promise<void> {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) {
|
||||
throw new RuvBotError(`Session with ID ${sessionId} not found`, 'SESSION_NOT_FOUND');
|
||||
}
|
||||
|
||||
this.sessions.delete(sessionId);
|
||||
this.logger.info({ sessionId }, 'Session ended');
|
||||
this.emit('session:end', sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a session by ID
|
||||
*/
|
||||
getSession(sessionId: string): Session | undefined {
|
||||
return this.sessions.get(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all active sessions
|
||||
*/
|
||||
listSessions(): Session[] {
|
||||
return Array.from(this.sessions.values());
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Message Handling
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Send a message to an agent in a session
|
||||
*/
|
||||
async chat(
|
||||
sessionId: string,
|
||||
content: string,
|
||||
options: {
|
||||
userId?: string;
|
||||
attachments?: Message['attachments'];
|
||||
metadata?: Message['metadata'];
|
||||
} = {}
|
||||
): Promise<Message> {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) {
|
||||
throw new RuvBotError(`Session with ID ${sessionId} not found`, 'SESSION_NOT_FOUND');
|
||||
}
|
||||
|
||||
const agent = this.agents.get(session.agentId);
|
||||
if (!agent) {
|
||||
throw new RuvBotError(`Agent with ID ${session.agentId} not found`, 'AGENT_NOT_FOUND');
|
||||
}
|
||||
|
||||
// Create user message
|
||||
const userMessage: Message = {
|
||||
id: uuidv4(),
|
||||
sessionId,
|
||||
role: 'user',
|
||||
content,
|
||||
attachments: options.attachments,
|
||||
metadata: options.metadata,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
// Add to session
|
||||
session.messages.push(userMessage);
|
||||
session.updatedAt = new Date();
|
||||
|
||||
// Update agent status
|
||||
agent.status = 'processing';
|
||||
agent.lastActiveAt = new Date();
|
||||
|
||||
this.logger.debug({ sessionId, messageId: userMessage.id }, 'User message received');
|
||||
this.emit('message', userMessage, session);
|
||||
|
||||
try {
|
||||
// Generate response (placeholder for LLM integration)
|
||||
const responseContent = await this.generateResponse(session, agent, content);
|
||||
|
||||
// Create assistant message
|
||||
const assistantMessage: Message = {
|
||||
id: uuidv4(),
|
||||
sessionId,
|
||||
role: 'assistant',
|
||||
content: responseContent,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
session.messages.push(assistantMessage);
|
||||
session.updatedAt = new Date();
|
||||
agent.status = 'idle';
|
||||
|
||||
this.logger.debug(
|
||||
{ sessionId, messageId: assistantMessage.id },
|
||||
'Assistant response generated'
|
||||
);
|
||||
this.emit('message', assistantMessage, session);
|
||||
|
||||
return assistantMessage;
|
||||
} catch (error) {
|
||||
agent.status = 'error';
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Status & Info
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Get the current bot status
|
||||
*/
|
||||
getStatus(): {
|
||||
id: string;
|
||||
name: string;
|
||||
state: BotState;
|
||||
isRunning: boolean;
|
||||
uptime?: number;
|
||||
agents: number;
|
||||
sessions: number;
|
||||
} {
|
||||
const config = this.configManager.getConfig();
|
||||
|
||||
return {
|
||||
id: this.id,
|
||||
name: config.name,
|
||||
state: this.stateManager.getStatus(),
|
||||
isRunning: this.isRunning,
|
||||
uptime: this.startTime
|
||||
? Date.now() - this.startTime.getTime()
|
||||
: undefined,
|
||||
agents: this.agents.size,
|
||||
sessions: this.sessions.size,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current configuration
|
||||
*/
|
||||
getConfig(): Readonly<BotConfig> {
|
||||
return this.configManager.getConfig();
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Private Methods
|
||||
// ==========================================================================
|
||||
|
||||
private async initializeServices(): Promise<void> {
|
||||
this.logger.debug('Initializing core services...');
|
||||
|
||||
const config = this.configManager.getConfig();
|
||||
|
||||
// Initialize LLM provider based on configuration
|
||||
const { provider, apiKey, model } = config.llm;
|
||||
|
||||
// Check for available API keys in priority order
|
||||
const openrouterKey = process.env.OPENROUTER_API_KEY;
|
||||
const anthropicKey = process.env.ANTHROPIC_API_KEY || apiKey;
|
||||
const googleAIKey = process.env.GOOGLE_AI_API_KEY || process.env.GEMINI_API_KEY;
|
||||
|
||||
if (openrouterKey) {
|
||||
// Use OpenRouter for Gemini 2.5 and other models
|
||||
this.llmProvider = createOpenRouterProvider({
|
||||
apiKey: openrouterKey,
|
||||
model: model || 'google/gemini-2.5-pro-preview-05-06',
|
||||
siteName: 'RuvBot',
|
||||
});
|
||||
this.logger.info({ provider: 'openrouter', model: model || 'google/gemini-2.5-pro-preview-05-06' }, 'LLM provider initialized');
|
||||
} else if (googleAIKey) {
|
||||
// Use Google AI directly (Gemini 2.5)
|
||||
this.llmProvider = createGoogleAIProvider({
|
||||
apiKey: googleAIKey,
|
||||
model: model || 'gemini-2.5-flash',
|
||||
});
|
||||
this.logger.info({ provider: 'google-ai', model: model || 'gemini-2.5-flash' }, 'LLM provider initialized');
|
||||
} else if (provider === 'anthropic' && anthropicKey) {
|
||||
this.llmProvider = createAnthropicProvider({
|
||||
apiKey: anthropicKey,
|
||||
model: model || 'claude-3-5-sonnet-20241022',
|
||||
});
|
||||
this.logger.info({ provider: 'anthropic', model }, 'LLM provider initialized');
|
||||
} else if (anthropicKey) {
|
||||
// Fallback to Anthropic if only that key is available
|
||||
this.llmProvider = createAnthropicProvider({
|
||||
apiKey: anthropicKey,
|
||||
model: model || 'claude-3-5-sonnet-20241022',
|
||||
});
|
||||
this.logger.info({ provider: 'anthropic', model }, 'LLM provider initialized');
|
||||
} else {
|
||||
this.logger.warn({}, 'No LLM API key found. Set GOOGLE_AI_API_KEY, ANTHROPIC_API_KEY, or OPENROUTER_API_KEY');
|
||||
}
|
||||
|
||||
// TODO: Initialize memory manager, skill registry, etc.
|
||||
}
|
||||
|
||||
private async startIntegrations(config: BotConfig): Promise<void> {
|
||||
this.logger.debug('Starting integrations...');
|
||||
|
||||
if (config.slack.enabled) {
|
||||
this.logger.info('Slack integration enabled');
|
||||
// TODO: Initialize Slack adapter
|
||||
}
|
||||
|
||||
if (config.discord.enabled) {
|
||||
this.logger.info('Discord integration enabled');
|
||||
// TODO: Initialize Discord adapter
|
||||
}
|
||||
|
||||
if (config.webhook.enabled) {
|
||||
this.logger.info('Webhook integration enabled');
|
||||
// TODO: Initialize webhook handler
|
||||
}
|
||||
}
|
||||
|
||||
private async stopIntegrations(): Promise<void> {
|
||||
this.logger.debug('Stopping integrations...');
|
||||
// TODO: Stop all integration adapters
|
||||
}
|
||||
|
||||
private async startApiServer(config: BotConfig): Promise<void> {
|
||||
const port = config.api.port || 3000;
|
||||
const host = config.api.host || '0.0.0.0';
|
||||
|
||||
this.httpServer = createServer((req, res) => {
|
||||
this.handleApiRequest(req, res).catch((error) => {
|
||||
this.logger.error({ err: error }, 'Unhandled API request error');
|
||||
if (!res.headersSent) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Internal server error' }));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this.httpServer!.on('error', (err) => {
|
||||
this.logger.error({ err, port, host }, 'API server failed to start');
|
||||
reject(err);
|
||||
});
|
||||
|
||||
this.httpServer!.listen(port, host, () => {
|
||||
this.logger.info({ port, host }, 'API server listening');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async stopApiServer(): Promise<void> {
|
||||
if (!this.httpServer) return;
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
this.httpServer!.close(() => {
|
||||
this.logger.debug('API server stopped');
|
||||
this.httpServer = null;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async handleApiRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
||||
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
||||
const path = url.pathname;
|
||||
const method = req.method || 'GET';
|
||||
|
||||
// CORS headers
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
|
||||
if (method === 'OPTIONS') {
|
||||
res.writeHead(204);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const json = (status: number, data: unknown) => {
|
||||
res.writeHead(status, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(data));
|
||||
};
|
||||
|
||||
// Health check
|
||||
if (path === '/health' || path === '/healthz') {
|
||||
json(200, {
|
||||
status: 'healthy',
|
||||
uptime: this.startTime ? Math.floor((Date.now() - this.startTime.getTime()) / 1000) : 0,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Readiness check
|
||||
if (path === '/ready' || path === '/readyz') {
|
||||
if (this.isRunning) {
|
||||
json(200, { status: 'ready' });
|
||||
} else {
|
||||
json(503, { status: 'not ready' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Status
|
||||
if (path === '/api/status') {
|
||||
json(200, this.getStatus());
|
||||
return;
|
||||
}
|
||||
|
||||
// Chat endpoint
|
||||
if (path === '/api/chat' && method === 'POST') {
|
||||
const body = await this.parseRequestBody(req);
|
||||
const message = body?.message as string;
|
||||
const agentId = (body?.agentId as string) || 'default-agent';
|
||||
|
||||
if (!message) {
|
||||
json(400, { error: 'Missing "message" field' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Create or reuse a session
|
||||
let sessionId = body?.sessionId as string;
|
||||
if (!sessionId || !this.sessions.has(sessionId)) {
|
||||
const session = await this.createSession(agentId);
|
||||
sessionId = session.id;
|
||||
}
|
||||
|
||||
const response = await this.chat(sessionId, message);
|
||||
json(200, { sessionId, agentId, response });
|
||||
return;
|
||||
}
|
||||
|
||||
// List agents
|
||||
if (path === '/api/agents' && method === 'GET') {
|
||||
json(200, { agents: this.listAgents() });
|
||||
return;
|
||||
}
|
||||
|
||||
// List sessions
|
||||
if (path === '/api/sessions' && method === 'GET') {
|
||||
json(200, { sessions: this.listSessions() });
|
||||
return;
|
||||
}
|
||||
|
||||
// Root — simple landing page
|
||||
if (path === '/') {
|
||||
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}
|
||||
.c{text-align:center}h1{font-size:3rem}p{color:#a0a0b0}a{color:#6366f1;text-decoration:none;padding:12px 24px;border:1px solid #6366f1;border-radius:8px;display:inline-block}a:hover{background:#6366f1;color:#fff}</style>
|
||||
</head><body><div class="c"><h1>RuvBot</h1><p>Enterprise-grade AI Assistant</p><a href="/api/status">API Status</a></div></body></html>`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 404
|
||||
json(404, { error: 'Not found' });
|
||||
}
|
||||
|
||||
private parseRequestBody(req: IncomingMessage): Promise<Record<string, unknown> | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
req.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||||
req.on('end', () => {
|
||||
if (chunks.length === 0) { resolve(null); return; }
|
||||
try { resolve(JSON.parse(Buffer.concat(chunks).toString('utf-8'))); }
|
||||
catch { reject(new Error('Invalid JSON')); }
|
||||
});
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
private async generateResponse(
|
||||
session: Session,
|
||||
agent: Agent,
|
||||
userMessage: string
|
||||
): Promise<string> {
|
||||
// If no LLM provider, return helpful error message
|
||||
if (!this.llmProvider) {
|
||||
this.logger.warn('No LLM provider configured');
|
||||
return `**LLM Not Configured**
|
||||
|
||||
To enable AI responses, please set one of these environment variables:
|
||||
|
||||
- \`GOOGLE_AI_API_KEY\` - Get from [Google AI Studio](https://aistudio.google.com/app/apikey)
|
||||
- \`ANTHROPIC_API_KEY\` - Get from [Anthropic Console](https://console.anthropic.com/)
|
||||
- \`OPENROUTER_API_KEY\` - Get from [OpenRouter](https://openrouter.ai/)
|
||||
|
||||
Then redeploy the service with the API key set.
|
||||
|
||||
*Your message was: "${userMessage}"*`;
|
||||
}
|
||||
|
||||
// Build message history for context
|
||||
const messages: LLMMessage[] = [];
|
||||
|
||||
// Add system prompt from agent config
|
||||
if (agent.config.systemPrompt) {
|
||||
messages.push({
|
||||
role: 'system',
|
||||
content: agent.config.systemPrompt,
|
||||
});
|
||||
}
|
||||
|
||||
// Add recent message history (last 20 messages for context)
|
||||
const recentMessages = session.messages.slice(-20);
|
||||
for (const msg of recentMessages) {
|
||||
messages.push({
|
||||
role: msg.role === 'user' ? 'user' : 'assistant',
|
||||
content: msg.content,
|
||||
});
|
||||
}
|
||||
|
||||
// Add current user message
|
||||
messages.push({
|
||||
role: 'user',
|
||||
content: userMessage,
|
||||
});
|
||||
|
||||
try {
|
||||
// Call LLM provider
|
||||
const completion = await this.llmProvider.complete(messages, {
|
||||
temperature: agent.config.temperature ?? 0.7,
|
||||
maxTokens: agent.config.maxTokens ?? 4096,
|
||||
});
|
||||
|
||||
this.logger.debug({
|
||||
inputTokens: completion.usage.inputTokens,
|
||||
outputTokens: completion.usage.outputTokens,
|
||||
finishReason: completion.finishReason,
|
||||
}, 'LLM response received');
|
||||
|
||||
return completion.content;
|
||||
} catch (error) {
|
||||
this.logger.error({ error }, 'LLM completion failed');
|
||||
throw new RuvBotError(
|
||||
`Failed to generate response: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
'LLM_ERROR'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Factory Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Create a new RuvBot instance
|
||||
*/
|
||||
export function createRuvBot(options?: RuvBotOptions): RuvBot {
|
||||
return new RuvBot(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a RuvBot instance from environment variables
|
||||
*/
|
||||
export function createRuvBotFromEnv(): RuvBot {
|
||||
return new RuvBot();
|
||||
}
|
||||
|
||||
export default RuvBot;
|
||||
1
vendor/ruvector/npm/packages/ruvbot/src/api/index.d.ts.map
vendored
Normal file
1
vendor/ruvector/npm/packages/ruvbot/src/api/index.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,eAAO,MAAM,kBAAkB,UAAU,CAAC;AAE1C,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE;QACV,GAAG,EAAE,MAAM,CAAC;QACZ,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,IAAI,CAAC,EAAE;QACL,OAAO,EAAE,OAAO,CAAC;QACjB,IAAI,EAAE,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;QACpC,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;CACH;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAO,CAAC;IACpD,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAChE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC"}
|
||||
1
vendor/ruvector/npm/packages/ruvbot/src/api/index.js.map
vendored
Normal file
1
vendor/ruvector/npm/packages/ruvbot/src/api/index.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;;AAEH,0CAA0C;AAC7B,QAAA,kBAAkB,GAAG,OAAO,CAAC"}
|
||||
30
vendor/ruvector/npm/packages/ruvbot/src/api/index.ts
vendored
Normal file
30
vendor/ruvector/npm/packages/ruvbot/src/api/index.ts
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* API module exports
|
||||
*
|
||||
* Provides REST and GraphQL endpoints.
|
||||
*/
|
||||
|
||||
// Placeholder exports - to be implemented
|
||||
export const API_MODULE_VERSION = '0.1.0';
|
||||
|
||||
export interface APIServerOptions {
|
||||
port: number;
|
||||
host?: string;
|
||||
cors?: boolean;
|
||||
rateLimit?: {
|
||||
max: number;
|
||||
timeWindow: number;
|
||||
};
|
||||
auth?: {
|
||||
enabled: boolean;
|
||||
type: 'bearer' | 'basic' | 'apikey';
|
||||
secret?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface APIRoute {
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
||||
path: string;
|
||||
handler: (request: unknown, reply: unknown) => Promise<unknown>;
|
||||
schema?: Record<string, unknown>;
|
||||
}
|
||||
934
vendor/ruvector/npm/packages/ruvbot/src/api/public/index.html
vendored
Normal file
934
vendor/ruvector/npm/packages/ruvbot/src/api/public/index.html
vendored
Normal file
@@ -0,0 +1,934 @@
|
||||
<!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; }
|
||||
}
|
||||
|
||||
/* Skill Badges */
|
||||
.skill-badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.skill-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.skill-badge.success {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.skill-badge.failed {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.skill-badge::before {
|
||||
content: '✨';
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.skill-badge.failed::before {
|
||||
content: '⚠️';
|
||||
}
|
||||
|
||||
/* 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() {
|
||||
console.log('[RuvBot] Creating new session...');
|
||||
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();
|
||||
console.log('[RuvBot] Session created:', data);
|
||||
sessionId = data.id || data.sessionId;
|
||||
return sessionId;
|
||||
} catch (err) {
|
||||
console.error('[RuvBot] 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
|
||||
console.log('[RuvBot] Sending message:', { sessionId, message, model: modelSelect.value });
|
||||
const startTime = performance.now();
|
||||
|
||||
const res = await fetch(`/api/sessions/${sessionId}/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
message,
|
||||
model: modelSelect.value
|
||||
})
|
||||
});
|
||||
|
||||
const responseTime = (performance.now() - startTime).toFixed(0);
|
||||
console.log(`[RuvBot] Response received in ${responseTime}ms, status: ${res.status}`);
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json();
|
||||
console.error('[RuvBot] API error:', error);
|
||||
throw new Error(error.message || error.error || 'Request failed');
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
console.log('[RuvBot] Chat response:', data);
|
||||
|
||||
// Remove typing indicator
|
||||
typingEl.remove();
|
||||
|
||||
// Build skill badges if skills were used
|
||||
let skillBadges = '';
|
||||
if (data.skillsUsed && data.skillsUsed.length > 0) {
|
||||
const badges = data.skillsUsed.map(s =>
|
||||
`<span class="skill-badge ${s.success ? 'success' : 'failed'}">${s.skillName}</span>`
|
||||
).join(' ');
|
||||
skillBadges = `<div class="skill-badges">${badges}</div>`;
|
||||
console.log('[RuvBot] Skills used:', data.skillsUsed.map(s => s.skillId));
|
||||
}
|
||||
|
||||
// Add assistant message
|
||||
const content = data.content || data.message || data.response || 'No response';
|
||||
console.log('[RuvBot] Displaying content:', content.substring(0, 100) + '...');
|
||||
addMessage('assistant', skillBadges + 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 and status
|
||||
async function checkHealth() {
|
||||
console.log('[RuvBot] Checking system health...');
|
||||
try {
|
||||
const [healthRes, statusRes] = await Promise.all([
|
||||
fetch('/health'),
|
||||
fetch('/api/status')
|
||||
]);
|
||||
const health = await healthRes.json();
|
||||
const status = await statusRes.json();
|
||||
console.log('[RuvBot] Health:', health);
|
||||
console.log('[RuvBot] Status:', status);
|
||||
|
||||
// Show LLM status indicator
|
||||
if (status.llm?.configured) {
|
||||
console.log('[RuvBot] LLM configured:', status.llm.provider, status.llm.model);
|
||||
} else {
|
||||
console.warn('[RuvBot] LLM not configured! Check ANTHROPIC_API_KEY or OPENROUTER_API_KEY');
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[RuvBot] Health check failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Init
|
||||
console.log('[RuvBot] Chat UI initialized');
|
||||
checkHealth();
|
||||
inputEl.focus();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
94
vendor/ruvector/npm/packages/ruvbot/src/channels/ChannelRegistry.d.ts
vendored
Normal file
94
vendor/ruvector/npm/packages/ruvbot/src/channels/ChannelRegistry.d.ts
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* ChannelRegistry - Multi-Channel Management
|
||||
*
|
||||
* Manages multiple channel adapters with unified message routing,
|
||||
* multi-tenant isolation, and rate limiting.
|
||||
*/
|
||||
import type { BaseAdapter, ChannelType, MessageHandler, AdapterConfig } from './adapters/BaseAdapter.js';
|
||||
export interface ChannelFilter {
|
||||
types?: ChannelType[];
|
||||
tenantIds?: string[];
|
||||
channelIds?: string[];
|
||||
}
|
||||
export interface ChannelRegistryConfig {
|
||||
defaultRateLimit?: {
|
||||
requests: number;
|
||||
windowMs: number;
|
||||
};
|
||||
}
|
||||
export interface AdapterFactory {
|
||||
(config: AdapterConfig): BaseAdapter;
|
||||
}
|
||||
export declare class ChannelRegistry {
|
||||
private adapters;
|
||||
private adaptersByType;
|
||||
private adaptersByTenant;
|
||||
private globalHandlers;
|
||||
private config;
|
||||
private rateLimitWindows;
|
||||
constructor(config?: ChannelRegistryConfig);
|
||||
/**
|
||||
* Generate unique adapter key
|
||||
*/
|
||||
private getAdapterKey;
|
||||
/**
|
||||
* Register a channel adapter
|
||||
*/
|
||||
register(adapter: BaseAdapter): void;
|
||||
/**
|
||||
* Unregister a channel adapter
|
||||
*/
|
||||
unregister(type: ChannelType, tenantId: string): boolean;
|
||||
/**
|
||||
* Get a specific adapter
|
||||
*/
|
||||
get(type: ChannelType, tenantId: string): BaseAdapter | undefined;
|
||||
/**
|
||||
* Get all adapters for a type
|
||||
*/
|
||||
getByType(type: ChannelType): BaseAdapter[];
|
||||
/**
|
||||
* Get all adapters for a tenant
|
||||
*/
|
||||
getByTenant(tenantId: string): BaseAdapter[];
|
||||
/**
|
||||
* Get all registered adapters
|
||||
*/
|
||||
getAll(): BaseAdapter[];
|
||||
/**
|
||||
* Register a global message handler
|
||||
*/
|
||||
onMessage(handler: MessageHandler): void;
|
||||
/**
|
||||
* Remove a global message handler
|
||||
*/
|
||||
offMessage(handler: MessageHandler): void;
|
||||
/**
|
||||
* Start all adapters
|
||||
*/
|
||||
start(): Promise<void>;
|
||||
/**
|
||||
* Stop all adapters
|
||||
*/
|
||||
stop(): Promise<void>;
|
||||
/**
|
||||
* Broadcast a message to multiple channels
|
||||
*/
|
||||
broadcast(message: string, channelIds: string[], filter?: ChannelFilter): Promise<Map<string, string>>;
|
||||
/**
|
||||
* Get registry statistics
|
||||
*/
|
||||
getStats(): {
|
||||
totalAdapters: number;
|
||||
byType: Record<ChannelType, number>;
|
||||
byTenant: Record<string, number>;
|
||||
connected: number;
|
||||
totalMessages: number;
|
||||
};
|
||||
private handleMessage;
|
||||
private filterAdapters;
|
||||
private checkRateLimit;
|
||||
}
|
||||
export declare function createChannelRegistry(config?: ChannelRegistryConfig): ChannelRegistry;
|
||||
export default ChannelRegistry;
|
||||
//# sourceMappingURL=ChannelRegistry.d.ts.map
|
||||
1
vendor/ruvector/npm/packages/ruvbot/src/channels/ChannelRegistry.d.ts.map
vendored
Normal file
1
vendor/ruvector/npm/packages/ruvbot/src/channels/ChannelRegistry.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"ChannelRegistry.d.ts","sourceRoot":"","sources":["ChannelRegistry.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,WAAW,EACX,WAAW,EAEX,cAAc,EACd,aAAa,EACd,MAAM,2BAA2B,CAAC;AAMnC,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,WAAW,EAAE,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,qBAAqB;IACpC,gBAAgB,CAAC,EAAE;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AAED,MAAM,WAAW,cAAc;IAC7B,CAAC,MAAM,EAAE,aAAa,GAAG,WAAW,CAAC;CACtC;AAMD,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAuC;IACvD,OAAO,CAAC,cAAc,CAA4C;IAClE,OAAO,CAAC,gBAAgB,CAAuC;IAC/D,OAAO,CAAC,cAAc,CAAwB;IAC9C,OAAO,CAAC,MAAM,CAAwB;IAGtC,OAAO,CAAC,gBAAgB,CAA8D;gBAE1E,MAAM,GAAE,qBAA0B;IAI9C;;OAEG;IACH,OAAO,CAAC,aAAa;IAIrB;;OAEG;IACH,QAAQ,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI;IAwBpC;;OAEG;IACH,UAAU,CAAC,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO;IAgBxD;;OAEG;IACH,GAAG,CAAC,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;IAIjE;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,WAAW,GAAG,WAAW,EAAE;IAS3C;;OAEG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,EAAE;IAS5C;;OAEG;IACH,MAAM,IAAI,WAAW,EAAE;IAIvB;;OAEG;IACH,SAAS,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI;IAIxC;;OAEG;IACH,UAAU,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI;IAOzC;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ5B;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAO3B;;OAEG;IACG,SAAS,CACb,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAAE,EACpB,MAAM,CAAC,EAAE,aAAa,GACrB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAoB/B;;OAEG;IACH,QAAQ,IAAI;QACV,aAAa,EAAE,MAAM,CAAC;QACtB,MAAM,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;QACpC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACjC,SAAS,EAAE,MAAM,CAAC;QAClB,aAAa,EAAE,MAAM,CAAC;KACvB;YAgCa,aAAa;IAU3B,OAAO,CAAC,cAAc;IActB,OAAO,CAAC,cAAc;CAoBvB;AAMD,wBAAgB,qBAAqB,CAAC,MAAM,CAAC,EAAE,qBAAqB,GAAG,eAAe,CAErF;AAED,eAAe,eAAe,CAAC"}
|
||||
230
vendor/ruvector/npm/packages/ruvbot/src/channels/ChannelRegistry.js
vendored
Normal file
230
vendor/ruvector/npm/packages/ruvbot/src/channels/ChannelRegistry.js
vendored
Normal file
@@ -0,0 +1,230 @@
|
||||
"use strict";
|
||||
/**
|
||||
* ChannelRegistry - Multi-Channel Management
|
||||
*
|
||||
* Manages multiple channel adapters with unified message routing,
|
||||
* multi-tenant isolation, and rate limiting.
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ChannelRegistry = void 0;
|
||||
exports.createChannelRegistry = createChannelRegistry;
|
||||
// ============================================================================
|
||||
// ChannelRegistry Implementation
|
||||
// ============================================================================
|
||||
class ChannelRegistry {
|
||||
constructor(config = {}) {
|
||||
this.adapters = new Map();
|
||||
this.adaptersByType = new Map();
|
||||
this.adaptersByTenant = new Map();
|
||||
this.globalHandlers = [];
|
||||
// Rate limiting state
|
||||
this.rateLimitWindows = new Map();
|
||||
this.config = config;
|
||||
}
|
||||
/**
|
||||
* Generate unique adapter key
|
||||
*/
|
||||
getAdapterKey(type, tenantId) {
|
||||
return `${type}:${tenantId}`;
|
||||
}
|
||||
/**
|
||||
* Register a channel adapter
|
||||
*/
|
||||
register(adapter) {
|
||||
const key = this.getAdapterKey(adapter.type, adapter.tenantId);
|
||||
// Store adapter
|
||||
this.adapters.set(key, adapter);
|
||||
// Index by type
|
||||
if (!this.adaptersByType.has(adapter.type)) {
|
||||
this.adaptersByType.set(adapter.type, new Set());
|
||||
}
|
||||
this.adaptersByType.get(adapter.type).add(key);
|
||||
// Index by tenant
|
||||
if (!this.adaptersByTenant.has(adapter.tenantId)) {
|
||||
this.adaptersByTenant.set(adapter.tenantId, new Set());
|
||||
}
|
||||
this.adaptersByTenant.get(adapter.tenantId).add(key);
|
||||
// Register global message handler on adapter
|
||||
adapter.onMessage(async (message) => {
|
||||
await this.handleMessage(message);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Unregister a channel adapter
|
||||
*/
|
||||
unregister(type, tenantId) {
|
||||
const key = this.getAdapterKey(type, tenantId);
|
||||
const adapter = this.adapters.get(key);
|
||||
if (!adapter)
|
||||
return false;
|
||||
// Remove from indices
|
||||
this.adaptersByType.get(type)?.delete(key);
|
||||
this.adaptersByTenant.get(tenantId)?.delete(key);
|
||||
// Remove adapter
|
||||
this.adapters.delete(key);
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Get a specific adapter
|
||||
*/
|
||||
get(type, tenantId) {
|
||||
return this.adapters.get(this.getAdapterKey(type, tenantId));
|
||||
}
|
||||
/**
|
||||
* Get all adapters for a type
|
||||
*/
|
||||
getByType(type) {
|
||||
const keys = this.adaptersByType.get(type);
|
||||
if (!keys)
|
||||
return [];
|
||||
return Array.from(keys)
|
||||
.map(key => this.adapters.get(key))
|
||||
.filter((a) => a !== undefined);
|
||||
}
|
||||
/**
|
||||
* Get all adapters for a tenant
|
||||
*/
|
||||
getByTenant(tenantId) {
|
||||
const keys = this.adaptersByTenant.get(tenantId);
|
||||
if (!keys)
|
||||
return [];
|
||||
return Array.from(keys)
|
||||
.map(key => this.adapters.get(key))
|
||||
.filter((a) => a !== undefined);
|
||||
}
|
||||
/**
|
||||
* Get all registered adapters
|
||||
*/
|
||||
getAll() {
|
||||
return Array.from(this.adapters.values());
|
||||
}
|
||||
/**
|
||||
* Register a global message handler
|
||||
*/
|
||||
onMessage(handler) {
|
||||
this.globalHandlers.push(handler);
|
||||
}
|
||||
/**
|
||||
* Remove a global message handler
|
||||
*/
|
||||
offMessage(handler) {
|
||||
const index = this.globalHandlers.indexOf(handler);
|
||||
if (index > -1) {
|
||||
this.globalHandlers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Start all adapters
|
||||
*/
|
||||
async start() {
|
||||
const startPromises = Array.from(this.adapters.values())
|
||||
.filter(adapter => adapter.enabled)
|
||||
.map(adapter => adapter.connect());
|
||||
await Promise.all(startPromises);
|
||||
}
|
||||
/**
|
||||
* Stop all adapters
|
||||
*/
|
||||
async stop() {
|
||||
const stopPromises = Array.from(this.adapters.values())
|
||||
.map(adapter => adapter.disconnect());
|
||||
await Promise.all(stopPromises);
|
||||
}
|
||||
/**
|
||||
* Broadcast a message to multiple channels
|
||||
*/
|
||||
async broadcast(message, channelIds, filter) {
|
||||
const results = new Map();
|
||||
const adapters = this.filterAdapters(filter);
|
||||
for (const adapter of adapters) {
|
||||
for (const channelId of channelIds) {
|
||||
try {
|
||||
if (this.checkRateLimit(adapter)) {
|
||||
const messageId = await adapter.send(channelId, message);
|
||||
results.set(`${adapter.type}:${channelId}`, messageId);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`Failed to broadcast to ${adapter.type}:${channelId}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
/**
|
||||
* Get registry statistics
|
||||
*/
|
||||
getStats() {
|
||||
const byType = {};
|
||||
const byTenant = {};
|
||||
let connected = 0;
|
||||
let totalMessages = 0;
|
||||
for (const adapter of this.adapters.values()) {
|
||||
// By type
|
||||
byType[adapter.type] = (byType[adapter.type] ?? 0) + 1;
|
||||
// By tenant
|
||||
byTenant[adapter.tenantId] = (byTenant[adapter.tenantId] ?? 0) + 1;
|
||||
// Connected status
|
||||
const status = adapter.getStatus();
|
||||
if (status.connected)
|
||||
connected++;
|
||||
totalMessages += status.messageCount;
|
||||
}
|
||||
return {
|
||||
totalAdapters: this.adapters.size,
|
||||
byType,
|
||||
byTenant,
|
||||
connected,
|
||||
totalMessages,
|
||||
};
|
||||
}
|
||||
// ==========================================================================
|
||||
// Private Methods
|
||||
// ==========================================================================
|
||||
async handleMessage(message) {
|
||||
for (const handler of this.globalHandlers) {
|
||||
try {
|
||||
await handler(message);
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Global message handler error:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
filterAdapters(filter) {
|
||||
let adapters = Array.from(this.adapters.values());
|
||||
if (filter?.types) {
|
||||
adapters = adapters.filter(a => filter.types.includes(a.type));
|
||||
}
|
||||
if (filter?.tenantIds) {
|
||||
adapters = adapters.filter(a => filter.tenantIds.includes(a.tenantId));
|
||||
}
|
||||
return adapters.filter(a => a.enabled);
|
||||
}
|
||||
checkRateLimit(adapter) {
|
||||
const config = this.config.defaultRateLimit;
|
||||
if (!config)
|
||||
return true;
|
||||
const key = this.getAdapterKey(adapter.type, adapter.tenantId);
|
||||
const now = Date.now();
|
||||
let window = this.rateLimitWindows.get(key);
|
||||
if (!window || now > window.resetAt) {
|
||||
window = { count: 0, resetAt: now + config.windowMs };
|
||||
this.rateLimitWindows.set(key, window);
|
||||
}
|
||||
if (window.count >= config.requests) {
|
||||
return false;
|
||||
}
|
||||
window.count++;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
exports.ChannelRegistry = ChannelRegistry;
|
||||
// ============================================================================
|
||||
// Factory Function
|
||||
// ============================================================================
|
||||
function createChannelRegistry(config) {
|
||||
return new ChannelRegistry(config);
|
||||
}
|
||||
exports.default = ChannelRegistry;
|
||||
//# sourceMappingURL=ChannelRegistry.js.map
|
||||
1
vendor/ruvector/npm/packages/ruvbot/src/channels/ChannelRegistry.js.map
vendored
Normal file
1
vendor/ruvector/npm/packages/ruvbot/src/channels/ChannelRegistry.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
306
vendor/ruvector/npm/packages/ruvbot/src/channels/ChannelRegistry.ts
vendored
Normal file
306
vendor/ruvector/npm/packages/ruvbot/src/channels/ChannelRegistry.ts
vendored
Normal file
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* ChannelRegistry - Multi-Channel Management
|
||||
*
|
||||
* Manages multiple channel adapters with unified message routing,
|
||||
* multi-tenant isolation, and rate limiting.
|
||||
*/
|
||||
|
||||
import type {
|
||||
BaseAdapter,
|
||||
ChannelType,
|
||||
UnifiedMessage,
|
||||
MessageHandler,
|
||||
AdapterConfig,
|
||||
} from './adapters/BaseAdapter.js';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
export interface ChannelFilter {
|
||||
types?: ChannelType[];
|
||||
tenantIds?: string[];
|
||||
channelIds?: string[];
|
||||
}
|
||||
|
||||
export interface ChannelRegistryConfig {
|
||||
defaultRateLimit?: {
|
||||
requests: number;
|
||||
windowMs: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AdapterFactory {
|
||||
(config: AdapterConfig): BaseAdapter;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ChannelRegistry Implementation
|
||||
// ============================================================================
|
||||
|
||||
export class ChannelRegistry {
|
||||
private adapters: Map<string, BaseAdapter> = new Map();
|
||||
private adaptersByType: Map<ChannelType, Set<string>> = new Map();
|
||||
private adaptersByTenant: Map<string, Set<string>> = new Map();
|
||||
private globalHandlers: MessageHandler[] = [];
|
||||
private config: ChannelRegistryConfig;
|
||||
|
||||
// Rate limiting state
|
||||
private rateLimitWindows: Map<string, { count: number; resetAt: number }> = new Map();
|
||||
|
||||
constructor(config: ChannelRegistryConfig = {}) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique adapter key
|
||||
*/
|
||||
private getAdapterKey(type: ChannelType, tenantId: string): string {
|
||||
return `${type}:${tenantId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a channel adapter
|
||||
*/
|
||||
register(adapter: BaseAdapter): void {
|
||||
const key = this.getAdapterKey(adapter.type, adapter.tenantId);
|
||||
|
||||
// Store adapter
|
||||
this.adapters.set(key, adapter);
|
||||
|
||||
// Index by type
|
||||
if (!this.adaptersByType.has(adapter.type)) {
|
||||
this.adaptersByType.set(adapter.type, new Set());
|
||||
}
|
||||
this.adaptersByType.get(adapter.type)!.add(key);
|
||||
|
||||
// Index by tenant
|
||||
if (!this.adaptersByTenant.has(adapter.tenantId)) {
|
||||
this.adaptersByTenant.set(adapter.tenantId, new Set());
|
||||
}
|
||||
this.adaptersByTenant.get(adapter.tenantId)!.add(key);
|
||||
|
||||
// Register global message handler on adapter
|
||||
adapter.onMessage(async (message) => {
|
||||
await this.handleMessage(message);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a channel adapter
|
||||
*/
|
||||
unregister(type: ChannelType, tenantId: string): boolean {
|
||||
const key = this.getAdapterKey(type, tenantId);
|
||||
const adapter = this.adapters.get(key);
|
||||
|
||||
if (!adapter) return false;
|
||||
|
||||
// Remove from indices
|
||||
this.adaptersByType.get(type)?.delete(key);
|
||||
this.adaptersByTenant.get(tenantId)?.delete(key);
|
||||
|
||||
// Remove adapter
|
||||
this.adapters.delete(key);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific adapter
|
||||
*/
|
||||
get(type: ChannelType, tenantId: string): BaseAdapter | undefined {
|
||||
return this.adapters.get(this.getAdapterKey(type, tenantId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all adapters for a type
|
||||
*/
|
||||
getByType(type: ChannelType): BaseAdapter[] {
|
||||
const keys = this.adaptersByType.get(type);
|
||||
if (!keys) return [];
|
||||
|
||||
return Array.from(keys)
|
||||
.map(key => this.adapters.get(key))
|
||||
.filter((a): a is BaseAdapter => a !== undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all adapters for a tenant
|
||||
*/
|
||||
getByTenant(tenantId: string): BaseAdapter[] {
|
||||
const keys = this.adaptersByTenant.get(tenantId);
|
||||
if (!keys) return [];
|
||||
|
||||
return Array.from(keys)
|
||||
.map(key => this.adapters.get(key))
|
||||
.filter((a): a is BaseAdapter => a !== undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered adapters
|
||||
*/
|
||||
getAll(): BaseAdapter[] {
|
||||
return Array.from(this.adapters.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a global message handler
|
||||
*/
|
||||
onMessage(handler: MessageHandler): void {
|
||||
this.globalHandlers.push(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a global message handler
|
||||
*/
|
||||
offMessage(handler: MessageHandler): void {
|
||||
const index = this.globalHandlers.indexOf(handler);
|
||||
if (index > -1) {
|
||||
this.globalHandlers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start all adapters
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
const startPromises = Array.from(this.adapters.values())
|
||||
.filter(adapter => adapter.enabled)
|
||||
.map(adapter => adapter.connect());
|
||||
|
||||
await Promise.all(startPromises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all adapters
|
||||
*/
|
||||
async stop(): Promise<void> {
|
||||
const stopPromises = Array.from(this.adapters.values())
|
||||
.map(adapter => adapter.disconnect());
|
||||
|
||||
await Promise.all(stopPromises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast a message to multiple channels
|
||||
*/
|
||||
async broadcast(
|
||||
message: string,
|
||||
channelIds: string[],
|
||||
filter?: ChannelFilter
|
||||
): Promise<Map<string, string>> {
|
||||
const results = new Map<string, string>();
|
||||
const adapters = this.filterAdapters(filter);
|
||||
|
||||
for (const adapter of adapters) {
|
||||
for (const channelId of channelIds) {
|
||||
try {
|
||||
if (this.checkRateLimit(adapter)) {
|
||||
const messageId = await adapter.send(channelId, message);
|
||||
results.set(`${adapter.type}:${channelId}`, messageId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to broadcast to ${adapter.type}:${channelId}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get registry statistics
|
||||
*/
|
||||
getStats(): {
|
||||
totalAdapters: number;
|
||||
byType: Record<ChannelType, number>;
|
||||
byTenant: Record<string, number>;
|
||||
connected: number;
|
||||
totalMessages: number;
|
||||
} {
|
||||
const byType = {} as Record<ChannelType, number>;
|
||||
const byTenant = {} as Record<string, number>;
|
||||
let connected = 0;
|
||||
let totalMessages = 0;
|
||||
|
||||
for (const adapter of this.adapters.values()) {
|
||||
// By type
|
||||
byType[adapter.type] = (byType[adapter.type] ?? 0) + 1;
|
||||
|
||||
// By tenant
|
||||
byTenant[adapter.tenantId] = (byTenant[adapter.tenantId] ?? 0) + 1;
|
||||
|
||||
// Connected status
|
||||
const status = adapter.getStatus();
|
||||
if (status.connected) connected++;
|
||||
totalMessages += status.messageCount;
|
||||
}
|
||||
|
||||
return {
|
||||
totalAdapters: this.adapters.size,
|
||||
byType,
|
||||
byTenant,
|
||||
connected,
|
||||
totalMessages,
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Private Methods
|
||||
// ==========================================================================
|
||||
|
||||
private async handleMessage(message: UnifiedMessage): Promise<void> {
|
||||
for (const handler of this.globalHandlers) {
|
||||
try {
|
||||
await handler(message);
|
||||
} catch (error) {
|
||||
console.error('Global message handler error:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private filterAdapters(filter?: ChannelFilter): BaseAdapter[] {
|
||||
let adapters = Array.from(this.adapters.values());
|
||||
|
||||
if (filter?.types) {
|
||||
adapters = adapters.filter(a => filter.types!.includes(a.type));
|
||||
}
|
||||
|
||||
if (filter?.tenantIds) {
|
||||
adapters = adapters.filter(a => filter.tenantIds!.includes(a.tenantId));
|
||||
}
|
||||
|
||||
return adapters.filter(a => a.enabled);
|
||||
}
|
||||
|
||||
private checkRateLimit(adapter: BaseAdapter): boolean {
|
||||
const config = this.config.defaultRateLimit;
|
||||
if (!config) return true;
|
||||
|
||||
const key = this.getAdapterKey(adapter.type, adapter.tenantId);
|
||||
const now = Date.now();
|
||||
|
||||
let window = this.rateLimitWindows.get(key);
|
||||
if (!window || now > window.resetAt) {
|
||||
window = { count: 0, resetAt: now + config.windowMs };
|
||||
this.rateLimitWindows.set(key, window);
|
||||
}
|
||||
|
||||
if (window.count >= config.requests) {
|
||||
return false;
|
||||
}
|
||||
|
||||
window.count++;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Factory Function
|
||||
// ============================================================================
|
||||
|
||||
export function createChannelRegistry(config?: ChannelRegistryConfig): ChannelRegistry {
|
||||
return new ChannelRegistry(config);
|
||||
}
|
||||
|
||||
export default ChannelRegistry;
|
||||
120
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/BaseAdapter.d.ts
vendored
Normal file
120
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/BaseAdapter.d.ts
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* BaseAdapter - Abstract Channel Adapter
|
||||
*
|
||||
* Base class for all channel adapters providing a unified interface
|
||||
* for multi-channel messaging support.
|
||||
*/
|
||||
import type { EventEmitter } from 'events';
|
||||
export type ChannelType = 'slack' | 'discord' | 'telegram' | 'signal' | 'whatsapp' | 'line' | 'imessage' | 'web' | 'api' | 'cli';
|
||||
export interface Attachment {
|
||||
id: string;
|
||||
type: 'image' | 'file' | 'audio' | 'video' | 'link';
|
||||
url?: string;
|
||||
data?: Buffer;
|
||||
mimeType?: string;
|
||||
filename?: string;
|
||||
size?: number;
|
||||
}
|
||||
export interface UnifiedMessage {
|
||||
id: string;
|
||||
channelId: string;
|
||||
channelType: ChannelType;
|
||||
tenantId: string;
|
||||
userId: string;
|
||||
username?: string;
|
||||
content: string;
|
||||
attachments?: Attachment[];
|
||||
threadId?: string;
|
||||
replyTo?: string;
|
||||
timestamp: Date;
|
||||
metadata: Record<string, unknown>;
|
||||
}
|
||||
export interface SendOptions {
|
||||
threadId?: string;
|
||||
replyTo?: string;
|
||||
attachments?: Attachment[];
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
export interface ChannelCredentials {
|
||||
token?: string;
|
||||
apiKey?: string;
|
||||
webhookUrl?: string;
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
botId?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
export interface AdapterConfig {
|
||||
type: ChannelType;
|
||||
tenantId: string;
|
||||
credentials: ChannelCredentials;
|
||||
enabled?: boolean;
|
||||
rateLimit?: {
|
||||
requests: number;
|
||||
windowMs: number;
|
||||
};
|
||||
}
|
||||
export interface AdapterStatus {
|
||||
connected: boolean;
|
||||
lastActivity?: Date;
|
||||
errorCount: number;
|
||||
messageCount: number;
|
||||
}
|
||||
export type MessageHandler = (message: UnifiedMessage) => Promise<void>;
|
||||
export declare abstract class BaseAdapter {
|
||||
protected readonly config: AdapterConfig;
|
||||
protected status: AdapterStatus;
|
||||
protected messageHandlers: MessageHandler[];
|
||||
protected eventEmitter?: EventEmitter;
|
||||
constructor(config: AdapterConfig);
|
||||
/**
|
||||
* Get channel type
|
||||
*/
|
||||
get type(): ChannelType;
|
||||
/**
|
||||
* Get tenant ID
|
||||
*/
|
||||
get tenantId(): string;
|
||||
/**
|
||||
* Check if adapter is enabled
|
||||
*/
|
||||
get enabled(): boolean;
|
||||
/**
|
||||
* Get adapter status
|
||||
*/
|
||||
getStatus(): AdapterStatus;
|
||||
/**
|
||||
* Register a message handler
|
||||
*/
|
||||
onMessage(handler: MessageHandler): void;
|
||||
/**
|
||||
* Remove a message handler
|
||||
*/
|
||||
offMessage(handler: MessageHandler): void;
|
||||
/**
|
||||
* Emit a received message to all handlers
|
||||
*/
|
||||
protected emitMessage(message: UnifiedMessage): Promise<void>;
|
||||
/**
|
||||
* Create a unified message from raw input
|
||||
*/
|
||||
protected createUnifiedMessage(content: string, userId: string, channelId: string, extra?: Partial<UnifiedMessage>): UnifiedMessage;
|
||||
/**
|
||||
* Connect to the channel
|
||||
*/
|
||||
abstract connect(): Promise<void>;
|
||||
/**
|
||||
* Disconnect from the channel
|
||||
*/
|
||||
abstract disconnect(): Promise<void>;
|
||||
/**
|
||||
* Send a message to the channel
|
||||
*/
|
||||
abstract send(channelId: string, content: string, options?: SendOptions): Promise<string>;
|
||||
/**
|
||||
* Reply to a message
|
||||
*/
|
||||
abstract reply(message: UnifiedMessage, content: string, options?: SendOptions): Promise<string>;
|
||||
}
|
||||
export default BaseAdapter;
|
||||
//# sourceMappingURL=BaseAdapter.d.ts.map
|
||||
1
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/BaseAdapter.d.ts.map
vendored
Normal file
1
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/BaseAdapter.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"BaseAdapter.d.ts","sourceRoot":"","sources":["BaseAdapter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAM3C,MAAM,MAAM,WAAW,GACnB,OAAO,GACP,SAAS,GACT,UAAU,GACV,QAAQ,GACR,UAAU,GACV,MAAM,GACN,UAAU,GACV,KAAK,GACL,KAAK,GACL,KAAK,CAAC;AAEV,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC;IACpD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,WAAW,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,IAAI,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,kBAAkB,CAAC;IAChC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE;QACV,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,IAAI,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB;AAMD,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAMxE,8BAAsB,WAAW;IAC/B,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IACzC,SAAS,CAAC,MAAM,EAAE,aAAa,CAAC;IAChC,SAAS,CAAC,eAAe,EAAE,cAAc,EAAE,CAAM;IACjD,SAAS,CAAC,YAAY,CAAC,EAAE,YAAY,CAAC;gBAE1B,MAAM,EAAE,aAAa;IAYjC;;OAEG;IACH,IAAI,IAAI,IAAI,WAAW,CAEtB;IAED;;OAEG;IACH,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED;;OAEG;IACH,IAAI,OAAO,IAAI,OAAO,CAErB;IAED;;OAEG;IACH,SAAS,IAAI,aAAa;IAI1B;;OAEG;IACH,SAAS,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI;IAIxC;;OAEG;IACH,UAAU,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI;IAOzC;;OAEG;cACa,WAAW,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAcnE;;OAEG;IACH,SAAS,CAAC,oBAAoB,CAC5B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,KAAK,GAAE,OAAO,CAAC,cAAc,CAAM,GAClC,cAAc;IAkBjB;;OAEG;IACH,QAAQ,CAAC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAEjC;;OAEG;IACH,QAAQ,CAAC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAEpC;;OAEG;IACH,QAAQ,CAAC,IAAI,CACX,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC,MAAM,CAAC;IAElB;;OAEG;IACH,QAAQ,CAAC,KAAK,CACZ,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC,MAAM,CAAC;CACnB;AAED,eAAe,WAAW,CAAC"}
|
||||
101
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/BaseAdapter.js
vendored
Normal file
101
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/BaseAdapter.js
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
"use strict";
|
||||
/**
|
||||
* BaseAdapter - Abstract Channel Adapter
|
||||
*
|
||||
* Base class for all channel adapters providing a unified interface
|
||||
* for multi-channel messaging support.
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.BaseAdapter = void 0;
|
||||
const uuid_1 = require("uuid");
|
||||
// ============================================================================
|
||||
// BaseAdapter Abstract Class
|
||||
// ============================================================================
|
||||
class BaseAdapter {
|
||||
constructor(config) {
|
||||
this.messageHandlers = [];
|
||||
this.config = {
|
||||
...config,
|
||||
enabled: config.enabled ?? true,
|
||||
};
|
||||
this.status = {
|
||||
connected: false,
|
||||
errorCount: 0,
|
||||
messageCount: 0,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Get channel type
|
||||
*/
|
||||
get type() {
|
||||
return this.config.type;
|
||||
}
|
||||
/**
|
||||
* Get tenant ID
|
||||
*/
|
||||
get tenantId() {
|
||||
return this.config.tenantId;
|
||||
}
|
||||
/**
|
||||
* Check if adapter is enabled
|
||||
*/
|
||||
get enabled() {
|
||||
return this.config.enabled ?? true;
|
||||
}
|
||||
/**
|
||||
* Get adapter status
|
||||
*/
|
||||
getStatus() {
|
||||
return { ...this.status };
|
||||
}
|
||||
/**
|
||||
* Register a message handler
|
||||
*/
|
||||
onMessage(handler) {
|
||||
this.messageHandlers.push(handler);
|
||||
}
|
||||
/**
|
||||
* Remove a message handler
|
||||
*/
|
||||
offMessage(handler) {
|
||||
const index = this.messageHandlers.indexOf(handler);
|
||||
if (index > -1) {
|
||||
this.messageHandlers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Emit a received message to all handlers
|
||||
*/
|
||||
async emitMessage(message) {
|
||||
this.status.messageCount++;
|
||||
this.status.lastActivity = new Date();
|
||||
for (const handler of this.messageHandlers) {
|
||||
try {
|
||||
await handler(message);
|
||||
}
|
||||
catch (error) {
|
||||
this.status.errorCount++;
|
||||
console.error(`Message handler error in ${this.type}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Create a unified message from raw input
|
||||
*/
|
||||
createUnifiedMessage(content, userId, channelId, extra = {}) {
|
||||
return {
|
||||
id: (0, uuid_1.v4)(),
|
||||
channelId,
|
||||
channelType: this.config.type,
|
||||
tenantId: this.config.tenantId,
|
||||
userId,
|
||||
content,
|
||||
timestamp: new Date(),
|
||||
metadata: {},
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
}
|
||||
exports.BaseAdapter = BaseAdapter;
|
||||
exports.default = BaseAdapter;
|
||||
//# sourceMappingURL=BaseAdapter.js.map
|
||||
1
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/BaseAdapter.js.map
vendored
Normal file
1
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/BaseAdapter.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"BaseAdapter.js","sourceRoot":"","sources":["BaseAdapter.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAEH,+BAAoC;AAqFpC,+EAA+E;AAC/E,6BAA6B;AAC7B,+EAA+E;AAE/E,MAAsB,WAAW;IAM/B,YAAY,MAAqB;QAHvB,oBAAe,GAAqB,EAAE,CAAC;QAI/C,IAAI,CAAC,MAAM,GAAG;YACZ,GAAG,MAAM;YACT,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,IAAI;SAChC,CAAC;QACF,IAAI,CAAC,MAAM,GAAG;YACZ,SAAS,EAAE,KAAK;YAChB,UAAU,EAAE,CAAC;YACb,YAAY,EAAE,CAAC;SAChB,CAAC;IACJ,CAAC;IAED;;OAEG;IACH,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;IAC1B,CAAC;IAED;;OAEG;IACH,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC;IAC9B,CAAC;IAED;;OAEG;IACH,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,IAAI,CAAC;IACrC,CAAC;IAED;;OAEG;IACH,SAAS;QACP,OAAO,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;IAC5B,CAAC;IAED;;OAEG;IACH,SAAS,CAAC,OAAuB;QAC/B,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACrC,CAAC;IAED;;OAEG;IACH,UAAU,CAAC,OAAuB;QAChC,MAAM,KAAK,GAAG,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACpD,IAAI,KAAK,GAAG,CAAC,CAAC,EAAE,CAAC;YACf,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QACxC,CAAC;IACH,CAAC;IAED;;OAEG;IACO,KAAK,CAAC,WAAW,CAAC,OAAuB;QACjD,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC;QAC3B,IAAI,CAAC,MAAM,CAAC,YAAY,GAAG,IAAI,IAAI,EAAE,CAAC;QAEtC,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YAC3C,IAAI,CAAC;gBACH,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;YACzB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;gBACzB,OAAO,CAAC,KAAK,CAAC,4BAA4B,IAAI,CAAC,IAAI,GAAG,EAAE,KAAK,CAAC,CAAC;YACjE,CAAC;QACH,CAAC;IACH,CAAC;IAED;;OAEG;IACO,oBAAoB,CAC5B,OAAe,EACf,MAAc,EACd,SAAiB,EACjB,QAAiC,EAAE;QAEnC,OAAO;YACL,EAAE,EAAE,IAAA,SAAM,GAAE;YACZ,SAAS;YACT,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI;YAC7B,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ;YAC9B,MAAM;YACN,OAAO;YACP,SAAS,EAAE,IAAI,IAAI,EAAE;YACrB,QAAQ,EAAE,EAAE;YACZ,GAAG,KAAK;SACT,CAAC;IACJ,CAAC;CAiCF;AArID,kCAqIC;AAED,kBAAe,WAAW,CAAC"}
|
||||
232
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/BaseAdapter.ts
vendored
Normal file
232
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/BaseAdapter.ts
vendored
Normal file
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* BaseAdapter - Abstract Channel Adapter
|
||||
*
|
||||
* Base class for all channel adapters providing a unified interface
|
||||
* for multi-channel messaging support.
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import type { EventEmitter } from 'events';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
export type ChannelType =
|
||||
| 'slack'
|
||||
| 'discord'
|
||||
| 'telegram'
|
||||
| 'signal'
|
||||
| 'whatsapp'
|
||||
| 'line'
|
||||
| 'imessage'
|
||||
| 'web'
|
||||
| 'api'
|
||||
| 'cli';
|
||||
|
||||
export interface Attachment {
|
||||
id: string;
|
||||
type: 'image' | 'file' | 'audio' | 'video' | 'link';
|
||||
url?: string;
|
||||
data?: Buffer;
|
||||
mimeType?: string;
|
||||
filename?: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export interface UnifiedMessage {
|
||||
id: string;
|
||||
channelId: string;
|
||||
channelType: ChannelType;
|
||||
tenantId: string;
|
||||
userId: string;
|
||||
username?: string;
|
||||
content: string;
|
||||
attachments?: Attachment[];
|
||||
threadId?: string;
|
||||
replyTo?: string;
|
||||
timestamp: Date;
|
||||
metadata: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface SendOptions {
|
||||
threadId?: string;
|
||||
replyTo?: string;
|
||||
attachments?: Attachment[];
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ChannelCredentials {
|
||||
token?: string;
|
||||
apiKey?: string;
|
||||
webhookUrl?: string;
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
botId?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface AdapterConfig {
|
||||
type: ChannelType;
|
||||
tenantId: string;
|
||||
credentials: ChannelCredentials;
|
||||
enabled?: boolean;
|
||||
rateLimit?: {
|
||||
requests: number;
|
||||
windowMs: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AdapterStatus {
|
||||
connected: boolean;
|
||||
lastActivity?: Date;
|
||||
errorCount: number;
|
||||
messageCount: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Message Handler Type
|
||||
// ============================================================================
|
||||
|
||||
export type MessageHandler = (message: UnifiedMessage) => Promise<void>;
|
||||
|
||||
// ============================================================================
|
||||
// BaseAdapter Abstract Class
|
||||
// ============================================================================
|
||||
|
||||
export abstract class BaseAdapter {
|
||||
protected readonly config: AdapterConfig;
|
||||
protected status: AdapterStatus;
|
||||
protected messageHandlers: MessageHandler[] = [];
|
||||
protected eventEmitter?: EventEmitter;
|
||||
|
||||
constructor(config: AdapterConfig) {
|
||||
this.config = {
|
||||
...config,
|
||||
enabled: config.enabled ?? true,
|
||||
};
|
||||
this.status = {
|
||||
connected: false,
|
||||
errorCount: 0,
|
||||
messageCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get channel type
|
||||
*/
|
||||
get type(): ChannelType {
|
||||
return this.config.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tenant ID
|
||||
*/
|
||||
get tenantId(): string {
|
||||
return this.config.tenantId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if adapter is enabled
|
||||
*/
|
||||
get enabled(): boolean {
|
||||
return this.config.enabled ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get adapter status
|
||||
*/
|
||||
getStatus(): AdapterStatus {
|
||||
return { ...this.status };
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a message handler
|
||||
*/
|
||||
onMessage(handler: MessageHandler): void {
|
||||
this.messageHandlers.push(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a message handler
|
||||
*/
|
||||
offMessage(handler: MessageHandler): void {
|
||||
const index = this.messageHandlers.indexOf(handler);
|
||||
if (index > -1) {
|
||||
this.messageHandlers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a received message to all handlers
|
||||
*/
|
||||
protected async emitMessage(message: UnifiedMessage): Promise<void> {
|
||||
this.status.messageCount++;
|
||||
this.status.lastActivity = new Date();
|
||||
|
||||
for (const handler of this.messageHandlers) {
|
||||
try {
|
||||
await handler(message);
|
||||
} catch (error) {
|
||||
this.status.errorCount++;
|
||||
console.error(`Message handler error in ${this.type}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a unified message from raw input
|
||||
*/
|
||||
protected createUnifiedMessage(
|
||||
content: string,
|
||||
userId: string,
|
||||
channelId: string,
|
||||
extra: Partial<UnifiedMessage> = {}
|
||||
): UnifiedMessage {
|
||||
return {
|
||||
id: uuidv4(),
|
||||
channelId,
|
||||
channelType: this.config.type,
|
||||
tenantId: this.config.tenantId,
|
||||
userId,
|
||||
content,
|
||||
timestamp: new Date(),
|
||||
metadata: {},
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Abstract Methods (must be implemented by subclasses)
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* Connect to the channel
|
||||
*/
|
||||
abstract connect(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Disconnect from the channel
|
||||
*/
|
||||
abstract disconnect(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Send a message to the channel
|
||||
*/
|
||||
abstract send(
|
||||
channelId: string,
|
||||
content: string,
|
||||
options?: SendOptions
|
||||
): Promise<string>;
|
||||
|
||||
/**
|
||||
* Reply to a message
|
||||
*/
|
||||
abstract reply(
|
||||
message: UnifiedMessage,
|
||||
content: string,
|
||||
options?: SendOptions
|
||||
): Promise<string>;
|
||||
}
|
||||
|
||||
export default BaseAdapter;
|
||||
67
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/DiscordAdapter.d.ts
vendored
Normal file
67
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/DiscordAdapter.d.ts
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* DiscordAdapter - Discord Channel Integration
|
||||
*
|
||||
* Connects to Discord servers using discord.js for real-time messaging.
|
||||
* Supports threads, embeds, reactions, and slash commands.
|
||||
*/
|
||||
import { BaseAdapter, type AdapterConfig, type UnifiedMessage, type SendOptions } from './BaseAdapter.js';
|
||||
export interface DiscordCredentials {
|
||||
token: string;
|
||||
clientId?: string;
|
||||
guildId?: string;
|
||||
intents?: number[];
|
||||
}
|
||||
export interface DiscordMessage {
|
||||
id: string;
|
||||
channelId: string;
|
||||
guildId?: string;
|
||||
author: {
|
||||
id: string;
|
||||
username: string;
|
||||
discriminator: string;
|
||||
};
|
||||
content: string;
|
||||
timestamp: Date;
|
||||
reference?: {
|
||||
messageId: string;
|
||||
};
|
||||
attachments: Map<string, DiscordAttachment>;
|
||||
}
|
||||
export interface DiscordAttachment {
|
||||
id: string;
|
||||
filename: string;
|
||||
contentType?: string;
|
||||
url: string;
|
||||
size: number;
|
||||
}
|
||||
export declare class DiscordAdapter extends BaseAdapter {
|
||||
private client;
|
||||
constructor(config: Omit<AdapterConfig, 'type'> & {
|
||||
credentials: DiscordCredentials;
|
||||
});
|
||||
/**
|
||||
* Connect to Discord
|
||||
*/
|
||||
connect(): Promise<void>;
|
||||
/**
|
||||
* Disconnect from Discord
|
||||
*/
|
||||
disconnect(): Promise<void>;
|
||||
/**
|
||||
* Send a message to a Discord channel
|
||||
*/
|
||||
send(channelId: string, content: string, options?: SendOptions): Promise<string>;
|
||||
/**
|
||||
* Reply to a Discord message
|
||||
*/
|
||||
reply(message: UnifiedMessage, content: string, options?: SendOptions): Promise<string>;
|
||||
private loadDiscordJs;
|
||||
private getChannel;
|
||||
private discordToUnified;
|
||||
private getMimeCategory;
|
||||
}
|
||||
export declare function createDiscordAdapter(config: Omit<AdapterConfig, 'type'> & {
|
||||
credentials: DiscordCredentials;
|
||||
}): DiscordAdapter;
|
||||
export default DiscordAdapter;
|
||||
//# sourceMappingURL=DiscordAdapter.d.ts.map
|
||||
1
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/DiscordAdapter.d.ts.map
vendored
Normal file
1
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/DiscordAdapter.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"DiscordAdapter.d.ts","sourceRoot":"","sources":["DiscordAdapter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,WAAW,EACX,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,WAAW,EAEjB,MAAM,kBAAkB,CAAC;AAM1B,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE;QACN,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,aAAa,EAAE,MAAM,CAAC;KACvB,CAAC;IACF,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,CAAC,EAAE;QACV,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;CAC7C;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAMD,qBAAa,cAAe,SAAQ,WAAW;IAC7C,OAAO,CAAC,MAAM,CAAiB;gBAEnB,MAAM,EAAE,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC,GAAG;QAAE,WAAW,EAAE,kBAAkB,CAAA;KAAE;IAIrF;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IA2C9B;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAQjC;;OAEG;IACG,IAAI,CACR,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC,MAAM,CAAC;IAwBlB;;OAEG;IACG,KAAK,CACT,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC,MAAM,CAAC;YAYJ,aAAa;YAUb,UAAU;IASxB,OAAO,CAAC,gBAAgB;IA8BxB,OAAO,CAAC,eAAe;CAMxB;AAMD,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC,GAAG;IAAE,WAAW,EAAE,kBAAkB,CAAA;CAAE,GACxE,cAAc,CAEhB;AAED,eAAe,cAAc,CAAC"}
|
||||
197
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/DiscordAdapter.js
vendored
Normal file
197
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/DiscordAdapter.js
vendored
Normal file
@@ -0,0 +1,197 @@
|
||||
"use strict";
|
||||
/**
|
||||
* DiscordAdapter - Discord Channel Integration
|
||||
*
|
||||
* Connects to Discord servers using discord.js for real-time messaging.
|
||||
* Supports threads, embeds, reactions, and slash commands.
|
||||
*/
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.DiscordAdapter = void 0;
|
||||
exports.createDiscordAdapter = createDiscordAdapter;
|
||||
const BaseAdapter_js_1 = require("./BaseAdapter.js");
|
||||
// ============================================================================
|
||||
// DiscordAdapter Implementation
|
||||
// ============================================================================
|
||||
class DiscordAdapter extends BaseAdapter_js_1.BaseAdapter {
|
||||
constructor(config) {
|
||||
super({ ...config, type: 'discord' });
|
||||
this.client = null;
|
||||
}
|
||||
/**
|
||||
* Connect to Discord
|
||||
*/
|
||||
async connect() {
|
||||
const credentials = this.config.credentials;
|
||||
try {
|
||||
// Dynamic import to avoid requiring discord.js if not used
|
||||
const discordModule = await this.loadDiscordJs();
|
||||
if (discordModule) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const Client = discordModule.Client;
|
||||
const GatewayIntentBits = discordModule.GatewayIntentBits;
|
||||
this.client = new Client({
|
||||
intents: credentials.intents ?? [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.MessageContent,
|
||||
GatewayIntentBits.DirectMessages,
|
||||
],
|
||||
});
|
||||
// Register message handler
|
||||
this.client.on('messageCreate', (message) => {
|
||||
// Ignore bot messages
|
||||
if (message.author.bot)
|
||||
return;
|
||||
const unified = this.discordToUnified(message);
|
||||
this.emitMessage(unified);
|
||||
});
|
||||
// Login
|
||||
await this.client.login(credentials.token);
|
||||
this.status.connected = true;
|
||||
}
|
||||
else {
|
||||
console.warn('DiscordAdapter: discord.js not available, running in mock mode');
|
||||
this.status.connected = true;
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
this.status.errorCount++;
|
||||
throw new Error(`Failed to connect to Discord: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Disconnect from Discord
|
||||
*/
|
||||
async disconnect() {
|
||||
if (this.client) {
|
||||
await this.client.destroy?.();
|
||||
this.client = null;
|
||||
}
|
||||
this.status.connected = false;
|
||||
}
|
||||
/**
|
||||
* Send a message to a Discord channel
|
||||
*/
|
||||
async send(channelId, content, options) {
|
||||
if (!this.client) {
|
||||
throw new Error('DiscordAdapter not connected');
|
||||
}
|
||||
try {
|
||||
const channel = await this.getChannel(channelId);
|
||||
const sendOptions = { content };
|
||||
if (options?.replyTo) {
|
||||
sendOptions.reply = { messageReference: options.replyTo };
|
||||
}
|
||||
const result = await channel.send(sendOptions);
|
||||
this.status.messageCount++;
|
||||
return result.id;
|
||||
}
|
||||
catch (error) {
|
||||
this.status.errorCount++;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Reply to a Discord message
|
||||
*/
|
||||
async reply(message, content, options) {
|
||||
return this.send(message.channelId, content, {
|
||||
...options,
|
||||
replyTo: message.id,
|
||||
});
|
||||
}
|
||||
// ==========================================================================
|
||||
// Private Methods
|
||||
// ==========================================================================
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async loadDiscordJs() {
|
||||
try {
|
||||
// Dynamic import - discord.js is optional
|
||||
// @ts-expect-error - discord.js may not be installed
|
||||
return await Promise.resolve().then(() => __importStar(require('discord.js'))).catch(() => null);
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
async getChannel(channelId) {
|
||||
if (!this.client) {
|
||||
throw new Error('Client not connected');
|
||||
}
|
||||
const channels = this.client.channels;
|
||||
return channels.fetch(channelId);
|
||||
}
|
||||
discordToUnified(message) {
|
||||
const attachments = [];
|
||||
message.attachments.forEach((attachment) => {
|
||||
attachments.push({
|
||||
id: attachment.id,
|
||||
type: this.getMimeCategory(attachment.contentType ?? ''),
|
||||
url: attachment.url,
|
||||
mimeType: attachment.contentType,
|
||||
filename: attachment.filename,
|
||||
size: attachment.size,
|
||||
});
|
||||
});
|
||||
return this.createUnifiedMessage(message.content, message.author.id, message.channelId, {
|
||||
username: `${message.author.username}#${message.author.discriminator}`,
|
||||
replyTo: message.reference?.messageId,
|
||||
attachments: attachments.length > 0 ? attachments : undefined,
|
||||
metadata: {
|
||||
guildId: message.guildId,
|
||||
originalId: message.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
getMimeCategory(mimeType) {
|
||||
if (mimeType.startsWith('image/'))
|
||||
return 'image';
|
||||
if (mimeType.startsWith('audio/'))
|
||||
return 'audio';
|
||||
if (mimeType.startsWith('video/'))
|
||||
return 'video';
|
||||
return 'file';
|
||||
}
|
||||
}
|
||||
exports.DiscordAdapter = DiscordAdapter;
|
||||
// ============================================================================
|
||||
// Factory Function
|
||||
// ============================================================================
|
||||
function createDiscordAdapter(config) {
|
||||
return new DiscordAdapter(config);
|
||||
}
|
||||
exports.default = DiscordAdapter;
|
||||
//# sourceMappingURL=DiscordAdapter.js.map
|
||||
1
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/DiscordAdapter.js.map
vendored
Normal file
1
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/DiscordAdapter.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"DiscordAdapter.js","sourceRoot":"","sources":["DiscordAdapter.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiOH,oDAIC;AAnOD,qDAM0B;AAsC1B,+EAA+E;AAC/E,gCAAgC;AAChC,+EAA+E;AAE/E,MAAa,cAAe,SAAQ,4BAAW;IAG7C,YAAY,MAAyE;QACnF,KAAK,CAAC,EAAE,GAAG,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC;QAHhC,WAAM,GAAY,IAAI,CAAC;IAI/B,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,OAAO;QACX,MAAM,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,WAA4C,CAAC;QAE7E,IAAI,CAAC;YACH,2DAA2D;YAC3D,MAAM,aAAa,GAAG,MAAM,IAAI,CAAC,aAAa,EAAE,CAAC;YAEjD,IAAI,aAAa,EAAE,CAAC;gBAClB,8DAA8D;gBAC9D,MAAM,MAAM,GAAG,aAAa,CAAC,MAAa,CAAC;gBAC3C,MAAM,iBAAiB,GAAG,aAAa,CAAC,iBAA2C,CAAC;gBAEpF,IAAI,CAAC,MAAM,GAAG,IAAI,MAAM,CAAC;oBACvB,OAAO,EAAE,WAAW,CAAC,OAAO,IAAI;wBAC9B,iBAAiB,CAAC,MAAM;wBACxB,iBAAiB,CAAC,aAAa;wBAC/B,iBAAiB,CAAC,cAAc;wBAChC,iBAAiB,CAAC,cAAc;qBACjC;iBACF,CAAC,CAAC;gBAEH,2BAA2B;gBAC1B,IAAI,CAAC,MAAsF,CAAC,EAAE,CAAC,eAAe,EAAE,CAAC,OAAuB,EAAE,EAAE;oBAC3I,sBAAsB;oBACtB,IAAK,OAAoD,CAAC,MAAM,CAAC,GAAG;wBAAE,OAAO;oBAE7E,MAAM,OAAO,GAAG,IAAI,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC;oBAC/C,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;gBAC5B,CAAC,CAAC,CAAC;gBAEH,QAAQ;gBACR,MAAO,IAAI,CAAC,MAAsD,CAAC,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;gBAC5F,IAAI,CAAC,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC;YAC/B,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,IAAI,CAAC,gEAAgE,CAAC,CAAC;gBAC/E,IAAI,CAAC,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC;YAC/B,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CAAC,iCAAiC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,EAAE,CAAC,CAAC;QAC/G,CAAC;IACH,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,UAAU;QACd,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,MAAO,IAAI,CAAC,MAA4C,CAAC,OAAO,EAAE,EAAE,CAAC;YACrE,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACrB,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,SAAS,GAAG,KAAK,CAAC;IAChC,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,IAAI,CACR,SAAiB,EACjB,OAAe,EACf,OAAqB;QAErB,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;QAClD,CAAC;QAED,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;YAEjD,MAAM,WAAW,GAA4B,EAAE,OAAO,EAAE,CAAC;YAEzD,IAAI,OAAO,EAAE,OAAO,EAAE,CAAC;gBACrB,WAAW,CAAC,KAAK,GAAG,EAAE,gBAAgB,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC;YAC5D,CAAC;YAED,MAAM,MAAM,GAAG,MAAO,OAAgE,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;YAEzG,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC;YAC3B,OAAO,MAAM,CAAC,EAAE,CAAC;QACnB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;YACzB,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,KAAK,CACT,OAAuB,EACvB,OAAe,EACf,OAAqB;QAErB,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,EAAE;YAC3C,GAAG,OAAO;YACV,OAAO,EAAE,OAAO,CAAC,EAAE;SACpB,CAAC,CAAC;IACL,CAAC;IAED,6EAA6E;IAC7E,kBAAkB;IAClB,6EAA6E;IAE7E,8DAA8D;IACtD,KAAK,CAAC,aAAa;QACzB,IAAI,CAAC;YACH,0CAA0C;YAC1C,qDAAqD;YACrD,OAAO,MAAM,kDAAO,YAAY,IAAE,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;QACtD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,UAAU,CAAC,SAAiB;QACxC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAC;QAC1C,CAAC;QAED,MAAM,QAAQ,GAAI,IAAI,CAAC,MAAoE,CAAC,QAAQ,CAAC;QACrG,OAAO,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IACnC,CAAC;IAEO,gBAAgB,CAAC,OAAuB;QAC9C,MAAM,WAAW,GAAiB,EAAE,CAAC;QAErC,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,UAAU,EAAE,EAAE;YACzC,WAAW,CAAC,IAAI,CAAC;gBACf,EAAE,EAAE,UAAU,CAAC,EAAE;gBACjB,IAAI,EAAE,IAAI,CAAC,eAAe,CAAC,UAAU,CAAC,WAAW,IAAI,EAAE,CAAC;gBACxD,GAAG,EAAE,UAAU,CAAC,GAAG;gBACnB,QAAQ,EAAE,UAAU,CAAC,WAAW;gBAChC,QAAQ,EAAE,UAAU,CAAC,QAAQ;gBAC7B,IAAI,EAAE,UAAU,CAAC,IAAI;aACtB,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,OAAO,IAAI,CAAC,oBAAoB,CAC9B,OAAO,CAAC,OAAO,EACf,OAAO,CAAC,MAAM,CAAC,EAAE,EACjB,OAAO,CAAC,SAAS,EACjB;YACE,QAAQ,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE;YACtE,OAAO,EAAE,OAAO,CAAC,SAAS,EAAE,SAAS;YACrC,WAAW,EAAE,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;YAC7D,QAAQ,EAAE;gBACR,OAAO,EAAE,OAAO,CAAC,OAAO;gBACxB,UAAU,EAAE,OAAO,CAAC,EAAE;aACvB;SACF,CACF,CAAC;IACJ,CAAC;IAEO,eAAe,CAAC,QAAgB;QACtC,IAAI,QAAQ,CAAC,UAAU,CAAC,QAAQ,CAAC;YAAE,OAAO,OAAO,CAAC;QAClD,IAAI,QAAQ,CAAC,UAAU,CAAC,QAAQ,CAAC;YAAE,OAAO,OAAO,CAAC;QAClD,IAAI,QAAQ,CAAC,UAAU,CAAC,QAAQ,CAAC;YAAE,OAAO,OAAO,CAAC;QAClD,OAAO,MAAM,CAAC;IAChB,CAAC;CACF;AAzKD,wCAyKC;AAED,+EAA+E;AAC/E,mBAAmB;AACnB,+EAA+E;AAE/E,SAAgB,oBAAoB,CAClC,MAAyE;IAEzE,OAAO,IAAI,cAAc,CAAC,MAAM,CAAC,CAAC;AACpC,CAAC;AAED,kBAAe,cAAc,CAAC"}
|
||||
237
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/DiscordAdapter.ts
vendored
Normal file
237
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/DiscordAdapter.ts
vendored
Normal file
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* DiscordAdapter - Discord Channel Integration
|
||||
*
|
||||
* Connects to Discord servers using discord.js for real-time messaging.
|
||||
* Supports threads, embeds, reactions, and slash commands.
|
||||
*/
|
||||
|
||||
import {
|
||||
BaseAdapter,
|
||||
type AdapterConfig,
|
||||
type UnifiedMessage,
|
||||
type SendOptions,
|
||||
type Attachment,
|
||||
} from './BaseAdapter.js';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
export interface DiscordCredentials {
|
||||
token: string; // Bot Token
|
||||
clientId?: string; // Application Client ID
|
||||
guildId?: string; // Optional: Specific guild to connect to
|
||||
intents?: number[]; // Discord intents
|
||||
}
|
||||
|
||||
export interface DiscordMessage {
|
||||
id: string;
|
||||
channelId: string;
|
||||
guildId?: string;
|
||||
author: {
|
||||
id: string;
|
||||
username: string;
|
||||
discriminator: string;
|
||||
};
|
||||
content: string;
|
||||
timestamp: Date;
|
||||
reference?: {
|
||||
messageId: string;
|
||||
};
|
||||
attachments: Map<string, DiscordAttachment>;
|
||||
}
|
||||
|
||||
export interface DiscordAttachment {
|
||||
id: string;
|
||||
filename: string;
|
||||
contentType?: string;
|
||||
url: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DiscordAdapter Implementation
|
||||
// ============================================================================
|
||||
|
||||
export class DiscordAdapter extends BaseAdapter {
|
||||
private client: unknown = null;
|
||||
|
||||
constructor(config: Omit<AdapterConfig, 'type'> & { credentials: DiscordCredentials }) {
|
||||
super({ ...config, type: 'discord' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to Discord
|
||||
*/
|
||||
async connect(): Promise<void> {
|
||||
const credentials = this.config.credentials as unknown as DiscordCredentials;
|
||||
|
||||
try {
|
||||
// Dynamic import to avoid requiring discord.js if not used
|
||||
const discordModule = await this.loadDiscordJs();
|
||||
|
||||
if (discordModule) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const Client = discordModule.Client as any;
|
||||
const GatewayIntentBits = discordModule.GatewayIntentBits as Record<string, number>;
|
||||
|
||||
this.client = new Client({
|
||||
intents: credentials.intents ?? [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.MessageContent,
|
||||
GatewayIntentBits.DirectMessages,
|
||||
],
|
||||
});
|
||||
|
||||
// Register message handler
|
||||
(this.client as { on: (event: string, handler: (message: DiscordMessage) => void) => void }).on('messageCreate', (message: DiscordMessage) => {
|
||||
// Ignore bot messages
|
||||
if ((message as unknown as { author: { bot?: boolean } }).author.bot) return;
|
||||
|
||||
const unified = this.discordToUnified(message);
|
||||
this.emitMessage(unified);
|
||||
});
|
||||
|
||||
// Login
|
||||
await (this.client as { login: (token: string) => Promise<void> }).login(credentials.token);
|
||||
this.status.connected = true;
|
||||
} else {
|
||||
console.warn('DiscordAdapter: discord.js not available, running in mock mode');
|
||||
this.status.connected = true;
|
||||
}
|
||||
} catch (error) {
|
||||
this.status.errorCount++;
|
||||
throw new Error(`Failed to connect to Discord: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from Discord
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
if (this.client) {
|
||||
await (this.client as { destroy?: () => Promise<void> }).destroy?.();
|
||||
this.client = null;
|
||||
}
|
||||
this.status.connected = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to a Discord channel
|
||||
*/
|
||||
async send(
|
||||
channelId: string,
|
||||
content: string,
|
||||
options?: SendOptions
|
||||
): Promise<string> {
|
||||
if (!this.client) {
|
||||
throw new Error('DiscordAdapter not connected');
|
||||
}
|
||||
|
||||
try {
|
||||
const channel = await this.getChannel(channelId);
|
||||
|
||||
const sendOptions: Record<string, unknown> = { content };
|
||||
|
||||
if (options?.replyTo) {
|
||||
sendOptions.reply = { messageReference: options.replyTo };
|
||||
}
|
||||
|
||||
const result = await (channel as { send: (opts: unknown) => Promise<{ id: string }> }).send(sendOptions);
|
||||
|
||||
this.status.messageCount++;
|
||||
return result.id;
|
||||
} catch (error) {
|
||||
this.status.errorCount++;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reply to a Discord message
|
||||
*/
|
||||
async reply(
|
||||
message: UnifiedMessage,
|
||||
content: string,
|
||||
options?: SendOptions
|
||||
): Promise<string> {
|
||||
return this.send(message.channelId, content, {
|
||||
...options,
|
||||
replyTo: message.id,
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Private Methods
|
||||
// ==========================================================================
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private async loadDiscordJs(): Promise<any | null> {
|
||||
try {
|
||||
// Dynamic import - discord.js is optional
|
||||
// @ts-expect-error - discord.js may not be installed
|
||||
return await import('discord.js').catch(() => null);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async getChannel(channelId: string): Promise<unknown> {
|
||||
if (!this.client) {
|
||||
throw new Error('Client not connected');
|
||||
}
|
||||
|
||||
const channels = (this.client as { channels: { fetch: (id: string) => Promise<unknown> } }).channels;
|
||||
return channels.fetch(channelId);
|
||||
}
|
||||
|
||||
private discordToUnified(message: DiscordMessage): UnifiedMessage {
|
||||
const attachments: Attachment[] = [];
|
||||
|
||||
message.attachments.forEach((attachment) => {
|
||||
attachments.push({
|
||||
id: attachment.id,
|
||||
type: this.getMimeCategory(attachment.contentType ?? ''),
|
||||
url: attachment.url,
|
||||
mimeType: attachment.contentType,
|
||||
filename: attachment.filename,
|
||||
size: attachment.size,
|
||||
});
|
||||
});
|
||||
|
||||
return this.createUnifiedMessage(
|
||||
message.content,
|
||||
message.author.id,
|
||||
message.channelId,
|
||||
{
|
||||
username: `${message.author.username}#${message.author.discriminator}`,
|
||||
replyTo: message.reference?.messageId,
|
||||
attachments: attachments.length > 0 ? attachments : undefined,
|
||||
metadata: {
|
||||
guildId: message.guildId,
|
||||
originalId: message.id,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private getMimeCategory(mimeType: string): Attachment['type'] {
|
||||
if (mimeType.startsWith('image/')) return 'image';
|
||||
if (mimeType.startsWith('audio/')) return 'audio';
|
||||
if (mimeType.startsWith('video/')) return 'video';
|
||||
return 'file';
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Factory Function
|
||||
// ============================================================================
|
||||
|
||||
export function createDiscordAdapter(
|
||||
config: Omit<AdapterConfig, 'type'> & { credentials: DiscordCredentials }
|
||||
): DiscordAdapter {
|
||||
return new DiscordAdapter(config);
|
||||
}
|
||||
|
||||
export default DiscordAdapter;
|
||||
62
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/SlackAdapter.d.ts
vendored
Normal file
62
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/SlackAdapter.d.ts
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* SlackAdapter - Slack Channel Integration
|
||||
*
|
||||
* Connects to Slack workspace using @slack/bolt for real-time messaging.
|
||||
* Supports threads, reactions, file attachments, and app mentions.
|
||||
*/
|
||||
import { BaseAdapter, type AdapterConfig, type UnifiedMessage, type SendOptions } from './BaseAdapter.js';
|
||||
export interface SlackCredentials {
|
||||
token: string;
|
||||
signingSecret: string;
|
||||
appToken?: string;
|
||||
socketMode?: boolean;
|
||||
}
|
||||
export interface SlackMessage {
|
||||
type: string;
|
||||
channel: string;
|
||||
user: string;
|
||||
text: string;
|
||||
ts: string;
|
||||
thread_ts?: string;
|
||||
files?: SlackFile[];
|
||||
blocks?: unknown[];
|
||||
}
|
||||
export interface SlackFile {
|
||||
id: string;
|
||||
name: string;
|
||||
mimetype: string;
|
||||
url_private: string;
|
||||
size: number;
|
||||
}
|
||||
export declare class SlackAdapter extends BaseAdapter {
|
||||
private client;
|
||||
private app;
|
||||
constructor(config: Omit<AdapterConfig, 'type'> & {
|
||||
credentials: SlackCredentials;
|
||||
});
|
||||
/**
|
||||
* Connect to Slack
|
||||
*/
|
||||
connect(): Promise<void>;
|
||||
/**
|
||||
* Disconnect from Slack
|
||||
*/
|
||||
disconnect(): Promise<void>;
|
||||
/**
|
||||
* Send a message to a Slack channel
|
||||
*/
|
||||
send(channelId: string, content: string, options?: SendOptions): Promise<string>;
|
||||
/**
|
||||
* Reply to a Slack message
|
||||
*/
|
||||
reply(message: UnifiedMessage, content: string, options?: SendOptions): Promise<string>;
|
||||
private loadSlackBolt;
|
||||
private getClient;
|
||||
private slackToUnified;
|
||||
private getMimeCategory;
|
||||
}
|
||||
export declare function createSlackAdapter(config: Omit<AdapterConfig, 'type'> & {
|
||||
credentials: SlackCredentials;
|
||||
}): SlackAdapter;
|
||||
export default SlackAdapter;
|
||||
//# sourceMappingURL=SlackAdapter.d.ts.map
|
||||
1
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/SlackAdapter.d.ts.map
vendored
Normal file
1
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/SlackAdapter.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"SlackAdapter.d.ts","sourceRoot":"","sources":["SlackAdapter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,WAAW,EACX,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,WAAW,EAEjB,MAAM,kBAAkB,CAAC;AAM1B,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,EAAE,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;CACd;AAgBD,qBAAa,YAAa,SAAQ,WAAW;IAC3C,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,GAAG,CAAiB;gBAEhB,MAAM,EAAE,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC,GAAG;QAAE,WAAW,EAAE,gBAAgB,CAAA;KAAE;IAInF;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAwC9B;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAQjC;;OAEG;IACG,IAAI,CACR,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC,MAAM,CAAC;IAsBlB;;OAEG;IACG,KAAK,CACT,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC,MAAM,CAAC;YAYJ,aAAa;IAQ3B,OAAO,CAAC,SAAS;IAYjB,OAAO,CAAC,cAAc;IAyBtB,OAAO,CAAC,eAAe;CAMxB;AAMD,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC,GAAG;IAAE,WAAW,EAAE,gBAAgB,CAAA;CAAE,GACtE,YAAY,CAEd;AAED,eAAe,YAAY,CAAC"}
|
||||
193
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/SlackAdapter.js
vendored
Normal file
193
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/SlackAdapter.js
vendored
Normal file
@@ -0,0 +1,193 @@
|
||||
"use strict";
|
||||
/**
|
||||
* SlackAdapter - Slack Channel Integration
|
||||
*
|
||||
* Connects to Slack workspace using @slack/bolt for real-time messaging.
|
||||
* Supports threads, reactions, file attachments, and app mentions.
|
||||
*/
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.SlackAdapter = void 0;
|
||||
exports.createSlackAdapter = createSlackAdapter;
|
||||
const BaseAdapter_js_1 = require("./BaseAdapter.js");
|
||||
// ============================================================================
|
||||
// SlackAdapter Implementation
|
||||
// ============================================================================
|
||||
class SlackAdapter extends BaseAdapter_js_1.BaseAdapter {
|
||||
constructor(config) {
|
||||
super({ ...config, type: 'slack' });
|
||||
this.client = null;
|
||||
this.app = null;
|
||||
}
|
||||
/**
|
||||
* Connect to Slack
|
||||
*/
|
||||
async connect() {
|
||||
const credentials = this.config.credentials;
|
||||
try {
|
||||
// Dynamic import to avoid requiring @slack/bolt if not used
|
||||
const boltModule = await this.loadSlackBolt();
|
||||
if (boltModule) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const App = boltModule.App;
|
||||
this.app = new App({
|
||||
token: credentials.token,
|
||||
signingSecret: credentials.signingSecret,
|
||||
socketMode: credentials.socketMode ?? false,
|
||||
appToken: credentials.appToken,
|
||||
});
|
||||
// Register message handler
|
||||
const app = this.app;
|
||||
const self = this;
|
||||
app.message(async function (args) {
|
||||
const unified = self.slackToUnified(args.message);
|
||||
await self.emitMessage(unified);
|
||||
});
|
||||
// Start the app
|
||||
await this.app.start();
|
||||
this.status.connected = true;
|
||||
}
|
||||
else {
|
||||
// Fallback: Mark as connected but log warning
|
||||
console.warn('SlackAdapter: @slack/bolt not available, running in mock mode');
|
||||
this.status.connected = true;
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
this.status.errorCount++;
|
||||
throw new Error(`Failed to connect to Slack: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Disconnect from Slack
|
||||
*/
|
||||
async disconnect() {
|
||||
if (this.app) {
|
||||
await this.app.stop?.();
|
||||
this.app = null;
|
||||
}
|
||||
this.status.connected = false;
|
||||
}
|
||||
/**
|
||||
* Send a message to a Slack channel
|
||||
*/
|
||||
async send(channelId, content, options) {
|
||||
if (!this.client && !this.app) {
|
||||
throw new Error('SlackAdapter not connected');
|
||||
}
|
||||
try {
|
||||
const client = this.getClient();
|
||||
const result = await client.chat.postMessage({
|
||||
channel: channelId,
|
||||
text: content,
|
||||
thread_ts: options?.threadId,
|
||||
});
|
||||
this.status.messageCount++;
|
||||
return result.ts;
|
||||
}
|
||||
catch (error) {
|
||||
this.status.errorCount++;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Reply to a Slack message
|
||||
*/
|
||||
async reply(message, content, options) {
|
||||
return this.send(message.channelId, content, {
|
||||
...options,
|
||||
threadId: message.threadId ?? message.metadata.ts,
|
||||
});
|
||||
}
|
||||
// ==========================================================================
|
||||
// Private Methods
|
||||
// ==========================================================================
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async loadSlackBolt() {
|
||||
try {
|
||||
return await Promise.resolve().then(() => __importStar(require('@slack/bolt')));
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
getClient() {
|
||||
if (this.app) {
|
||||
return this.app.client;
|
||||
}
|
||||
// Mock client for testing
|
||||
return {
|
||||
chat: {
|
||||
postMessage: async () => ({ ts: Date.now().toString() }),
|
||||
},
|
||||
};
|
||||
}
|
||||
slackToUnified(message) {
|
||||
const attachments = (message.files ?? []).map(file => ({
|
||||
id: file.id,
|
||||
type: this.getMimeCategory(file.mimetype),
|
||||
url: file.url_private,
|
||||
mimeType: file.mimetype,
|
||||
filename: file.name,
|
||||
size: file.size,
|
||||
}));
|
||||
return this.createUnifiedMessage(message.text, message.user, message.channel, {
|
||||
threadId: message.thread_ts,
|
||||
attachments: attachments.length > 0 ? attachments : undefined,
|
||||
metadata: {
|
||||
ts: message.ts,
|
||||
blocks: message.blocks,
|
||||
},
|
||||
});
|
||||
}
|
||||
getMimeCategory(mimeType) {
|
||||
if (mimeType.startsWith('image/'))
|
||||
return 'image';
|
||||
if (mimeType.startsWith('audio/'))
|
||||
return 'audio';
|
||||
if (mimeType.startsWith('video/'))
|
||||
return 'video';
|
||||
return 'file';
|
||||
}
|
||||
}
|
||||
exports.SlackAdapter = SlackAdapter;
|
||||
// ============================================================================
|
||||
// Factory Function
|
||||
// ============================================================================
|
||||
function createSlackAdapter(config) {
|
||||
return new SlackAdapter(config);
|
||||
}
|
||||
exports.default = SlackAdapter;
|
||||
//# sourceMappingURL=SlackAdapter.js.map
|
||||
1
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/SlackAdapter.js.map
vendored
Normal file
1
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/SlackAdapter.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"SlackAdapter.js","sourceRoot":"","sources":["SlackAdapter.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6NH,gDAIC;AA/ND,qDAM0B;AA0C1B,+EAA+E;AAC/E,8BAA8B;AAC9B,+EAA+E;AAE/E,MAAa,YAAa,SAAQ,4BAAW;IAI3C,YAAY,MAAuE;QACjF,KAAK,CAAC,EAAE,GAAG,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QAJ9B,WAAM,GAAY,IAAI,CAAC;QACvB,QAAG,GAAY,IAAI,CAAC;IAI5B,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,OAAO;QACX,MAAM,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,WAA0C,CAAC;QAE3E,IAAI,CAAC;YACH,4DAA4D;YAC5D,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,aAAa,EAAE,CAAC;YAE9C,IAAI,UAAU,EAAE,CAAC;gBACf,8DAA8D;gBAC9D,MAAM,GAAG,GAAG,UAAU,CAAC,GAAU,CAAC;gBAElC,IAAI,CAAC,GAAG,GAAG,IAAI,GAAG,CAAC;oBACjB,KAAK,EAAE,WAAW,CAAC,KAAK;oBACxB,aAAa,EAAE,WAAW,CAAC,aAAa;oBACxC,UAAU,EAAE,WAAW,CAAC,UAAU,IAAI,KAAK;oBAC3C,QAAQ,EAAE,WAAW,CAAC,QAAQ;iBAC/B,CAAC,CAAC;gBAEH,2BAA2B;gBAC3B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAyF,CAAC;gBAC3G,MAAM,IAAI,GAAG,IAAI,CAAC;gBAClB,GAAG,CAAC,OAAO,CAAC,KAAK,WAAU,IAA+B;oBACxD,MAAM,OAAO,GAAG,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;oBAClD,MAAM,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;gBAClC,CAAC,CAAC,CAAC;gBAEH,gBAAgB;gBAChB,MAAO,IAAI,CAAC,GAAsC,CAAC,KAAK,EAAE,CAAC;gBAC3D,IAAI,CAAC,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC;YAC/B,CAAC;iBAAM,CAAC;gBACN,8CAA8C;gBAC9C,OAAO,CAAC,IAAI,CAAC,+DAA+D,CAAC,CAAC;gBAC9E,IAAI,CAAC,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC;YAC/B,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CAAC,+BAA+B,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,EAAE,CAAC,CAAC;QAC7G,CAAC;IACH,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,UAAU;QACd,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;YACb,MAAO,IAAI,CAAC,GAAsC,CAAC,IAAI,EAAE,EAAE,CAAC;YAC5D,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC;QAClB,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,SAAS,GAAG,KAAK,CAAC;IAChC,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,IAAI,CACR,SAAiB,EACjB,OAAe,EACf,OAAqB;QAErB,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;QAChD,CAAC;QAED,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;YAEhC,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC;gBAC3C,OAAO,EAAE,SAAS;gBAClB,IAAI,EAAE,OAAO;gBACb,SAAS,EAAE,OAAO,EAAE,QAAQ;aAC7B,CAAC,CAAC;YAEH,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC;YAC3B,OAAO,MAAM,CAAC,EAAY,CAAC;QAC7B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;YACzB,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,KAAK,CACT,OAAuB,EACvB,OAAe,EACf,OAAqB;QAErB,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,EAAE;YAC3C,GAAG,OAAO;YACV,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC,EAAY;SAC5D,CAAC,CAAC;IACL,CAAC;IAED,6EAA6E;IAC7E,kBAAkB;IAClB,6EAA6E;IAE7E,8DAA8D;IACtD,KAAK,CAAC,aAAa;QACzB,IAAI,CAAC;YACH,OAAO,wDAAa,aAAa,GAAC,CAAC;QACrC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAEO,SAAS;QACf,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;YACb,OAAQ,IAAI,CAAC,GAA+B,CAAC,MAAM,CAAC;QACtD,CAAC;QACD,0BAA0B;QAC1B,OAAO;YACL,IAAI,EAAE;gBACJ,WAAW,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC;aACzD;SACF,CAAC;IACJ,CAAC;IAEO,cAAc,CAAC,OAAqB;QAC1C,MAAM,WAAW,GAAiB,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACnE,EAAE,EAAE,IAAI,CAAC,EAAE;YACX,IAAI,EAAE,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC;YACzC,GAAG,EAAE,IAAI,CAAC,WAAW;YACrB,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,QAAQ,EAAE,IAAI,CAAC,IAAI;YACnB,IAAI,EAAE,IAAI,CAAC,IAAI;SAChB,CAAC,CAAC,CAAC;QAEJ,OAAO,IAAI,CAAC,oBAAoB,CAC9B,OAAO,CAAC,IAAI,EACZ,OAAO,CAAC,IAAI,EACZ,OAAO,CAAC,OAAO,EACf;YACE,QAAQ,EAAE,OAAO,CAAC,SAAS;YAC3B,WAAW,EAAE,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;YAC7D,QAAQ,EAAE;gBACR,EAAE,EAAE,OAAO,CAAC,EAAE;gBACd,MAAM,EAAE,OAAO,CAAC,MAAM;aACvB;SACF,CACF,CAAC;IACJ,CAAC;IAEO,eAAe,CAAC,QAAgB;QACtC,IAAI,QAAQ,CAAC,UAAU,CAAC,QAAQ,CAAC;YAAE,OAAO,OAAO,CAAC;QAClD,IAAI,QAAQ,CAAC,UAAU,CAAC,QAAQ,CAAC;YAAE,OAAO,OAAO,CAAC;QAClD,IAAI,QAAQ,CAAC,UAAU,CAAC,QAAQ,CAAC;YAAE,OAAO,OAAO,CAAC;QAClD,OAAO,MAAM,CAAC;IAChB,CAAC;CACF;AAjKD,oCAiKC;AAED,+EAA+E;AAC/E,mBAAmB;AACnB,+EAA+E;AAE/E,SAAgB,kBAAkB,CAChC,MAAuE;IAEvE,OAAO,IAAI,YAAY,CAAC,MAAM,CAAC,CAAC;AAClC,CAAC;AAED,kBAAe,YAAY,CAAC"}
|
||||
233
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/SlackAdapter.ts
vendored
Normal file
233
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/SlackAdapter.ts
vendored
Normal file
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* SlackAdapter - Slack Channel Integration
|
||||
*
|
||||
* Connects to Slack workspace using @slack/bolt for real-time messaging.
|
||||
* Supports threads, reactions, file attachments, and app mentions.
|
||||
*/
|
||||
|
||||
import {
|
||||
BaseAdapter,
|
||||
type AdapterConfig,
|
||||
type UnifiedMessage,
|
||||
type SendOptions,
|
||||
type Attachment,
|
||||
} from './BaseAdapter.js';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
export interface SlackCredentials {
|
||||
token: string; // Bot User OAuth Token (xoxb-)
|
||||
signingSecret: string; // App Signing Secret
|
||||
appToken?: string; // App-Level Token for Socket Mode (xapp-)
|
||||
socketMode?: boolean;
|
||||
}
|
||||
|
||||
export interface SlackMessage {
|
||||
type: string;
|
||||
channel: string;
|
||||
user: string;
|
||||
text: string;
|
||||
ts: string;
|
||||
thread_ts?: string;
|
||||
files?: SlackFile[];
|
||||
blocks?: unknown[];
|
||||
}
|
||||
|
||||
export interface SlackFile {
|
||||
id: string;
|
||||
name: string;
|
||||
mimetype: string;
|
||||
url_private: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
interface SlackClient {
|
||||
chat: {
|
||||
postMessage: (args: {
|
||||
channel: string;
|
||||
text: string;
|
||||
thread_ts?: string;
|
||||
}) => Promise<{ ts: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SlackAdapter Implementation
|
||||
// ============================================================================
|
||||
|
||||
export class SlackAdapter extends BaseAdapter {
|
||||
private client: unknown = null;
|
||||
private app: unknown = null;
|
||||
|
||||
constructor(config: Omit<AdapterConfig, 'type'> & { credentials: SlackCredentials }) {
|
||||
super({ ...config, type: 'slack' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to Slack
|
||||
*/
|
||||
async connect(): Promise<void> {
|
||||
const credentials = this.config.credentials as unknown as SlackCredentials;
|
||||
|
||||
try {
|
||||
// Dynamic import to avoid requiring @slack/bolt if not used
|
||||
const boltModule = await this.loadSlackBolt();
|
||||
|
||||
if (boltModule) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const App = boltModule.App as any;
|
||||
|
||||
this.app = new App({
|
||||
token: credentials.token,
|
||||
signingSecret: credentials.signingSecret,
|
||||
socketMode: credentials.socketMode ?? false,
|
||||
appToken: credentials.appToken,
|
||||
});
|
||||
|
||||
// Register message handler
|
||||
const app = this.app as { message: (handler: (args: { message: SlackMessage }) => Promise<void>) => void };
|
||||
const self = this;
|
||||
app.message(async function(args: { message: SlackMessage }) {
|
||||
const unified = self.slackToUnified(args.message);
|
||||
await self.emitMessage(unified);
|
||||
});
|
||||
|
||||
// Start the app
|
||||
await (this.app as { start: () => Promise<void> }).start();
|
||||
this.status.connected = true;
|
||||
} else {
|
||||
// Fallback: Mark as connected but log warning
|
||||
console.warn('SlackAdapter: @slack/bolt not available, running in mock mode');
|
||||
this.status.connected = true;
|
||||
}
|
||||
} catch (error) {
|
||||
this.status.errorCount++;
|
||||
throw new Error(`Failed to connect to Slack: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from Slack
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
if (this.app) {
|
||||
await (this.app as { stop?: () => Promise<void> }).stop?.();
|
||||
this.app = null;
|
||||
}
|
||||
this.status.connected = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to a Slack channel
|
||||
*/
|
||||
async send(
|
||||
channelId: string,
|
||||
content: string,
|
||||
options?: SendOptions
|
||||
): Promise<string> {
|
||||
if (!this.client && !this.app) {
|
||||
throw new Error('SlackAdapter not connected');
|
||||
}
|
||||
|
||||
try {
|
||||
const client = this.getClient();
|
||||
|
||||
const result = await client.chat.postMessage({
|
||||
channel: channelId,
|
||||
text: content,
|
||||
thread_ts: options?.threadId,
|
||||
});
|
||||
|
||||
this.status.messageCount++;
|
||||
return result.ts as string;
|
||||
} catch (error) {
|
||||
this.status.errorCount++;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reply to a Slack message
|
||||
*/
|
||||
async reply(
|
||||
message: UnifiedMessage,
|
||||
content: string,
|
||||
options?: SendOptions
|
||||
): Promise<string> {
|
||||
return this.send(message.channelId, content, {
|
||||
...options,
|
||||
threadId: message.threadId ?? message.metadata.ts as string,
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Private Methods
|
||||
// ==========================================================================
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private async loadSlackBolt(): Promise<any | null> {
|
||||
try {
|
||||
return await import('@slack/bolt');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private getClient(): SlackClient {
|
||||
if (this.app) {
|
||||
return (this.app as { client: SlackClient }).client;
|
||||
}
|
||||
// Mock client for testing
|
||||
return {
|
||||
chat: {
|
||||
postMessage: async () => ({ ts: Date.now().toString() }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private slackToUnified(message: SlackMessage): UnifiedMessage {
|
||||
const attachments: Attachment[] = (message.files ?? []).map(file => ({
|
||||
id: file.id,
|
||||
type: this.getMimeCategory(file.mimetype),
|
||||
url: file.url_private,
|
||||
mimeType: file.mimetype,
|
||||
filename: file.name,
|
||||
size: file.size,
|
||||
}));
|
||||
|
||||
return this.createUnifiedMessage(
|
||||
message.text,
|
||||
message.user,
|
||||
message.channel,
|
||||
{
|
||||
threadId: message.thread_ts,
|
||||
attachments: attachments.length > 0 ? attachments : undefined,
|
||||
metadata: {
|
||||
ts: message.ts,
|
||||
blocks: message.blocks,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private getMimeCategory(mimeType: string): Attachment['type'] {
|
||||
if (mimeType.startsWith('image/')) return 'image';
|
||||
if (mimeType.startsWith('audio/')) return 'audio';
|
||||
if (mimeType.startsWith('video/')) return 'video';
|
||||
return 'file';
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Factory Function
|
||||
// ============================================================================
|
||||
|
||||
export function createSlackAdapter(
|
||||
config: Omit<AdapterConfig, 'type'> & { credentials: SlackCredentials }
|
||||
): SlackAdapter {
|
||||
return new SlackAdapter(config);
|
||||
}
|
||||
|
||||
export default SlackAdapter;
|
||||
93
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/TelegramAdapter.d.ts
vendored
Normal file
93
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/TelegramAdapter.d.ts
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* TelegramAdapter - Telegram Channel Integration
|
||||
*
|
||||
* Connects to Telegram using telegraf for real-time messaging.
|
||||
* Supports inline keyboards, commands, and rich media.
|
||||
*/
|
||||
import { BaseAdapter, type AdapterConfig, type UnifiedMessage, type SendOptions } from './BaseAdapter.js';
|
||||
export interface TelegramCredentials {
|
||||
token: string;
|
||||
webhookUrl?: string;
|
||||
pollingTimeout?: number;
|
||||
}
|
||||
export interface TelegramMessage {
|
||||
message_id: number;
|
||||
chat: {
|
||||
id: number;
|
||||
type: string;
|
||||
title?: string;
|
||||
username?: string;
|
||||
};
|
||||
from: {
|
||||
id: number;
|
||||
username?: string;
|
||||
first_name: string;
|
||||
last_name?: string;
|
||||
};
|
||||
text?: string;
|
||||
date: number;
|
||||
reply_to_message?: TelegramMessage;
|
||||
photo?: TelegramPhoto[];
|
||||
document?: TelegramDocument;
|
||||
audio?: TelegramAudio;
|
||||
video?: TelegramVideo;
|
||||
}
|
||||
export interface TelegramPhoto {
|
||||
file_id: string;
|
||||
file_unique_id: string;
|
||||
width: number;
|
||||
height: number;
|
||||
file_size?: number;
|
||||
}
|
||||
export interface TelegramDocument {
|
||||
file_id: string;
|
||||
file_unique_id: string;
|
||||
file_name?: string;
|
||||
mime_type?: string;
|
||||
file_size?: number;
|
||||
}
|
||||
export interface TelegramAudio {
|
||||
file_id: string;
|
||||
file_unique_id: string;
|
||||
duration: number;
|
||||
performer?: string;
|
||||
title?: string;
|
||||
file_size?: number;
|
||||
}
|
||||
export interface TelegramVideo {
|
||||
file_id: string;
|
||||
file_unique_id: string;
|
||||
width: number;
|
||||
height: number;
|
||||
duration: number;
|
||||
file_size?: number;
|
||||
}
|
||||
export declare class TelegramAdapter extends BaseAdapter {
|
||||
private bot;
|
||||
constructor(config: Omit<AdapterConfig, 'type'> & {
|
||||
credentials: TelegramCredentials;
|
||||
});
|
||||
/**
|
||||
* Connect to Telegram
|
||||
*/
|
||||
connect(): Promise<void>;
|
||||
/**
|
||||
* Disconnect from Telegram
|
||||
*/
|
||||
disconnect(): Promise<void>;
|
||||
/**
|
||||
* Send a message to a Telegram chat
|
||||
*/
|
||||
send(channelId: string, content: string, options?: SendOptions): Promise<string>;
|
||||
/**
|
||||
* Reply to a Telegram message
|
||||
*/
|
||||
reply(message: UnifiedMessage, content: string, options?: SendOptions): Promise<string>;
|
||||
private loadTelegraf;
|
||||
private telegramToUnified;
|
||||
}
|
||||
export declare function createTelegramAdapter(config: Omit<AdapterConfig, 'type'> & {
|
||||
credentials: TelegramCredentials;
|
||||
}): TelegramAdapter;
|
||||
export default TelegramAdapter;
|
||||
//# sourceMappingURL=TelegramAdapter.d.ts.map
|
||||
1
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/TelegramAdapter.d.ts.map
vendored
Normal file
1
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/TelegramAdapter.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"TelegramAdapter.d.ts","sourceRoot":"","sources":["TelegramAdapter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACL,WAAW,EACX,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,WAAW,EAEjB,MAAM,kBAAkB,CAAC;AAM1B,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE;QACJ,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,IAAI,EAAE;QACJ,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,eAAe,CAAC;IACnC,KAAK,CAAC,EAAE,aAAa,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAMD,qBAAa,eAAgB,SAAQ,WAAW;IAC9C,OAAO,CAAC,GAAG,CAAiB;gBAEhB,MAAM,EAAE,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC,GAAG;QAAE,WAAW,EAAE,mBAAmB,CAAA;KAAE;IAItF;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAyC9B;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAQjC;;OAEG;IACG,IAAI,CACR,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC,MAAM,CAAC;IA8BlB;;OAEG;IACG,KAAK,CACT,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC,MAAM,CAAC;YAYJ,YAAY;IAU1B,OAAO,CAAC,iBAAiB;CA8D1B;AAMD,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC,GAAG;IAAE,WAAW,EAAE,mBAAmB,CAAA;CAAE,GACzE,eAAe,CAEjB;AAED,eAAe,eAAe,CAAC"}
|
||||
204
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/TelegramAdapter.js
vendored
Normal file
204
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/TelegramAdapter.js
vendored
Normal file
@@ -0,0 +1,204 @@
|
||||
"use strict";
|
||||
/**
|
||||
* TelegramAdapter - Telegram Channel Integration
|
||||
*
|
||||
* Connects to Telegram using telegraf for real-time messaging.
|
||||
* Supports inline keyboards, commands, and rich media.
|
||||
*/
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.TelegramAdapter = void 0;
|
||||
exports.createTelegramAdapter = createTelegramAdapter;
|
||||
const BaseAdapter_js_1 = require("./BaseAdapter.js");
|
||||
// ============================================================================
|
||||
// TelegramAdapter Implementation
|
||||
// ============================================================================
|
||||
class TelegramAdapter extends BaseAdapter_js_1.BaseAdapter {
|
||||
constructor(config) {
|
||||
super({ ...config, type: 'telegram' });
|
||||
this.bot = null;
|
||||
}
|
||||
/**
|
||||
* Connect to Telegram
|
||||
*/
|
||||
async connect() {
|
||||
const credentials = this.config.credentials;
|
||||
try {
|
||||
// Dynamic import to avoid requiring telegraf if not used
|
||||
const telegrafModule = await this.loadTelegraf();
|
||||
if (telegrafModule) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const Telegraf = telegrafModule.Telegraf;
|
||||
this.bot = new Telegraf(credentials.token);
|
||||
// Register message handler
|
||||
this.bot.on('message', (ctx) => {
|
||||
const unified = this.telegramToUnified(ctx.message);
|
||||
this.emitMessage(unified);
|
||||
});
|
||||
// Start polling or webhook
|
||||
if (credentials.webhookUrl) {
|
||||
await this.bot.telegram.setWebhook(credentials.webhookUrl);
|
||||
}
|
||||
else {
|
||||
this.bot.launch();
|
||||
}
|
||||
this.status.connected = true;
|
||||
}
|
||||
else {
|
||||
console.warn('TelegramAdapter: telegraf not available, running in mock mode');
|
||||
this.status.connected = true;
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
this.status.errorCount++;
|
||||
throw new Error(`Failed to connect to Telegram: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Disconnect from Telegram
|
||||
*/
|
||||
async disconnect() {
|
||||
if (this.bot) {
|
||||
this.bot.stop?.('SIGTERM');
|
||||
this.bot = null;
|
||||
}
|
||||
this.status.connected = false;
|
||||
}
|
||||
/**
|
||||
* Send a message to a Telegram chat
|
||||
*/
|
||||
async send(channelId, content, options) {
|
||||
if (!this.bot) {
|
||||
throw new Error('TelegramAdapter not connected');
|
||||
}
|
||||
try {
|
||||
const telegram = this.bot.telegram;
|
||||
const extra = {};
|
||||
if (options?.replyTo) {
|
||||
extra.reply_to_message_id = parseInt(options.replyTo, 10);
|
||||
}
|
||||
const result = await telegram.sendMessage(channelId, content, Object.keys(extra).length > 0 ? extra : undefined);
|
||||
this.status.messageCount++;
|
||||
return result.message_id.toString();
|
||||
}
|
||||
catch (error) {
|
||||
this.status.errorCount++;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Reply to a Telegram message
|
||||
*/
|
||||
async reply(message, content, options) {
|
||||
return this.send(message.channelId, content, {
|
||||
...options,
|
||||
replyTo: message.metadata.messageId,
|
||||
});
|
||||
}
|
||||
// ==========================================================================
|
||||
// Private Methods
|
||||
// ==========================================================================
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async loadTelegraf() {
|
||||
try {
|
||||
// Dynamic import - telegraf is optional
|
||||
// @ts-expect-error - telegraf may not be installed
|
||||
return await Promise.resolve().then(() => __importStar(require('telegraf'))).catch(() => null);
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
telegramToUnified(message) {
|
||||
const attachments = [];
|
||||
// Handle photos (get largest)
|
||||
if (message.photo && message.photo.length > 0) {
|
||||
const photo = message.photo[message.photo.length - 1];
|
||||
attachments.push({
|
||||
id: photo.file_id,
|
||||
type: 'image',
|
||||
size: photo.file_size,
|
||||
});
|
||||
}
|
||||
// Handle document
|
||||
if (message.document) {
|
||||
attachments.push({
|
||||
id: message.document.file_id,
|
||||
type: 'file',
|
||||
filename: message.document.file_name,
|
||||
mimeType: message.document.mime_type,
|
||||
size: message.document.file_size,
|
||||
});
|
||||
}
|
||||
// Handle audio
|
||||
if (message.audio) {
|
||||
attachments.push({
|
||||
id: message.audio.file_id,
|
||||
type: 'audio',
|
||||
size: message.audio.file_size,
|
||||
});
|
||||
}
|
||||
// Handle video
|
||||
if (message.video) {
|
||||
attachments.push({
|
||||
id: message.video.file_id,
|
||||
type: 'video',
|
||||
size: message.video.file_size,
|
||||
});
|
||||
}
|
||||
const username = message.from.username ??
|
||||
`${message.from.first_name}${message.from.last_name ? ' ' + message.from.last_name : ''}`;
|
||||
return this.createUnifiedMessage(message.text ?? '[media]', message.from.id.toString(), message.chat.id.toString(), {
|
||||
username,
|
||||
replyTo: message.reply_to_message?.message_id.toString(),
|
||||
timestamp: new Date(message.date * 1000),
|
||||
attachments: attachments.length > 0 ? attachments : undefined,
|
||||
metadata: {
|
||||
messageId: message.message_id.toString(),
|
||||
chatType: message.chat.type,
|
||||
chatTitle: message.chat.title,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
exports.TelegramAdapter = TelegramAdapter;
|
||||
// ============================================================================
|
||||
// Factory Function
|
||||
// ============================================================================
|
||||
function createTelegramAdapter(config) {
|
||||
return new TelegramAdapter(config);
|
||||
}
|
||||
exports.default = TelegramAdapter;
|
||||
//# sourceMappingURL=TelegramAdapter.js.map
|
||||
1
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/TelegramAdapter.js.map
vendored
Normal file
1
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/TelegramAdapter.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"TelegramAdapter.js","sourceRoot":"","sources":["TelegramAdapter.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqRH,sDAIC;AAvRD,qDAM0B;AAqE1B,+EAA+E;AAC/E,iCAAiC;AACjC,+EAA+E;AAE/E,MAAa,eAAgB,SAAQ,4BAAW;IAG9C,YAAY,MAA0E;QACpF,KAAK,CAAC,EAAE,GAAG,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;QAHjC,QAAG,GAAY,IAAI,CAAC;IAI5B,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,OAAO;QACX,MAAM,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,WAA6C,CAAC;QAE9E,IAAI,CAAC;YACH,yDAAyD;YACzD,MAAM,cAAc,GAAG,MAAM,IAAI,CAAC,YAAY,EAAE,CAAC;YAEjD,IAAI,cAAc,EAAE,CAAC;gBACnB,8DAA8D;gBAC9D,MAAM,QAAQ,GAAG,cAAc,CAAC,QAAe,CAAC;gBAEhD,IAAI,CAAC,GAAG,GAAG,IAAI,QAAQ,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;gBAE3C,2BAA2B;gBAC1B,IAAI,CAAC,GAA6F,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,GAAiC,EAAE,EAAE;oBACtJ,MAAM,OAAO,GAAG,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;oBACpD,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;gBAC5B,CAAC,CAAC,CAAC;gBAEH,2BAA2B;gBAC3B,IAAI,WAAW,CAAC,UAAU,EAAE,CAAC;oBAC3B,MAAO,IAAI,CAAC,GAIV,CAAC,QAAQ,CAAC,UAAU,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;gBACjD,CAAC;qBAAM,CAAC;oBACL,IAAI,CAAC,GAA8B,CAAC,MAAM,EAAE,CAAC;gBAChD,CAAC;gBAED,IAAI,CAAC,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC;YAC/B,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,IAAI,CAAC,+DAA+D,CAAC,CAAC;gBAC9E,IAAI,CAAC,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC;YAC/B,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CAAC,kCAAkC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,EAAE,CAAC,CAAC;QAChH,CAAC;IACH,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,UAAU;QACd,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;YACZ,IAAI,CAAC,GAA4C,CAAC,IAAI,EAAE,CAAC,SAAS,CAAC,CAAC;YACrE,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC;QAClB,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,SAAS,GAAG,KAAK,CAAC;IAChC,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,IAAI,CACR,SAAiB,EACjB,OAAe,EACf,OAAqB;QAErB,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;YACd,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;QACnD,CAAC;QAED,IAAI,CAAC;YACH,MAAM,QAAQ,GAAI,IAAI,CAAC,GAA6B,CAAC,QAEpD,CAAC;YAEF,MAAM,KAAK,GAA4B,EAAE,CAAC;YAE1C,IAAI,OAAO,EAAE,OAAO,EAAE,CAAC;gBACrB,KAAK,CAAC,mBAAmB,GAAG,QAAQ,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;YAC5D,CAAC;YAED,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,WAAW,CACvC,SAAS,EACT,OAAO,EACP,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAClD,CAAC;YAEF,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC;YAC3B,OAAO,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC;QACtC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;YACzB,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,KAAK,CACT,OAAuB,EACvB,OAAe,EACf,OAAqB;QAErB,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,EAAE;YAC3C,GAAG,OAAO;YACV,OAAO,EAAE,OAAO,CAAC,QAAQ,CAAC,SAAmB;SAC9C,CAAC,CAAC;IACL,CAAC;IAED,6EAA6E;IAC7E,kBAAkB;IAClB,6EAA6E;IAE7E,8DAA8D;IACtD,KAAK,CAAC,YAAY;QACxB,IAAI,CAAC;YACH,wCAAwC;YACxC,mDAAmD;YACnD,OAAO,MAAM,kDAAO,UAAU,IAAE,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;QACpD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAEO,iBAAiB,CAAC,OAAwB;QAChD,MAAM,WAAW,GAAiB,EAAE,CAAC;QAErC,8BAA8B;QAC9B,IAAI,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC9C,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YACtD,WAAW,CAAC,IAAI,CAAC;gBACf,EAAE,EAAE,KAAK,CAAC,OAAO;gBACjB,IAAI,EAAE,OAAO;gBACb,IAAI,EAAE,KAAK,CAAC,SAAS;aACtB,CAAC,CAAC;QACL,CAAC;QAED,kBAAkB;QAClB,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;YACrB,WAAW,CAAC,IAAI,CAAC;gBACf,EAAE,EAAE,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAC5B,IAAI,EAAE,MAAM;gBACZ,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,SAAS;gBACpC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,SAAS;gBACpC,IAAI,EAAE,OAAO,CAAC,QAAQ,CAAC,SAAS;aACjC,CAAC,CAAC;QACL,CAAC;QAED,eAAe;QACf,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;YAClB,WAAW,CAAC,IAAI,CAAC;gBACf,EAAE,EAAE,OAAO,CAAC,KAAK,CAAC,OAAO;gBACzB,IAAI,EAAE,OAAO;gBACb,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC,SAAS;aAC9B,CAAC,CAAC;QACL,CAAC;QAED,eAAe;QACf,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;YAClB,WAAW,CAAC,IAAI,CAAC;gBACf,EAAE,EAAE,OAAO,CAAC,KAAK,CAAC,OAAO;gBACzB,IAAI,EAAE,OAAO;gBACb,IAAI,EAAE,OAAO,CAAC,KAAK,CAAC,SAAS;aAC9B,CAAC,CAAC;QACL,CAAC;QAED,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ;YACpC,GAAG,OAAO,CAAC,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QAE5F,OAAO,IAAI,CAAC,oBAAoB,CAC9B,OAAO,CAAC,IAAI,IAAI,SAAS,EACzB,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,QAAQ,EAAE,EAC1B,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,QAAQ,EAAE,EAC1B;YACE,QAAQ;YACR,OAAO,EAAE,OAAO,CAAC,gBAAgB,EAAE,UAAU,CAAC,QAAQ,EAAE;YACxD,SAAS,EAAE,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;YACxC,WAAW,EAAE,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;YAC7D,QAAQ,EAAE;gBACR,SAAS,EAAE,OAAO,CAAC,UAAU,CAAC,QAAQ,EAAE;gBACxC,QAAQ,EAAE,OAAO,CAAC,IAAI,CAAC,IAAI;gBAC3B,SAAS,EAAE,OAAO,CAAC,IAAI,CAAC,KAAK;aAC9B;SACF,CACF,CAAC;IACJ,CAAC;CACF;AA9LD,0CA8LC;AAED,+EAA+E;AAC/E,mBAAmB;AACnB,+EAA+E;AAE/E,SAAgB,qBAAqB,CACnC,MAA0E;IAE1E,OAAO,IAAI,eAAe,CAAC,MAAM,CAAC,CAAC;AACrC,CAAC;AAED,kBAAe,eAAe,CAAC"}
|
||||
289
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/TelegramAdapter.ts
vendored
Normal file
289
vendor/ruvector/npm/packages/ruvbot/src/channels/adapters/TelegramAdapter.ts
vendored
Normal file
@@ -0,0 +1,289 @@
|
||||
/**
|
||||
* TelegramAdapter - Telegram Channel Integration
|
||||
*
|
||||
* Connects to Telegram using telegraf for real-time messaging.
|
||||
* Supports inline keyboards, commands, and rich media.
|
||||
*/
|
||||
|
||||
import {
|
||||
BaseAdapter,
|
||||
type AdapterConfig,
|
||||
type UnifiedMessage,
|
||||
type SendOptions,
|
||||
type Attachment,
|
||||
} from './BaseAdapter.js';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
export interface TelegramCredentials {
|
||||
token: string; // Bot Token from @BotFather
|
||||
webhookUrl?: string; // Optional webhook URL for production
|
||||
pollingTimeout?: number; // Long polling timeout
|
||||
}
|
||||
|
||||
export interface TelegramMessage {
|
||||
message_id: number;
|
||||
chat: {
|
||||
id: number;
|
||||
type: string;
|
||||
title?: string;
|
||||
username?: string;
|
||||
};
|
||||
from: {
|
||||
id: number;
|
||||
username?: string;
|
||||
first_name: string;
|
||||
last_name?: string;
|
||||
};
|
||||
text?: string;
|
||||
date: number;
|
||||
reply_to_message?: TelegramMessage;
|
||||
photo?: TelegramPhoto[];
|
||||
document?: TelegramDocument;
|
||||
audio?: TelegramAudio;
|
||||
video?: TelegramVideo;
|
||||
}
|
||||
|
||||
export interface TelegramPhoto {
|
||||
file_id: string;
|
||||
file_unique_id: string;
|
||||
width: number;
|
||||
height: number;
|
||||
file_size?: number;
|
||||
}
|
||||
|
||||
export interface TelegramDocument {
|
||||
file_id: string;
|
||||
file_unique_id: string;
|
||||
file_name?: string;
|
||||
mime_type?: string;
|
||||
file_size?: number;
|
||||
}
|
||||
|
||||
export interface TelegramAudio {
|
||||
file_id: string;
|
||||
file_unique_id: string;
|
||||
duration: number;
|
||||
performer?: string;
|
||||
title?: string;
|
||||
file_size?: number;
|
||||
}
|
||||
|
||||
export interface TelegramVideo {
|
||||
file_id: string;
|
||||
file_unique_id: string;
|
||||
width: number;
|
||||
height: number;
|
||||
duration: number;
|
||||
file_size?: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TelegramAdapter Implementation
|
||||
// ============================================================================
|
||||
|
||||
export class TelegramAdapter extends BaseAdapter {
|
||||
private bot: unknown = null;
|
||||
|
||||
constructor(config: Omit<AdapterConfig, 'type'> & { credentials: TelegramCredentials }) {
|
||||
super({ ...config, type: 'telegram' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to Telegram
|
||||
*/
|
||||
async connect(): Promise<void> {
|
||||
const credentials = this.config.credentials as unknown as TelegramCredentials;
|
||||
|
||||
try {
|
||||
// Dynamic import to avoid requiring telegraf if not used
|
||||
const telegrafModule = await this.loadTelegraf();
|
||||
|
||||
if (telegrafModule) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const Telegraf = telegrafModule.Telegraf as any;
|
||||
|
||||
this.bot = new Telegraf(credentials.token);
|
||||
|
||||
// Register message handler
|
||||
(this.bot as { on: (event: string, handler: (ctx: { message: TelegramMessage }) => void) => void }).on('message', (ctx: { message: TelegramMessage }) => {
|
||||
const unified = this.telegramToUnified(ctx.message);
|
||||
this.emitMessage(unified);
|
||||
});
|
||||
|
||||
// Start polling or webhook
|
||||
if (credentials.webhookUrl) {
|
||||
await (this.bot as {
|
||||
telegram: {
|
||||
setWebhook: (url: string) => Promise<void>
|
||||
}
|
||||
}).telegram.setWebhook(credentials.webhookUrl);
|
||||
} else {
|
||||
(this.bot as { launch: () => void }).launch();
|
||||
}
|
||||
|
||||
this.status.connected = true;
|
||||
} else {
|
||||
console.warn('TelegramAdapter: telegraf not available, running in mock mode');
|
||||
this.status.connected = true;
|
||||
}
|
||||
} catch (error) {
|
||||
this.status.errorCount++;
|
||||
throw new Error(`Failed to connect to Telegram: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from Telegram
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
if (this.bot) {
|
||||
(this.bot as { stop?: (signal?: string) => void }).stop?.('SIGTERM');
|
||||
this.bot = null;
|
||||
}
|
||||
this.status.connected = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to a Telegram chat
|
||||
*/
|
||||
async send(
|
||||
channelId: string,
|
||||
content: string,
|
||||
options?: SendOptions
|
||||
): Promise<string> {
|
||||
if (!this.bot) {
|
||||
throw new Error('TelegramAdapter not connected');
|
||||
}
|
||||
|
||||
try {
|
||||
const telegram = (this.bot as { telegram: unknown }).telegram as {
|
||||
sendMessage: (chatId: string | number, text: string, extra?: unknown) => Promise<{ message_id: number }>;
|
||||
};
|
||||
|
||||
const extra: Record<string, unknown> = {};
|
||||
|
||||
if (options?.replyTo) {
|
||||
extra.reply_to_message_id = parseInt(options.replyTo, 10);
|
||||
}
|
||||
|
||||
const result = await telegram.sendMessage(
|
||||
channelId,
|
||||
content,
|
||||
Object.keys(extra).length > 0 ? extra : undefined
|
||||
);
|
||||
|
||||
this.status.messageCount++;
|
||||
return result.message_id.toString();
|
||||
} catch (error) {
|
||||
this.status.errorCount++;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reply to a Telegram message
|
||||
*/
|
||||
async reply(
|
||||
message: UnifiedMessage,
|
||||
content: string,
|
||||
options?: SendOptions
|
||||
): Promise<string> {
|
||||
return this.send(message.channelId, content, {
|
||||
...options,
|
||||
replyTo: message.metadata.messageId as string,
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Private Methods
|
||||
// ==========================================================================
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private async loadTelegraf(): Promise<any | null> {
|
||||
try {
|
||||
// Dynamic import - telegraf is optional
|
||||
// @ts-expect-error - telegraf may not be installed
|
||||
return await import('telegraf').catch(() => null);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private telegramToUnified(message: TelegramMessage): UnifiedMessage {
|
||||
const attachments: Attachment[] = [];
|
||||
|
||||
// Handle photos (get largest)
|
||||
if (message.photo && message.photo.length > 0) {
|
||||
const photo = message.photo[message.photo.length - 1];
|
||||
attachments.push({
|
||||
id: photo.file_id,
|
||||
type: 'image',
|
||||
size: photo.file_size,
|
||||
});
|
||||
}
|
||||
|
||||
// Handle document
|
||||
if (message.document) {
|
||||
attachments.push({
|
||||
id: message.document.file_id,
|
||||
type: 'file',
|
||||
filename: message.document.file_name,
|
||||
mimeType: message.document.mime_type,
|
||||
size: message.document.file_size,
|
||||
});
|
||||
}
|
||||
|
||||
// Handle audio
|
||||
if (message.audio) {
|
||||
attachments.push({
|
||||
id: message.audio.file_id,
|
||||
type: 'audio',
|
||||
size: message.audio.file_size,
|
||||
});
|
||||
}
|
||||
|
||||
// Handle video
|
||||
if (message.video) {
|
||||
attachments.push({
|
||||
id: message.video.file_id,
|
||||
type: 'video',
|
||||
size: message.video.file_size,
|
||||
});
|
||||
}
|
||||
|
||||
const username = message.from.username ??
|
||||
`${message.from.first_name}${message.from.last_name ? ' ' + message.from.last_name : ''}`;
|
||||
|
||||
return this.createUnifiedMessage(
|
||||
message.text ?? '[media]',
|
||||
message.from.id.toString(),
|
||||
message.chat.id.toString(),
|
||||
{
|
||||
username,
|
||||
replyTo: message.reply_to_message?.message_id.toString(),
|
||||
timestamp: new Date(message.date * 1000),
|
||||
attachments: attachments.length > 0 ? attachments : undefined,
|
||||
metadata: {
|
||||
messageId: message.message_id.toString(),
|
||||
chatType: message.chat.type,
|
||||
chatTitle: message.chat.title,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Factory Function
|
||||
// ============================================================================
|
||||
|
||||
export function createTelegramAdapter(
|
||||
config: Omit<AdapterConfig, 'type'> & { credentials: TelegramCredentials }
|
||||
): TelegramAdapter {
|
||||
return new TelegramAdapter(config);
|
||||
}
|
||||
|
||||
export default TelegramAdapter;
|
||||
1
vendor/ruvector/npm/packages/ruvbot/src/channels/index.d.ts.map
vendored
Normal file
1
vendor/ruvector/npm/packages/ruvbot/src/channels/index.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EACL,WAAW,EACX,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,cAAc,GACpB,MAAM,2BAA2B,CAAC;AAGnC,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,KAAK,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AACrG,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,KAAK,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAC7G,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,KAAK,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAGjH,OAAO,EACL,eAAe,EACf,qBAAqB,EACrB,KAAK,aAAa,EAClB,KAAK,qBAAqB,EAC1B,KAAK,cAAc,GACpB,MAAM,sBAAsB,CAAC"}
|
||||
1
vendor/ruvector/npm/packages/ruvbot/src/channels/index.js.map
vendored
Normal file
1
vendor/ruvector/npm/packages/ruvbot/src/channels/index.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;;AAEH,yBAAyB;AACzB,4DAUmC;AATjC,6GAAA,WAAW,OAAA;AAWb,mBAAmB;AACnB,8DAAqG;AAA5F,+GAAA,YAAY,OAAA;AAAE,qHAAA,kBAAkB,OAAA;AACzC,kEAA6G;AAApG,mHAAA,cAAc,OAAA;AAAE,yHAAA,oBAAoB,OAAA;AAC7C,oEAAiH;AAAxG,qHAAA,eAAe,OAAA;AAAE,2HAAA,qBAAqB,OAAA;AAE/C,mBAAmB;AACnB,2DAM8B;AAL5B,qHAAA,eAAe,OAAA;AACf,2HAAA,qBAAqB,OAAA"}
|
||||
32
vendor/ruvector/npm/packages/ruvbot/src/channels/index.ts
vendored
Normal file
32
vendor/ruvector/npm/packages/ruvbot/src/channels/index.ts
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Channels module exports
|
||||
*
|
||||
* Multi-channel messaging support with unified interface.
|
||||
*/
|
||||
|
||||
// Base adapter and types
|
||||
export {
|
||||
BaseAdapter,
|
||||
type ChannelType,
|
||||
type Attachment,
|
||||
type UnifiedMessage,
|
||||
type SendOptions,
|
||||
type ChannelCredentials,
|
||||
type AdapterConfig,
|
||||
type AdapterStatus,
|
||||
type MessageHandler,
|
||||
} from './adapters/BaseAdapter.js';
|
||||
|
||||
// Channel adapters
|
||||
export { SlackAdapter, createSlackAdapter, type SlackCredentials } from './adapters/SlackAdapter.js';
|
||||
export { DiscordAdapter, createDiscordAdapter, type DiscordCredentials } from './adapters/DiscordAdapter.js';
|
||||
export { TelegramAdapter, createTelegramAdapter, type TelegramCredentials } from './adapters/TelegramAdapter.js';
|
||||
|
||||
// Channel registry
|
||||
export {
|
||||
ChannelRegistry,
|
||||
createChannelRegistry,
|
||||
type ChannelFilter,
|
||||
type ChannelRegistryConfig,
|
||||
type AdapterFactory,
|
||||
} from './ChannelRegistry.js';
|
||||
15
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/agent.d.ts
vendored
Normal file
15
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/agent.d.ts
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Agent Command - Agent and swarm management
|
||||
*
|
||||
* Commands:
|
||||
* agent spawn Spawn a new agent
|
||||
* agent list List running agents
|
||||
* agent stop Stop an agent
|
||||
* agent status Show agent status
|
||||
* swarm init Initialize swarm coordination
|
||||
* swarm status Show swarm status
|
||||
*/
|
||||
import { Command } from 'commander';
|
||||
export declare function createAgentCommand(): Command;
|
||||
export default createAgentCommand;
|
||||
//# sourceMappingURL=agent.d.ts.map
|
||||
1
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/agent.d.ts.map
vendored
Normal file
1
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/agent.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["agent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAUpC,wBAAgB,kBAAkB,IAAI,OAAO,CAkR5C;AAED,eAAe,kBAAkB,CAAC"}
|
||||
271
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/agent.js
vendored
Normal file
271
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/agent.js
vendored
Normal file
@@ -0,0 +1,271 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Agent Command - Agent and swarm management
|
||||
*
|
||||
* Commands:
|
||||
* agent spawn Spawn a new agent
|
||||
* agent list List running agents
|
||||
* agent stop Stop an agent
|
||||
* agent status Show agent status
|
||||
* swarm init Initialize swarm coordination
|
||||
* swarm status Show swarm status
|
||||
*/
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createAgentCommand = createAgentCommand;
|
||||
const commander_1 = require("commander");
|
||||
const chalk_1 = __importDefault(require("chalk"));
|
||||
const ora_1 = __importDefault(require("ora"));
|
||||
const SwarmCoordinator_js_1 = require("../../swarm/SwarmCoordinator.js");
|
||||
const VALID_WORKER_TYPES = [
|
||||
'ultralearn', 'optimize', 'consolidate', 'predict', 'audit',
|
||||
'map', 'preload', 'deepdive', 'document', 'refactor', 'benchmark', 'testgaps'
|
||||
];
|
||||
function createAgentCommand() {
|
||||
const agent = new commander_1.Command('agent');
|
||||
agent.description('Agent and swarm management commands');
|
||||
// Spawn command
|
||||
agent
|
||||
.command('spawn')
|
||||
.description('Spawn a new agent')
|
||||
.option('-t, --type <type>', 'Agent type (worker type)', 'optimize')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action(async (options) => {
|
||||
const spinner = (0, ora_1.default)(`Spawning ${options.type} agent...`).start();
|
||||
try {
|
||||
const workerType = options.type;
|
||||
if (!VALID_WORKER_TYPES.includes(workerType)) {
|
||||
spinner.fail(chalk_1.default.red(`Invalid worker type: ${options.type}`));
|
||||
console.log(chalk_1.default.gray(`Valid types: ${VALID_WORKER_TYPES.join(', ')}`));
|
||||
process.exit(1);
|
||||
}
|
||||
const coordinator = new SwarmCoordinator_js_1.SwarmCoordinator();
|
||||
await coordinator.start();
|
||||
const spawnedAgent = await coordinator.spawnAgent(workerType);
|
||||
spinner.stop();
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(spawnedAgent, null, 2));
|
||||
return;
|
||||
}
|
||||
console.log(chalk_1.default.green(`✓ Agent spawned: ${chalk_1.default.cyan(spawnedAgent.id)}`));
|
||||
console.log(chalk_1.default.gray(` Type: ${spawnedAgent.type}`));
|
||||
console.log(chalk_1.default.gray(` Status: ${spawnedAgent.status}`));
|
||||
}
|
||||
catch (error) {
|
||||
spinner.fail(chalk_1.default.red(`Spawn failed: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
// List command
|
||||
agent
|
||||
.command('list')
|
||||
.description('List running agents')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
const coordinator = new SwarmCoordinator_js_1.SwarmCoordinator();
|
||||
const agents = coordinator.getAgents();
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(agents, null, 2));
|
||||
return;
|
||||
}
|
||||
if (agents.length === 0) {
|
||||
console.log(chalk_1.default.yellow('No agents running'));
|
||||
console.log(chalk_1.default.gray('Spawn one with: ruvbot agent spawn -t optimize'));
|
||||
return;
|
||||
}
|
||||
console.log(chalk_1.default.bold(`\n🤖 Agents (${agents.length})\n`));
|
||||
console.log('─'.repeat(70));
|
||||
console.log(chalk_1.default.gray('ID'.padEnd(40) + 'TYPE'.padEnd(15) + 'STATUS'.padEnd(12) + 'TASKS'));
|
||||
console.log('─'.repeat(70));
|
||||
for (const a of agents) {
|
||||
const statusColor = a.status === 'busy' ? chalk_1.default.green : a.status === 'idle' ? chalk_1.default.yellow : chalk_1.default.gray;
|
||||
console.log(chalk_1.default.cyan(a.id.padEnd(40)) +
|
||||
a.type.padEnd(15) +
|
||||
statusColor(a.status.padEnd(12)) +
|
||||
chalk_1.default.gray(String(a.completedTasks)));
|
||||
}
|
||||
console.log('─'.repeat(70));
|
||||
}
|
||||
catch (error) {
|
||||
console.error(chalk_1.default.red(`List failed: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
// Stop command
|
||||
agent
|
||||
.command('stop')
|
||||
.description('Stop an agent')
|
||||
.argument('<id>', 'Agent ID')
|
||||
.action(async (id) => {
|
||||
const spinner = (0, ora_1.default)(`Stopping agent ${id}...`).start();
|
||||
try {
|
||||
const coordinator = new SwarmCoordinator_js_1.SwarmCoordinator();
|
||||
const removed = await coordinator.removeAgent(id);
|
||||
if (removed) {
|
||||
spinner.succeed(chalk_1.default.green(`Agent ${id} stopped`));
|
||||
}
|
||||
else {
|
||||
spinner.fail(chalk_1.default.red(`Agent ${id} not found`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
spinner.fail(chalk_1.default.red(`Stop failed: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
// Status command
|
||||
agent
|
||||
.command('status')
|
||||
.description('Show agent/swarm status')
|
||||
.argument('[id]', 'Agent ID (optional)')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action(async (id, options) => {
|
||||
try {
|
||||
const coordinator = new SwarmCoordinator_js_1.SwarmCoordinator();
|
||||
if (id) {
|
||||
const agentStatus = coordinator.getAgent(id);
|
||||
if (!agentStatus) {
|
||||
console.log(chalk_1.default.red(`Agent ${id} not found`));
|
||||
process.exit(1);
|
||||
}
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(agentStatus, null, 2));
|
||||
return;
|
||||
}
|
||||
console.log(chalk_1.default.bold(`\n🤖 Agent: ${id}\n`));
|
||||
console.log('─'.repeat(40));
|
||||
console.log(`Status: ${agentStatus.status === 'busy' ? chalk_1.default.green(agentStatus.status) : chalk_1.default.yellow(agentStatus.status)}`);
|
||||
console.log(`Type: ${chalk_1.default.cyan(agentStatus.type)}`);
|
||||
console.log(`Completed: ${agentStatus.completedTasks}`);
|
||||
console.log(`Failed: ${agentStatus.failedTasks}`);
|
||||
if (agentStatus.currentTask) {
|
||||
console.log(`Task: ${agentStatus.currentTask}`);
|
||||
}
|
||||
console.log('─'.repeat(40));
|
||||
}
|
||||
else {
|
||||
// Show overall swarm status
|
||||
const status = coordinator.getStatus();
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(status, null, 2));
|
||||
return;
|
||||
}
|
||||
console.log(chalk_1.default.bold('\n🐝 Swarm Status\n'));
|
||||
console.log('─'.repeat(40));
|
||||
console.log(`Topology: ${chalk_1.default.cyan(status.topology)}`);
|
||||
console.log(`Consensus: ${chalk_1.default.cyan(status.consensus)}`);
|
||||
console.log(`Total Agents: ${chalk_1.default.cyan(status.agentCount)} / ${status.maxAgents}`);
|
||||
console.log(`Idle: ${chalk_1.default.yellow(status.idleAgents)}`);
|
||||
console.log(`Busy: ${chalk_1.default.green(status.busyAgents)}`);
|
||||
console.log(`Pending Tasks: ${chalk_1.default.yellow(status.pendingTasks)}`);
|
||||
console.log(`Running Tasks: ${chalk_1.default.blue(status.runningTasks)}`);
|
||||
console.log(`Completed: ${chalk_1.default.green(status.completedTasks)}`);
|
||||
console.log(`Failed: ${chalk_1.default.red(status.failedTasks)}`);
|
||||
console.log('─'.repeat(40));
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(chalk_1.default.red(`Status failed: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
// Swarm subcommands
|
||||
const swarm = agent.command('swarm').description('Swarm coordination commands');
|
||||
// Swarm init
|
||||
swarm
|
||||
.command('init')
|
||||
.description('Initialize swarm coordination')
|
||||
.option('--topology <topology>', 'Swarm topology: hierarchical, mesh, hierarchical-mesh, adaptive', 'hierarchical')
|
||||
.option('--max-agents <max>', 'Maximum agents', '8')
|
||||
.option('--strategy <strategy>', 'Coordination strategy: specialized, balanced, adaptive', 'specialized')
|
||||
.option('--consensus <consensus>', 'Consensus algorithm: raft, byzantine, gossip, crdt', 'raft')
|
||||
.action(async (options) => {
|
||||
const spinner = (0, ora_1.default)('Initializing swarm...').start();
|
||||
try {
|
||||
const coordinator = new SwarmCoordinator_js_1.SwarmCoordinator({
|
||||
topology: options.topology,
|
||||
maxAgents: parseInt(options.maxAgents, 10),
|
||||
strategy: options.strategy,
|
||||
consensus: options.consensus,
|
||||
});
|
||||
await coordinator.start();
|
||||
spinner.succeed(chalk_1.default.green('Swarm initialized'));
|
||||
console.log(chalk_1.default.gray(` Topology: ${options.topology}`));
|
||||
console.log(chalk_1.default.gray(` Max Agents: ${options.maxAgents}`));
|
||||
console.log(chalk_1.default.gray(` Strategy: ${options.strategy}`));
|
||||
console.log(chalk_1.default.gray(` Consensus: ${options.consensus}`));
|
||||
}
|
||||
catch (error) {
|
||||
spinner.fail(chalk_1.default.red(`Init failed: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
// Swarm status
|
||||
swarm
|
||||
.command('status')
|
||||
.description('Show swarm status')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
const coordinator = new SwarmCoordinator_js_1.SwarmCoordinator();
|
||||
const status = coordinator.getStatus();
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(status, null, 2));
|
||||
return;
|
||||
}
|
||||
console.log(chalk_1.default.bold('\n🐝 Swarm Status\n'));
|
||||
console.log('─'.repeat(50));
|
||||
console.log(`Topology: ${chalk_1.default.cyan(status.topology)}`);
|
||||
console.log(`Consensus: ${chalk_1.default.cyan(status.consensus)}`);
|
||||
console.log(`Total Agents: ${chalk_1.default.cyan(status.agentCount)}`);
|
||||
console.log(`Active: ${chalk_1.default.green(status.busyAgents)}`);
|
||||
console.log(`Idle: ${chalk_1.default.yellow(status.idleAgents)}`);
|
||||
console.log(`Pending Tasks: ${chalk_1.default.yellow(status.pendingTasks)}`);
|
||||
console.log(`Completed: ${chalk_1.default.green(status.completedTasks)}`);
|
||||
console.log('─'.repeat(50));
|
||||
}
|
||||
catch (error) {
|
||||
console.error(chalk_1.default.red(`Status failed: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
// Swarm dispatch (bonus command)
|
||||
swarm
|
||||
.command('dispatch')
|
||||
.description('Dispatch a task to the swarm')
|
||||
.requiredOption('-w, --worker <type>', 'Worker type')
|
||||
.requiredOption('--task <task>', 'Task type')
|
||||
.option('--content <content>', 'Task content')
|
||||
.option('--priority <priority>', 'Priority: low, normal, high, critical', 'normal')
|
||||
.action(async (options) => {
|
||||
const spinner = (0, ora_1.default)('Dispatching task...').start();
|
||||
try {
|
||||
const coordinator = new SwarmCoordinator_js_1.SwarmCoordinator();
|
||||
await coordinator.start();
|
||||
const task = await coordinator.dispatch({
|
||||
worker: options.worker,
|
||||
task: {
|
||||
type: options.task,
|
||||
content: options.content || {},
|
||||
},
|
||||
priority: options.priority,
|
||||
});
|
||||
spinner.succeed(chalk_1.default.green(`Task dispatched: ${task.id}`));
|
||||
console.log(chalk_1.default.gray(` Worker: ${task.worker}`));
|
||||
console.log(chalk_1.default.gray(` Type: ${task.type}`));
|
||||
console.log(chalk_1.default.gray(` Priority: ${task.priority}`));
|
||||
console.log(chalk_1.default.gray(` Status: ${task.status}`));
|
||||
}
|
||||
catch (error) {
|
||||
spinner.fail(chalk_1.default.red(`Dispatch failed: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
return agent;
|
||||
}
|
||||
exports.default = createAgentCommand;
|
||||
//# sourceMappingURL=agent.js.map
|
||||
1
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/agent.js.map
vendored
Normal file
1
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/agent.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
299
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/agent.ts
vendored
Normal file
299
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/agent.ts
vendored
Normal file
@@ -0,0 +1,299 @@
|
||||
/**
|
||||
* Agent Command - Agent and swarm management
|
||||
*
|
||||
* Commands:
|
||||
* agent spawn Spawn a new agent
|
||||
* agent list List running agents
|
||||
* agent stop Stop an agent
|
||||
* agent status Show agent status
|
||||
* swarm init Initialize swarm coordination
|
||||
* swarm status Show swarm status
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { SwarmCoordinator, type WorkerType } from '../../swarm/SwarmCoordinator.js';
|
||||
|
||||
const VALID_WORKER_TYPES: WorkerType[] = [
|
||||
'ultralearn', 'optimize', 'consolidate', 'predict', 'audit',
|
||||
'map', 'preload', 'deepdive', 'document', 'refactor', 'benchmark', 'testgaps'
|
||||
];
|
||||
|
||||
export function createAgentCommand(): Command {
|
||||
const agent = new Command('agent');
|
||||
agent.description('Agent and swarm management commands');
|
||||
|
||||
// Spawn command
|
||||
agent
|
||||
.command('spawn')
|
||||
.description('Spawn a new agent')
|
||||
.option('-t, --type <type>', 'Agent type (worker type)', 'optimize')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action(async (options) => {
|
||||
const spinner = ora(`Spawning ${options.type} agent...`).start();
|
||||
|
||||
try {
|
||||
const workerType = options.type as WorkerType;
|
||||
if (!VALID_WORKER_TYPES.includes(workerType)) {
|
||||
spinner.fail(chalk.red(`Invalid worker type: ${options.type}`));
|
||||
console.log(chalk.gray(`Valid types: ${VALID_WORKER_TYPES.join(', ')}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const coordinator = new SwarmCoordinator();
|
||||
await coordinator.start();
|
||||
|
||||
const spawnedAgent = await coordinator.spawnAgent(workerType);
|
||||
|
||||
spinner.stop();
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(spawnedAgent, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.green(`✓ Agent spawned: ${chalk.cyan(spawnedAgent.id)}`));
|
||||
console.log(chalk.gray(` Type: ${spawnedAgent.type}`));
|
||||
console.log(chalk.gray(` Status: ${spawnedAgent.status}`));
|
||||
} catch (error: any) {
|
||||
spinner.fail(chalk.red(`Spawn failed: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// List command
|
||||
agent
|
||||
.command('list')
|
||||
.description('List running agents')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
const coordinator = new SwarmCoordinator();
|
||||
const agents = coordinator.getAgents();
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(agents, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
if (agents.length === 0) {
|
||||
console.log(chalk.yellow('No agents running'));
|
||||
console.log(chalk.gray('Spawn one with: ruvbot agent spawn -t optimize'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.bold(`\n🤖 Agents (${agents.length})\n`));
|
||||
console.log('─'.repeat(70));
|
||||
console.log(
|
||||
chalk.gray('ID'.padEnd(40) + 'TYPE'.padEnd(15) + 'STATUS'.padEnd(12) + 'TASKS')
|
||||
);
|
||||
console.log('─'.repeat(70));
|
||||
|
||||
for (const a of agents) {
|
||||
const statusColor = a.status === 'busy' ? chalk.green : a.status === 'idle' ? chalk.yellow : chalk.gray;
|
||||
console.log(
|
||||
chalk.cyan(a.id.padEnd(40)) +
|
||||
a.type.padEnd(15) +
|
||||
statusColor(a.status.padEnd(12)) +
|
||||
chalk.gray(String(a.completedTasks))
|
||||
);
|
||||
}
|
||||
|
||||
console.log('─'.repeat(70));
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red(`List failed: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Stop command
|
||||
agent
|
||||
.command('stop')
|
||||
.description('Stop an agent')
|
||||
.argument('<id>', 'Agent ID')
|
||||
.action(async (id) => {
|
||||
const spinner = ora(`Stopping agent ${id}...`).start();
|
||||
|
||||
try {
|
||||
const coordinator = new SwarmCoordinator();
|
||||
const removed = await coordinator.removeAgent(id);
|
||||
|
||||
if (removed) {
|
||||
spinner.succeed(chalk.green(`Agent ${id} stopped`));
|
||||
} else {
|
||||
spinner.fail(chalk.red(`Agent ${id} not found`));
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error: any) {
|
||||
spinner.fail(chalk.red(`Stop failed: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Status command
|
||||
agent
|
||||
.command('status')
|
||||
.description('Show agent/swarm status')
|
||||
.argument('[id]', 'Agent ID (optional)')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action(async (id, options) => {
|
||||
try {
|
||||
const coordinator = new SwarmCoordinator();
|
||||
|
||||
if (id) {
|
||||
const agentStatus = coordinator.getAgent(id);
|
||||
|
||||
if (!agentStatus) {
|
||||
console.log(chalk.red(`Agent ${id} not found`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(agentStatus, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.bold(`\n🤖 Agent: ${id}\n`));
|
||||
console.log('─'.repeat(40));
|
||||
console.log(`Status: ${agentStatus.status === 'busy' ? chalk.green(agentStatus.status) : chalk.yellow(agentStatus.status)}`);
|
||||
console.log(`Type: ${chalk.cyan(agentStatus.type)}`);
|
||||
console.log(`Completed: ${agentStatus.completedTasks}`);
|
||||
console.log(`Failed: ${agentStatus.failedTasks}`);
|
||||
if (agentStatus.currentTask) {
|
||||
console.log(`Task: ${agentStatus.currentTask}`);
|
||||
}
|
||||
console.log('─'.repeat(40));
|
||||
} else {
|
||||
// Show overall swarm status
|
||||
const status = coordinator.getStatus();
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(status, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.bold('\n🐝 Swarm Status\n'));
|
||||
console.log('─'.repeat(40));
|
||||
console.log(`Topology: ${chalk.cyan(status.topology)}`);
|
||||
console.log(`Consensus: ${chalk.cyan(status.consensus)}`);
|
||||
console.log(`Total Agents: ${chalk.cyan(status.agentCount)} / ${status.maxAgents}`);
|
||||
console.log(`Idle: ${chalk.yellow(status.idleAgents)}`);
|
||||
console.log(`Busy: ${chalk.green(status.busyAgents)}`);
|
||||
console.log(`Pending Tasks: ${chalk.yellow(status.pendingTasks)}`);
|
||||
console.log(`Running Tasks: ${chalk.blue(status.runningTasks)}`);
|
||||
console.log(`Completed: ${chalk.green(status.completedTasks)}`);
|
||||
console.log(`Failed: ${chalk.red(status.failedTasks)}`);
|
||||
console.log('─'.repeat(40));
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red(`Status failed: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Swarm subcommands
|
||||
const swarm = agent.command('swarm').description('Swarm coordination commands');
|
||||
|
||||
// Swarm init
|
||||
swarm
|
||||
.command('init')
|
||||
.description('Initialize swarm coordination')
|
||||
.option('--topology <topology>', 'Swarm topology: hierarchical, mesh, hierarchical-mesh, adaptive', 'hierarchical')
|
||||
.option('--max-agents <max>', 'Maximum agents', '8')
|
||||
.option('--strategy <strategy>', 'Coordination strategy: specialized, balanced, adaptive', 'specialized')
|
||||
.option('--consensus <consensus>', 'Consensus algorithm: raft, byzantine, gossip, crdt', 'raft')
|
||||
.action(async (options) => {
|
||||
const spinner = ora('Initializing swarm...').start();
|
||||
|
||||
try {
|
||||
const coordinator = new SwarmCoordinator({
|
||||
topology: options.topology as any,
|
||||
maxAgents: parseInt(options.maxAgents, 10),
|
||||
strategy: options.strategy as any,
|
||||
consensus: options.consensus as any,
|
||||
});
|
||||
|
||||
await coordinator.start();
|
||||
|
||||
spinner.succeed(chalk.green('Swarm initialized'));
|
||||
console.log(chalk.gray(` Topology: ${options.topology}`));
|
||||
console.log(chalk.gray(` Max Agents: ${options.maxAgents}`));
|
||||
console.log(chalk.gray(` Strategy: ${options.strategy}`));
|
||||
console.log(chalk.gray(` Consensus: ${options.consensus}`));
|
||||
} catch (error: any) {
|
||||
spinner.fail(chalk.red(`Init failed: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Swarm status
|
||||
swarm
|
||||
.command('status')
|
||||
.description('Show swarm status')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
const coordinator = new SwarmCoordinator();
|
||||
const status = coordinator.getStatus();
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(status, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.bold('\n🐝 Swarm Status\n'));
|
||||
console.log('─'.repeat(50));
|
||||
console.log(`Topology: ${chalk.cyan(status.topology)}`);
|
||||
console.log(`Consensus: ${chalk.cyan(status.consensus)}`);
|
||||
console.log(`Total Agents: ${chalk.cyan(status.agentCount)}`);
|
||||
console.log(`Active: ${chalk.green(status.busyAgents)}`);
|
||||
console.log(`Idle: ${chalk.yellow(status.idleAgents)}`);
|
||||
console.log(`Pending Tasks: ${chalk.yellow(status.pendingTasks)}`);
|
||||
console.log(`Completed: ${chalk.green(status.completedTasks)}`);
|
||||
console.log('─'.repeat(50));
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red(`Status failed: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Swarm dispatch (bonus command)
|
||||
swarm
|
||||
.command('dispatch')
|
||||
.description('Dispatch a task to the swarm')
|
||||
.requiredOption('-w, --worker <type>', 'Worker type')
|
||||
.requiredOption('--task <task>', 'Task type')
|
||||
.option('--content <content>', 'Task content')
|
||||
.option('--priority <priority>', 'Priority: low, normal, high, critical', 'normal')
|
||||
.action(async (options) => {
|
||||
const spinner = ora('Dispatching task...').start();
|
||||
|
||||
try {
|
||||
const coordinator = new SwarmCoordinator();
|
||||
await coordinator.start();
|
||||
|
||||
const task = await coordinator.dispatch({
|
||||
worker: options.worker as WorkerType,
|
||||
task: {
|
||||
type: options.task,
|
||||
content: options.content || {},
|
||||
},
|
||||
priority: options.priority as any,
|
||||
});
|
||||
|
||||
spinner.succeed(chalk.green(`Task dispatched: ${task.id}`));
|
||||
console.log(chalk.gray(` Worker: ${task.worker}`));
|
||||
console.log(chalk.gray(` Type: ${task.type}`));
|
||||
console.log(chalk.gray(` Priority: ${task.priority}`));
|
||||
console.log(chalk.gray(` Status: ${task.status}`));
|
||||
} catch (error: any) {
|
||||
spinner.fail(chalk.red(`Dispatch failed: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
return agent;
|
||||
}
|
||||
|
||||
export default createAgentCommand;
|
||||
10
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/channels.d.ts
vendored
Normal file
10
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/channels.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* RuvBot CLI - Channels Command
|
||||
*
|
||||
* Setup and manage channel integrations (Slack, Discord, Telegram, Webhooks).
|
||||
*/
|
||||
import { Command } from 'commander';
|
||||
export declare function createChannelsCommand(): Command;
|
||||
export declare function createWebhooksCommand(): Command;
|
||||
export default createChannelsCommand;
|
||||
//# sourceMappingURL=channels.d.ts.map
|
||||
1
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/channels.d.ts.map
vendored
Normal file
1
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/channels.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"channels.d.ts","sourceRoot":"","sources":["channels.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,wBAAgB,qBAAqB,IAAI,OAAO,CA8G/C;AAgOD,wBAAgB,qBAAqB,IAAI,OAAO,CAiE/C;AAED,eAAe,qBAAqB,CAAC"}
|
||||
362
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/channels.js
vendored
Normal file
362
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/channels.js
vendored
Normal file
@@ -0,0 +1,362 @@
|
||||
"use strict";
|
||||
/**
|
||||
* RuvBot CLI - Channels Command
|
||||
*
|
||||
* Setup and manage channel integrations (Slack, Discord, Telegram, Webhooks).
|
||||
*/
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createChannelsCommand = createChannelsCommand;
|
||||
exports.createWebhooksCommand = createWebhooksCommand;
|
||||
const commander_1 = require("commander");
|
||||
const chalk_1 = __importDefault(require("chalk"));
|
||||
function createChannelsCommand() {
|
||||
const channels = new commander_1.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_1.default.bold('\n📡 Available Channel Integrations\n'));
|
||||
console.log('─'.repeat(60));
|
||||
for (const ch of channelList) {
|
||||
const icon = getChannelIcon(ch.name);
|
||||
console.log(`${icon} ${chalk_1.default.cyan(ch.name.padEnd(12))} ${ch.description}`);
|
||||
console.log(` Package: ${chalk_1.default.gray(ch.package)}`);
|
||||
console.log();
|
||||
}
|
||||
console.log('─'.repeat(60));
|
||||
console.log(chalk_1.default.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_1.default.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_1.default.cyan(`\nTesting ${normalizedChannel} connection...`));
|
||||
const envVars = getRequiredEnvVars(normalizedChannel);
|
||||
const missing = envVars.filter((v) => !process.env[v]);
|
||||
if (missing.length > 0) {
|
||||
console.log(chalk_1.default.red('\n✗ Missing environment variables:'));
|
||||
missing.forEach((v) => console.log(chalk_1.default.red(` - ${v}`)));
|
||||
console.log(chalk_1.default.gray(`\nRun 'ruvbot channels setup ${normalizedChannel}' for instructions`));
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(chalk_1.default.green('✓ All required environment variables are set'));
|
||||
console.log(chalk_1.default.gray('\nStart the bot with:'));
|
||||
console.log(chalk_1.default.cyan(` ruvbot start --channel ${normalizedChannel}`));
|
||||
});
|
||||
return channels;
|
||||
}
|
||||
function getChannelIcon(channel) {
|
||||
const icons = {
|
||||
slack: '💬',
|
||||
discord: '🎮',
|
||||
telegram: '✈️',
|
||||
webhook: '🔗',
|
||||
};
|
||||
return icons[channel] || '📡';
|
||||
}
|
||||
function getRequiredEnvVars(channel) {
|
||||
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() {
|
||||
console.log(chalk_1.default.bold('\n💬 Slack Integration Setup\n'));
|
||||
console.log('═'.repeat(60));
|
||||
console.log(chalk_1.default.bold('\n📋 Step 1: Create a Slack App\n'));
|
||||
console.log(' 1. Go to: ' + chalk_1.default.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_1.default.bold('\n🔐 Step 2: Configure Bot Permissions\n'));
|
||||
console.log(' Navigate to OAuth & Permissions and add these Bot Token Scopes:');
|
||||
console.log(chalk_1.default.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_1.default.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_1.default.cyan('connections:write') + ' scope');
|
||||
console.log(' 3. Save the ' + chalk_1.default.yellow('xapp-...') + ' token');
|
||||
console.log(chalk_1.default.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_1.default.yellow('xoxb-...'));
|
||||
console.log(' 3. Copy Signing Secret from Basic Information');
|
||||
console.log(chalk_1.default.bold('\n🔧 Step 5: Configure Environment\n'));
|
||||
console.log(chalk_1.default.gray(' ─────────────────────────────────────'));
|
||||
console.log(chalk_1.default.cyan(' export SLACK_BOT_TOKEN="xoxb-your-bot-token"'));
|
||||
console.log(chalk_1.default.cyan(' export SLACK_SIGNING_SECRET="your-signing-secret"'));
|
||||
console.log(chalk_1.default.cyan(' export SLACK_APP_TOKEN="xapp-your-app-token"'));
|
||||
console.log(chalk_1.default.bold('\n🚀 Step 6: Start RuvBot\n'));
|
||||
console.log(chalk_1.default.cyan(' ruvbot start --channel slack'));
|
||||
console.log(chalk_1.default.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_1.default.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_1.default.gray('Install optional dependency: npm install @slack/bolt @slack/web-api\n'));
|
||||
}
|
||||
function printDiscordSetup() {
|
||||
console.log(chalk_1.default.bold('\n🎮 Discord Integration Setup\n'));
|
||||
console.log('═'.repeat(60));
|
||||
console.log(chalk_1.default.bold('\n📋 Step 1: Create a Discord Application\n'));
|
||||
console.log(' 1. Go to: ' + chalk_1.default.cyan('https://discord.com/developers/applications'));
|
||||
console.log(' 2. Click "New Application" and name it');
|
||||
console.log(chalk_1.default.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_1.default.green(' ✓ MESSAGE CONTENT INTENT'));
|
||||
console.log(chalk_1.default.green(' ✓ SERVER MEMBERS INTENT'));
|
||||
console.log(' 3. Click "Reset Token" and copy the bot token');
|
||||
console.log(chalk_1.default.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_1.default.bold('\n📨 Step 4: Invite Bot to Server\n'));
|
||||
console.log(' 1. Go to OAuth2 → URL Generator');
|
||||
console.log(' 2. Select scopes: ' + chalk_1.default.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_1.default.bold('\n🔧 Step 5: Configure Environment\n'));
|
||||
console.log(chalk_1.default.gray(' ─────────────────────────────────────'));
|
||||
console.log(chalk_1.default.cyan(' export DISCORD_TOKEN="your-bot-token"'));
|
||||
console.log(chalk_1.default.cyan(' export DISCORD_CLIENT_ID="your-application-id"'));
|
||||
console.log(chalk_1.default.cyan(' export DISCORD_GUILD_ID="your-server-id" # Optional'));
|
||||
console.log(chalk_1.default.bold('\n🚀 Step 6: Start RuvBot\n'));
|
||||
console.log(chalk_1.default.cyan(' ruvbot start --channel discord'));
|
||||
console.log('\n' + '═'.repeat(60));
|
||||
console.log(chalk_1.default.gray('Install optional dependency: npm install discord.js\n'));
|
||||
}
|
||||
function printTelegramSetup() {
|
||||
console.log(chalk_1.default.bold('\n✈️ Telegram Integration Setup\n'));
|
||||
console.log('═'.repeat(60));
|
||||
console.log(chalk_1.default.bold('\n📋 Step 1: Create a Bot with BotFather\n'));
|
||||
console.log(' 1. Open Telegram and search for ' + chalk_1.default.cyan('@BotFather'));
|
||||
console.log(' 2. Send ' + chalk_1.default.cyan('/newbot') + ' command');
|
||||
console.log(' 3. Follow prompts to name your bot');
|
||||
console.log(' 4. Copy the HTTP API token (format: ' + chalk_1.default.yellow('123456789:ABC-DEF...') + ')');
|
||||
console.log(chalk_1.default.bold('\n🔧 Step 2: Configure Environment\n'));
|
||||
console.log(chalk_1.default.gray(' ─────────────────────────────────────'));
|
||||
console.log(chalk_1.default.cyan(' export TELEGRAM_BOT_TOKEN="your-bot-token"'));
|
||||
console.log(chalk_1.default.bold('\n🚀 Step 3: Start RuvBot (Polling Mode)\n'));
|
||||
console.log(chalk_1.default.cyan(' ruvbot start --channel telegram'));
|
||||
console.log(chalk_1.default.bold('\n🌐 Webhook Mode (for Production/Cloud Run)\n'));
|
||||
console.log(' For serverless deployments, use webhook mode:');
|
||||
console.log(chalk_1.default.gray(' ─────────────────────────────────────'));
|
||||
console.log(chalk_1.default.cyan(' export TELEGRAM_BOT_TOKEN="your-bot-token"'));
|
||||
console.log(chalk_1.default.cyan(' export TELEGRAM_WEBHOOK_URL="https://your-ruvbot.run.app/telegram/webhook"'));
|
||||
console.log(chalk_1.default.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_1.default.cyan('/start'));
|
||||
console.log(' 3. Send messages to interact with RuvBot');
|
||||
console.log(chalk_1.default.bold('\n⚙️ Optional: Set Bot Commands\n'));
|
||||
console.log(' Send to @BotFather:');
|
||||
console.log(chalk_1.default.cyan(' /setcommands'));
|
||||
console.log(' Then paste:');
|
||||
console.log(chalk_1.default.gray(' start - Start the bot'));
|
||||
console.log(chalk_1.default.gray(' help - Show help message'));
|
||||
console.log(chalk_1.default.gray(' status - Check bot status'));
|
||||
console.log('\n' + '═'.repeat(60));
|
||||
console.log(chalk_1.default.gray('Install optional dependency: npm install telegraf\n'));
|
||||
}
|
||||
function printWebhookSetup() {
|
||||
console.log(chalk_1.default.bold('\n🔗 Webhook Integration Setup\n'));
|
||||
console.log('═'.repeat(60));
|
||||
console.log(chalk_1.default.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_1.default.bold('\n🔌 Available Webhook Endpoints\n'));
|
||||
console.log(chalk_1.default.gray(' ─────────────────────────────────────'));
|
||||
console.log(` POST ${chalk_1.default.cyan('/webhook/message')} - Receive messages`);
|
||||
console.log(` POST ${chalk_1.default.cyan('/webhook/event')} - Receive events`);
|
||||
console.log(` GET ${chalk_1.default.cyan('/webhook/health')} - Health check`);
|
||||
console.log(` POST ${chalk_1.default.cyan('/api/sessions/:id/chat')} - Chat endpoint`);
|
||||
console.log(chalk_1.default.bold('\n📤 Outbound Webhooks\n'));
|
||||
console.log(' Configure RuvBot to send responses to your endpoint:');
|
||||
console.log(chalk_1.default.gray(' ─────────────────────────────────────'));
|
||||
console.log(chalk_1.default.cyan(' export WEBHOOK_URL="https://your-service.com/callback"'));
|
||||
console.log(chalk_1.default.cyan(' export WEBHOOK_SECRET="your-shared-secret"'));
|
||||
console.log(chalk_1.default.bold('\n📥 Inbound Webhook Format\n'));
|
||||
console.log(' Send POST requests with JSON body:');
|
||||
console.log(chalk_1.default.gray(' ─────────────────────────────────────'));
|
||||
console.log(chalk_1.default.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_1.default.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_1.default.bold('\n📋 Configuration File\n'));
|
||||
console.log(chalk_1.default.gray(' ─────────────────────────────────────'));
|
||||
console.log(chalk_1.default.cyan(` {
|
||||
"channels": {
|
||||
"webhook": {
|
||||
"enabled": true,
|
||||
"inbound": {
|
||||
"path": "/webhook/message",
|
||||
"secret": "\${WEBHOOK_SECRET}"
|
||||
},
|
||||
"outbound": {
|
||||
"url": "\${WEBHOOK_URL}",
|
||||
"retries": 3,
|
||||
"timeout": 30000
|
||||
}
|
||||
}
|
||||
}
|
||||
}`));
|
||||
console.log(chalk_1.default.bold('\n🚀 Start with Webhook Support\n'));
|
||||
console.log(chalk_1.default.cyan(' ruvbot start --port 3000'));
|
||||
console.log(chalk_1.default.gray(' # Webhooks are always available on the API server'));
|
||||
console.log('\n' + '═'.repeat(60) + '\n');
|
||||
}
|
||||
function createWebhooksCommand() {
|
||||
const webhooks = new commander_1.Command('webhooks')
|
||||
.alias('wh')
|
||||
.description('Configure webhook integrations');
|
||||
// List webhooks
|
||||
webhooks
|
||||
.command('list')
|
||||
.description('List configured webhooks')
|
||||
.action(() => {
|
||||
console.log(chalk_1.default.bold('\n🔗 Configured Webhooks\n'));
|
||||
console.log('─'.repeat(50));
|
||||
const outboundUrl = process.env.WEBHOOK_URL;
|
||||
if (outboundUrl) {
|
||||
console.log(chalk_1.default.green('✓ Outbound webhook:'), outboundUrl);
|
||||
}
|
||||
else {
|
||||
console.log(chalk_1.default.gray('○ No outbound webhook configured'));
|
||||
}
|
||||
console.log();
|
||||
console.log('Inbound endpoints (always available):');
|
||||
console.log(` POST ${chalk_1.default.cyan('/webhook/message')}`);
|
||||
console.log(` POST ${chalk_1.default.cyan('/webhook/event')}`);
|
||||
console.log(` POST ${chalk_1.default.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_1.default.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_1.default.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_1.default.red('✗ Webhook failed'));
|
||||
console.log(` Status: ${response.status}`);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.log(chalk_1.default.red('✗ Failed to reach webhook'));
|
||||
console.log(` Error: ${error instanceof Error ? error.message : 'Unknown'}`);
|
||||
}
|
||||
});
|
||||
return webhooks;
|
||||
}
|
||||
exports.default = createChannelsCommand;
|
||||
//# sourceMappingURL=channels.js.map
|
||||
1
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/channels.js.map
vendored
Normal file
1
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/channels.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
411
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/channels.ts
vendored
Normal file
411
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/channels.ts
vendored
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;
|
||||
9
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/deploy.d.ts
vendored
Normal file
9
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/deploy.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* RuvBot CLI - Deploy Command
|
||||
*
|
||||
* Deploy RuvBot to various cloud platforms with interactive wizards.
|
||||
*/
|
||||
import { Command } from 'commander';
|
||||
export declare function createDeploymentCommand(): Command;
|
||||
export default createDeploymentCommand;
|
||||
//# sourceMappingURL=deploy.d.ts.map
|
||||
1
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/deploy.d.ts.map
vendored
Normal file
1
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/deploy.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["deploy.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,wBAAgB,uBAAuB,IAAI,OAAO,CAgEjD;AA4ZD,eAAe,uBAAuB,CAAC"}
|
||||
472
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/deploy.js
vendored
Normal file
472
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/deploy.js
vendored
Normal file
@@ -0,0 +1,472 @@
|
||||
"use strict";
|
||||
/**
|
||||
* RuvBot CLI - Deploy Command
|
||||
*
|
||||
* Deploy RuvBot to various cloud platforms with interactive wizards.
|
||||
*/
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createDeploymentCommand = createDeploymentCommand;
|
||||
const commander_1 = require("commander");
|
||||
const chalk_1 = __importDefault(require("chalk"));
|
||||
const ora_1 = __importDefault(require("ora"));
|
||||
const child_process_1 = require("child_process");
|
||||
function createDeploymentCommand() {
|
||||
const deploy = new commander_1.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) {
|
||||
console.log(chalk_1.default.bold('\n☁️ Google Cloud Run Deployment\n'));
|
||||
console.log('═'.repeat(50));
|
||||
// Check gcloud
|
||||
if (!commandExists('gcloud')) {
|
||||
console.error(chalk_1.default.red('\n✗ gcloud CLI is required'));
|
||||
console.log(chalk_1.default.gray(' Install from: https://cloud.google.com/sdk'));
|
||||
process.exit(1);
|
||||
}
|
||||
const spinner = (0, ora_1.default)('Checking gcloud authentication...').start();
|
||||
try {
|
||||
// Check authentication
|
||||
const account = (0, child_process_1.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_1.default.yellow('\nRun: gcloud auth login'));
|
||||
process.exit(1);
|
||||
}
|
||||
spinner.succeed(`Authenticated as ${account}`);
|
||||
// Get or prompt for project
|
||||
let projectId = options.project;
|
||||
if (!projectId) {
|
||||
projectId = (0, child_process_1.execSync)('gcloud config get-value project', {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
}).trim();
|
||||
if (!projectId) {
|
||||
console.error(chalk_1.default.red('\n✗ No project ID specified'));
|
||||
console.log(chalk_1.default.gray(' Use --project <id> or run: gcloud config set project <id>'));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
console.log(chalk_1.default.cyan(` Project: ${projectId}`));
|
||||
console.log(chalk_1.default.cyan(` Region: ${options.region}`));
|
||||
console.log(chalk_1.default.cyan(` Service: ${options.service}`));
|
||||
// Enable APIs
|
||||
spinner.start('Enabling required APIs...');
|
||||
(0, child_process_1.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 Promise.resolve().then(() => __importStar(require('fs/promises')));
|
||||
const envContent = await fs.readFile(options.envFile, '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 Promise.resolve().then(() => __importStar(require('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(' ');
|
||||
(0, child_process_1.execSync)(deployCmd, { stdio: 'inherit' });
|
||||
// Get URL
|
||||
const serviceUrl = (0, child_process_1.execSync)(`gcloud run services describe ${options.service} --region ${options.region} --format='value(status.url)'`, { encoding: 'utf-8' }).trim();
|
||||
console.log('\n' + chalk_1.default.green('═'.repeat(50)));
|
||||
console.log(chalk_1.default.bold.green('🚀 Deployment successful!'));
|
||||
console.log(chalk_1.default.green('═'.repeat(50)));
|
||||
console.log(`\n URL: ${chalk_1.default.cyan(serviceUrl)}`);
|
||||
console.log(` Health: ${chalk_1.default.cyan(serviceUrl + '/health')}`);
|
||||
console.log(` API: ${chalk_1.default.cyan(serviceUrl + '/api/status')}`);
|
||||
console.log(`\n Test: ${chalk_1.default.gray(`curl ${serviceUrl}/health`)}`);
|
||||
console.log();
|
||||
}
|
||||
catch (error) {
|
||||
spinner.fail('Deployment failed');
|
||||
console.error(chalk_1.default.red(`\nError: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
async function deployToDocker(options) {
|
||||
console.log(chalk_1.default.bold('\n🐳 Docker Deployment\n'));
|
||||
console.log('═'.repeat(50));
|
||||
if (!commandExists('docker')) {
|
||||
console.error(chalk_1.default.red('\n✗ Docker is required'));
|
||||
console.log(chalk_1.default.gray(' Install from: https://docker.com'));
|
||||
process.exit(1);
|
||||
}
|
||||
const fs = await Promise.resolve().then(() => __importStar(require('fs/promises')));
|
||||
const spinner = (0, ora_1.default)('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...');
|
||||
(0, child_process_1.execSync)('docker-compose up -d', { stdio: 'pipe' });
|
||||
spinner.succeed('Containers started');
|
||||
console.log('\n' + chalk_1.default.green('═'.repeat(50)));
|
||||
console.log(chalk_1.default.bold.green('🚀 RuvBot is running!'));
|
||||
console.log(chalk_1.default.green('═'.repeat(50)));
|
||||
console.log(`\n URL: ${chalk_1.default.cyan(`http://localhost:${options.port}`)}`);
|
||||
console.log(` Health: ${chalk_1.default.cyan(`http://localhost:${options.port}/health`)}`);
|
||||
console.log(`\n Logs: ${chalk_1.default.gray('docker-compose logs -f')}`);
|
||||
console.log(` Stop: ${chalk_1.default.gray('docker-compose down')}`);
|
||||
console.log();
|
||||
}
|
||||
else {
|
||||
console.log(chalk_1.default.cyan('\nRun: docker-compose up'));
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
spinner.fail('Docker deployment failed');
|
||||
console.error(chalk_1.default.red(`\nError: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
async function deployToK8s(options) {
|
||||
console.log(chalk_1.default.bold('\n☸️ Kubernetes Deployment\n'));
|
||||
console.log('═'.repeat(50));
|
||||
if (!commandExists('kubectl')) {
|
||||
console.error(chalk_1.default.red('\n✗ kubectl is required'));
|
||||
console.log(chalk_1.default.gray(' Install from: https://kubernetes.io/docs/tasks/tools/'));
|
||||
process.exit(1);
|
||||
}
|
||||
const fs = await Promise.resolve().then(() => __importStar(require('fs/promises')));
|
||||
const spinner = (0, ora_1.default)('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_1.default.yellow('⚠️ Before applying:'));
|
||||
console.log(chalk_1.default.gray(' 1. Edit k8s/secret.yaml with your API keys'));
|
||||
console.log(chalk_1.default.gray(' 2. Review k8s/deployment.yaml'));
|
||||
console.log('\n Apply with:');
|
||||
console.log(chalk_1.default.cyan(' kubectl apply -f k8s/'));
|
||||
console.log('\n Check status:');
|
||||
console.log(chalk_1.default.cyan(' kubectl get pods -l app=ruvbot'));
|
||||
console.log();
|
||||
}
|
||||
catch (error) {
|
||||
spinner.fail('Kubernetes manifest creation failed');
|
||||
console.error(chalk_1.default.red(`\nError: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
async function runDeploymentWizard() {
|
||||
console.log(chalk_1.default.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_1.default.cyan('ruvbot deploy-cloud cloudrun'));
|
||||
console.log();
|
||||
console.log(' 2. Docker (local or server deployment)');
|
||||
console.log(' ' + chalk_1.default.cyan('ruvbot deploy-cloud docker'));
|
||||
console.log();
|
||||
console.log(' 3. Kubernetes (production cluster)');
|
||||
console.log(' ' + chalk_1.default.cyan('ruvbot deploy-cloud k8s'));
|
||||
console.log();
|
||||
console.log('For interactive setup, use the install script:');
|
||||
console.log(chalk_1.default.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) {
|
||||
const platform = options.platform;
|
||||
console.log(chalk_1.default.bold('\n📊 Deployment Status\n'));
|
||||
if (!platform || platform === 'cloudrun') {
|
||||
console.log(chalk_1.default.cyan('Cloud Run:'));
|
||||
if (commandExists('gcloud')) {
|
||||
try {
|
||||
const services = (0, child_process_1.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_1.default.gray(' Not configured or no services'));
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.log(chalk_1.default.gray(' gcloud CLI not installed'));
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
if (!platform || platform === 'docker') {
|
||||
console.log(chalk_1.default.cyan('Docker:'));
|
||||
if (commandExists('docker')) {
|
||||
try {
|
||||
const containers = (0, child_process_1.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_1.default.gray(' No containers found'));
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.log(chalk_1.default.gray(' Docker not installed'));
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
if (!platform || platform === 'k8s') {
|
||||
console.log(chalk_1.default.cyan('Kubernetes:'));
|
||||
if (commandExists('kubectl')) {
|
||||
try {
|
||||
const pods = (0, child_process_1.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_1.default.gray(' No pods found or not configured'));
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.log(chalk_1.default.gray(' kubectl not installed'));
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
function commandExists(cmd) {
|
||||
try {
|
||||
(0, child_process_1.execSync)(`which ${cmd}`, { stdio: 'pipe' });
|
||||
return true;
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
exports.default = createDeploymentCommand;
|
||||
//# sourceMappingURL=deploy.js.map
|
||||
1
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/deploy.js.map
vendored
Normal file
1
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/deploy.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
488
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/deploy.ts
vendored
Normal file
488
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/deploy.ts
vendored
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;
|
||||
17
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/doctor.d.ts
vendored
Normal file
17
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/doctor.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Doctor Command - System diagnostics and health checks
|
||||
*
|
||||
* Checks:
|
||||
* - Node.js version
|
||||
* - Required dependencies
|
||||
* - Environment variables
|
||||
* - Database connectivity
|
||||
* - LLM provider connectivity
|
||||
* - Memory system
|
||||
* - Security configuration
|
||||
* - Plugin system
|
||||
*/
|
||||
import { Command } from 'commander';
|
||||
export declare function createDoctorCommand(): Command;
|
||||
export default createDoctorCommand;
|
||||
//# sourceMappingURL=doctor.d.ts.map
|
||||
1
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/doctor.d.ts.map
vendored
Normal file
1
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/doctor.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["doctor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWpC,wBAAgB,mBAAmB,IAAI,OAAO,CA+F7C;AAuVD,eAAe,mBAAmB,CAAC"}
|
||||
458
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/doctor.js
vendored
Normal file
458
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/doctor.js
vendored
Normal file
@@ -0,0 +1,458 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Doctor Command - System diagnostics and health checks
|
||||
*
|
||||
* Checks:
|
||||
* - Node.js version
|
||||
* - Required dependencies
|
||||
* - Environment variables
|
||||
* - Database connectivity
|
||||
* - LLM provider connectivity
|
||||
* - Memory system
|
||||
* - Security configuration
|
||||
* - Plugin system
|
||||
*/
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createDoctorCommand = createDoctorCommand;
|
||||
const commander_1 = require("commander");
|
||||
const chalk_1 = __importDefault(require("chalk"));
|
||||
const ora_1 = __importDefault(require("ora"));
|
||||
function createDoctorCommand() {
|
||||
const doctor = new commander_1.Command('doctor');
|
||||
doctor
|
||||
.description('Run diagnostics and health checks')
|
||||
.option('--fix', 'Attempt to fix issues automatically')
|
||||
.option('--json', 'Output results as JSON')
|
||||
.option('-v, --verbose', 'Show detailed information')
|
||||
.action(async (options) => {
|
||||
const results = [];
|
||||
const spinner = (0, ora_1.default)('Running diagnostics...').start();
|
||||
try {
|
||||
// Check Node.js version
|
||||
results.push(await checkNodeVersion());
|
||||
// Check environment variables
|
||||
results.push(...await checkEnvironment());
|
||||
// Check dependencies
|
||||
results.push(...await checkDependencies());
|
||||
// Check database connectivity
|
||||
results.push(await checkDatabase());
|
||||
// Check LLM providers
|
||||
results.push(...await checkLLMProviders());
|
||||
// Check memory system
|
||||
results.push(await checkMemorySystem());
|
||||
// Check security configuration
|
||||
results.push(await checkSecurity());
|
||||
// Check plugin system
|
||||
results.push(await checkPlugins());
|
||||
// Check disk space
|
||||
results.push(await checkDiskSpace());
|
||||
spinner.stop();
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(results, null, 2));
|
||||
return;
|
||||
}
|
||||
// Display results
|
||||
console.log(chalk_1.default.bold('\n🏥 RuvBot Doctor Results\n'));
|
||||
console.log('─'.repeat(60));
|
||||
let passCount = 0;
|
||||
let warnCount = 0;
|
||||
let failCount = 0;
|
||||
for (const result of results) {
|
||||
const icon = result.status === 'pass' ? '✓' : result.status === 'warn' ? '⚠' : '✗';
|
||||
const color = result.status === 'pass' ? chalk_1.default.green : result.status === 'warn' ? chalk_1.default.yellow : chalk_1.default.red;
|
||||
console.log(color(`${icon} ${result.name}`));
|
||||
if (options.verbose || result.status !== 'pass') {
|
||||
console.log(chalk_1.default.gray(` ${result.message}`));
|
||||
}
|
||||
if (result.fix && result.status !== 'pass') {
|
||||
console.log(chalk_1.default.cyan(` Fix: ${result.fix}`));
|
||||
}
|
||||
if (result.status === 'pass')
|
||||
passCount++;
|
||||
else if (result.status === 'warn')
|
||||
warnCount++;
|
||||
else
|
||||
failCount++;
|
||||
}
|
||||
console.log('─'.repeat(60));
|
||||
console.log(`\nSummary: ${chalk_1.default.green(passCount + ' passed')}, ` +
|
||||
`${chalk_1.default.yellow(warnCount + ' warnings')}, ` +
|
||||
`${chalk_1.default.red(failCount + ' failed')}`);
|
||||
if (failCount > 0) {
|
||||
console.log(chalk_1.default.red('\n⚠ Some checks failed. Run with --fix to attempt automatic fixes.'));
|
||||
process.exit(1);
|
||||
}
|
||||
else if (warnCount > 0) {
|
||||
console.log(chalk_1.default.yellow('\n⚠ Some warnings detected. Review and address if needed.'));
|
||||
}
|
||||
else {
|
||||
console.log(chalk_1.default.green('\n✓ All checks passed! RuvBot is healthy.'));
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
spinner.fail(chalk_1.default.red('Diagnostics failed'));
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
return doctor;
|
||||
}
|
||||
async function checkNodeVersion() {
|
||||
const version = process.version;
|
||||
const major = parseInt(version.slice(1).split('.')[0], 10);
|
||||
if (major >= 20) {
|
||||
return { name: 'Node.js Version', status: 'pass', message: `${version} (recommended)` };
|
||||
}
|
||||
else if (major >= 18) {
|
||||
return { name: 'Node.js Version', status: 'warn', message: `${version} (18+ supported, 20+ recommended)` };
|
||||
}
|
||||
else {
|
||||
return {
|
||||
name: 'Node.js Version',
|
||||
status: 'fail',
|
||||
message: `${version} (requires 18+)`,
|
||||
fix: 'Install Node.js 20 LTS from https://nodejs.org',
|
||||
};
|
||||
}
|
||||
}
|
||||
async function checkEnvironment() {
|
||||
const results = [];
|
||||
// Check for .env file
|
||||
const fs = await Promise.resolve().then(() => __importStar(require('fs/promises')));
|
||||
try {
|
||||
await fs.access('.env');
|
||||
results.push({ name: 'Environment File', status: 'pass', message: '.env file found' });
|
||||
}
|
||||
catch {
|
||||
results.push({
|
||||
name: 'Environment File',
|
||||
status: 'warn',
|
||||
message: 'No .env file found',
|
||||
fix: 'Copy .env.example to .env and configure',
|
||||
});
|
||||
}
|
||||
// Check critical environment variables
|
||||
const criticalVars = ['ANTHROPIC_API_KEY', 'OPENROUTER_API_KEY', 'OPENAI_API_KEY'];
|
||||
const hasApiKey = criticalVars.some((v) => process.env[v]);
|
||||
if (hasApiKey) {
|
||||
results.push({ name: 'LLM API Key', status: 'pass', message: 'At least one LLM API key configured' });
|
||||
}
|
||||
else {
|
||||
results.push({
|
||||
name: 'LLM API Key',
|
||||
status: 'warn',
|
||||
message: 'No LLM API key found',
|
||||
fix: 'Set ANTHROPIC_API_KEY or OPENROUTER_API_KEY in .env',
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
async function checkDependencies() {
|
||||
const results = [];
|
||||
// Check if package.json exists
|
||||
const fs = await Promise.resolve().then(() => __importStar(require('fs/promises')));
|
||||
try {
|
||||
const pkg = JSON.parse(await fs.readFile('package.json', 'utf-8'));
|
||||
const hasRuvbot = pkg.dependencies?.['@ruvector/ruvbot'] || pkg.devDependencies?.['@ruvector/ruvbot'];
|
||||
if (hasRuvbot) {
|
||||
results.push({ name: 'RuvBot Package', status: 'pass', message: '@ruvector/ruvbot installed' });
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Not in a project directory, skip this check
|
||||
}
|
||||
// Check for optional dependencies
|
||||
const optionalDeps = [
|
||||
{ name: '@slack/bolt', desc: 'Slack integration' },
|
||||
{ name: 'pg', desc: 'PostgreSQL support' },
|
||||
{ name: 'ioredis', desc: 'Redis caching' },
|
||||
];
|
||||
for (const dep of optionalDeps) {
|
||||
try {
|
||||
await Promise.resolve(`${dep.name}`).then(s => __importStar(require(s)));
|
||||
results.push({ name: dep.desc, status: 'pass', message: `${dep.name} available` });
|
||||
}
|
||||
catch {
|
||||
results.push({
|
||||
name: dep.desc,
|
||||
status: 'warn',
|
||||
message: `${dep.name} not installed (optional)`,
|
||||
fix: `npm install ${dep.name}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
async function checkDatabase() {
|
||||
const storageType = process.env.RUVBOT_STORAGE_TYPE || 'sqlite';
|
||||
if (storageType === 'sqlite') {
|
||||
const fs = await Promise.resolve().then(() => __importStar(require('fs/promises')));
|
||||
const dbPath = process.env.RUVBOT_SQLITE_PATH || './data/ruvbot.db';
|
||||
try {
|
||||
await fs.access(dbPath);
|
||||
return { name: 'Database (SQLite)', status: 'pass', message: `Database found at ${dbPath}` };
|
||||
}
|
||||
catch {
|
||||
return {
|
||||
name: 'Database (SQLite)',
|
||||
status: 'warn',
|
||||
message: `Database not found at ${dbPath}`,
|
||||
fix: 'Run `ruvbot init` to create database',
|
||||
};
|
||||
}
|
||||
}
|
||||
else if (storageType === 'postgres') {
|
||||
const dbUrl = process.env.DATABASE_URL;
|
||||
if (!dbUrl) {
|
||||
return {
|
||||
name: 'Database (PostgreSQL)',
|
||||
status: 'fail',
|
||||
message: 'DATABASE_URL not configured',
|
||||
fix: 'Set DATABASE_URL in .env',
|
||||
};
|
||||
}
|
||||
try {
|
||||
const { default: pg } = await Promise.resolve().then(() => __importStar(require('pg')));
|
||||
const client = new pg.Client(dbUrl);
|
||||
await client.connect();
|
||||
await client.query('SELECT 1');
|
||||
await client.end();
|
||||
return { name: 'Database (PostgreSQL)', status: 'pass', message: 'Connection successful' };
|
||||
}
|
||||
catch (error) {
|
||||
return {
|
||||
name: 'Database (PostgreSQL)',
|
||||
status: 'fail',
|
||||
message: `Connection failed: ${error.message}`,
|
||||
fix: 'Check DATABASE_URL and ensure PostgreSQL is running',
|
||||
};
|
||||
}
|
||||
}
|
||||
return { name: 'Database', status: 'pass', message: `Using ${storageType} storage` };
|
||||
}
|
||||
async function checkLLMProviders() {
|
||||
const results = [];
|
||||
// Check Anthropic
|
||||
if (process.env.ANTHROPIC_API_KEY) {
|
||||
try {
|
||||
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'x-api-key': process.env.ANTHROPIC_API_KEY,
|
||||
'anthropic-version': '2023-06-01',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'claude-3-haiku-20240307',
|
||||
max_tokens: 1,
|
||||
messages: [{ role: 'user', content: 'hi' }],
|
||||
}),
|
||||
});
|
||||
if (response.ok || response.status === 400) {
|
||||
// 400 means API key is valid but request is bad (expected with minimal request)
|
||||
results.push({ name: 'Anthropic API', status: 'pass', message: 'API key valid' });
|
||||
}
|
||||
else if (response.status === 401) {
|
||||
results.push({
|
||||
name: 'Anthropic API',
|
||||
status: 'fail',
|
||||
message: 'Invalid API key',
|
||||
fix: 'Check ANTHROPIC_API_KEY in .env',
|
||||
});
|
||||
}
|
||||
else {
|
||||
results.push({ name: 'Anthropic API', status: 'warn', message: `Status: ${response.status}` });
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
results.push({
|
||||
name: 'Anthropic API',
|
||||
status: 'fail',
|
||||
message: `Connection failed: ${error.message}`,
|
||||
fix: 'Check network connectivity',
|
||||
});
|
||||
}
|
||||
}
|
||||
// Check OpenRouter
|
||||
if (process.env.OPENROUTER_API_KEY) {
|
||||
try {
|
||||
const response = await fetch('https://openrouter.ai/api/v1/models', {
|
||||
headers: { Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}` },
|
||||
});
|
||||
if (response.ok) {
|
||||
results.push({ name: 'OpenRouter API', status: 'pass', message: 'API key valid' });
|
||||
}
|
||||
else {
|
||||
results.push({
|
||||
name: 'OpenRouter API',
|
||||
status: 'fail',
|
||||
message: 'Invalid API key',
|
||||
fix: 'Check OPENROUTER_API_KEY in .env',
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
results.push({
|
||||
name: 'OpenRouter API',
|
||||
status: 'fail',
|
||||
message: `Connection failed: ${error.message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (results.length === 0) {
|
||||
results.push({
|
||||
name: 'LLM Providers',
|
||||
status: 'warn',
|
||||
message: 'No LLM providers configured',
|
||||
fix: 'Set ANTHROPIC_API_KEY or OPENROUTER_API_KEY',
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
async function checkMemorySystem() {
|
||||
const memoryPath = process.env.RUVBOT_MEMORY_PATH || './data/memory';
|
||||
const fs = await Promise.resolve().then(() => __importStar(require('fs/promises')));
|
||||
try {
|
||||
await fs.access(memoryPath);
|
||||
const stats = await fs.stat(memoryPath);
|
||||
if (stats.isDirectory()) {
|
||||
return { name: 'Memory System', status: 'pass', message: `Memory directory exists at ${memoryPath}` };
|
||||
}
|
||||
}
|
||||
catch {
|
||||
return {
|
||||
name: 'Memory System',
|
||||
status: 'warn',
|
||||
message: `Memory directory not found at ${memoryPath}`,
|
||||
fix: 'Run `ruvbot init` to create directories',
|
||||
};
|
||||
}
|
||||
return { name: 'Memory System', status: 'pass', message: 'Ready' };
|
||||
}
|
||||
async function checkSecurity() {
|
||||
const aidefenceEnabled = process.env.RUVBOT_AIDEFENCE_ENABLED !== 'false';
|
||||
const piiEnabled = process.env.RUVBOT_PII_DETECTION !== 'false';
|
||||
const auditEnabled = process.env.RUVBOT_AUDIT_LOG !== 'false';
|
||||
const features = [];
|
||||
if (aidefenceEnabled)
|
||||
features.push('AI Defense');
|
||||
if (piiEnabled)
|
||||
features.push('PII Detection');
|
||||
if (auditEnabled)
|
||||
features.push('Audit Logging');
|
||||
if (features.length === 0) {
|
||||
return {
|
||||
name: 'Security Configuration',
|
||||
status: 'warn',
|
||||
message: 'All security features disabled',
|
||||
fix: 'Enable RUVBOT_AIDEFENCE_ENABLED=true in .env',
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: 'Security Configuration',
|
||||
status: 'pass',
|
||||
message: `Enabled: ${features.join(', ')}`,
|
||||
};
|
||||
}
|
||||
async function checkPlugins() {
|
||||
const pluginsEnabled = process.env.RUVBOT_PLUGINS_ENABLED !== 'false';
|
||||
const pluginsDir = process.env.RUVBOT_PLUGINS_DIR || './plugins';
|
||||
if (!pluginsEnabled) {
|
||||
return { name: 'Plugin System', status: 'pass', message: 'Disabled' };
|
||||
}
|
||||
const fs = await Promise.resolve().then(() => __importStar(require('fs/promises')));
|
||||
try {
|
||||
const files = await fs.readdir(pluginsDir);
|
||||
const plugins = files.filter((f) => f.endsWith('.js') || f.endsWith('.ts'));
|
||||
return {
|
||||
name: 'Plugin System',
|
||||
status: 'pass',
|
||||
message: `${plugins.length} plugin(s) found in ${pluginsDir}`,
|
||||
};
|
||||
}
|
||||
catch {
|
||||
return {
|
||||
name: 'Plugin System',
|
||||
status: 'warn',
|
||||
message: `Plugin directory not found at ${pluginsDir}`,
|
||||
fix: `mkdir -p ${pluginsDir}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
async function checkDiskSpace() {
|
||||
try {
|
||||
const os = await Promise.resolve().then(() => __importStar(require('os')));
|
||||
const { execSync } = await Promise.resolve().then(() => __importStar(require('child_process')));
|
||||
// Get disk space (works on Unix-like systems)
|
||||
const df = execSync('df -h . 2>/dev/null || echo "N/A"').toString().trim();
|
||||
const lines = df.split('\n');
|
||||
if (lines.length > 1) {
|
||||
const parts = lines[1].split(/\s+/);
|
||||
const available = parts[3];
|
||||
const usePercent = parts[4];
|
||||
const useNum = parseInt(usePercent, 10);
|
||||
if (useNum > 90) {
|
||||
return {
|
||||
name: 'Disk Space',
|
||||
status: 'fail',
|
||||
message: `${usePercent} used, ${available} available`,
|
||||
fix: 'Free up disk space',
|
||||
};
|
||||
}
|
||||
else if (useNum > 80) {
|
||||
return {
|
||||
name: 'Disk Space',
|
||||
status: 'warn',
|
||||
message: `${usePercent} used, ${available} available`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: 'Disk Space',
|
||||
status: 'pass',
|
||||
message: `${available} available`,
|
||||
};
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Disk check not available
|
||||
}
|
||||
return { name: 'Disk Space', status: 'pass', message: 'Check not available on this platform' };
|
||||
}
|
||||
exports.default = createDoctorCommand;
|
||||
//# sourceMappingURL=doctor.js.map
|
||||
1
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/doctor.js.map
vendored
Normal file
1
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/doctor.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
464
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/doctor.ts
vendored
Normal file
464
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/doctor.ts
vendored
Normal file
@@ -0,0 +1,464 @@
|
||||
/**
|
||||
* Doctor Command - System diagnostics and health checks
|
||||
*
|
||||
* Checks:
|
||||
* - Node.js version
|
||||
* - Required dependencies
|
||||
* - Environment variables
|
||||
* - Database connectivity
|
||||
* - LLM provider connectivity
|
||||
* - Memory system
|
||||
* - Security configuration
|
||||
* - Plugin system
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
|
||||
interface CheckResult {
|
||||
name: string;
|
||||
status: 'pass' | 'warn' | 'fail';
|
||||
message: string;
|
||||
fix?: string;
|
||||
}
|
||||
|
||||
export function createDoctorCommand(): Command {
|
||||
const doctor = new Command('doctor');
|
||||
|
||||
doctor
|
||||
.description('Run diagnostics and health checks')
|
||||
.option('--fix', 'Attempt to fix issues automatically')
|
||||
.option('--json', 'Output results as JSON')
|
||||
.option('-v, --verbose', 'Show detailed information')
|
||||
.action(async (options) => {
|
||||
const results: CheckResult[] = [];
|
||||
const spinner = ora('Running diagnostics...').start();
|
||||
|
||||
try {
|
||||
// Check Node.js version
|
||||
results.push(await checkNodeVersion());
|
||||
|
||||
// Check environment variables
|
||||
results.push(...await checkEnvironment());
|
||||
|
||||
// Check dependencies
|
||||
results.push(...await checkDependencies());
|
||||
|
||||
// Check database connectivity
|
||||
results.push(await checkDatabase());
|
||||
|
||||
// Check LLM providers
|
||||
results.push(...await checkLLMProviders());
|
||||
|
||||
// Check memory system
|
||||
results.push(await checkMemorySystem());
|
||||
|
||||
// Check security configuration
|
||||
results.push(await checkSecurity());
|
||||
|
||||
// Check plugin system
|
||||
results.push(await checkPlugins());
|
||||
|
||||
// Check disk space
|
||||
results.push(await checkDiskSpace());
|
||||
|
||||
spinner.stop();
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(results, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
// Display results
|
||||
console.log(chalk.bold('\n🏥 RuvBot Doctor Results\n'));
|
||||
console.log('─'.repeat(60));
|
||||
|
||||
let passCount = 0;
|
||||
let warnCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
for (const result of results) {
|
||||
const icon = result.status === 'pass' ? '✓' : result.status === 'warn' ? '⚠' : '✗';
|
||||
const color = result.status === 'pass' ? chalk.green : result.status === 'warn' ? chalk.yellow : chalk.red;
|
||||
|
||||
console.log(color(`${icon} ${result.name}`));
|
||||
if (options.verbose || result.status !== 'pass') {
|
||||
console.log(chalk.gray(` ${result.message}`));
|
||||
}
|
||||
if (result.fix && result.status !== 'pass') {
|
||||
console.log(chalk.cyan(` Fix: ${result.fix}`));
|
||||
}
|
||||
|
||||
if (result.status === 'pass') passCount++;
|
||||
else if (result.status === 'warn') warnCount++;
|
||||
else failCount++;
|
||||
}
|
||||
|
||||
console.log('─'.repeat(60));
|
||||
console.log(
|
||||
`\nSummary: ${chalk.green(passCount + ' passed')}, ` +
|
||||
`${chalk.yellow(warnCount + ' warnings')}, ` +
|
||||
`${chalk.red(failCount + ' failed')}`
|
||||
);
|
||||
|
||||
if (failCount > 0) {
|
||||
console.log(chalk.red('\n⚠ Some checks failed. Run with --fix to attempt automatic fixes.'));
|
||||
process.exit(1);
|
||||
} else if (warnCount > 0) {
|
||||
console.log(chalk.yellow('\n⚠ Some warnings detected. Review and address if needed.'));
|
||||
} else {
|
||||
console.log(chalk.green('\n✓ All checks passed! RuvBot is healthy.'));
|
||||
}
|
||||
} catch (error) {
|
||||
spinner.fail(chalk.red('Diagnostics failed'));
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
return doctor;
|
||||
}
|
||||
|
||||
async function checkNodeVersion(): Promise<CheckResult> {
|
||||
const version = process.version;
|
||||
const major = parseInt(version.slice(1).split('.')[0], 10);
|
||||
|
||||
if (major >= 20) {
|
||||
return { name: 'Node.js Version', status: 'pass', message: `${version} (recommended)` };
|
||||
} else if (major >= 18) {
|
||||
return { name: 'Node.js Version', status: 'warn', message: `${version} (18+ supported, 20+ recommended)` };
|
||||
} else {
|
||||
return {
|
||||
name: 'Node.js Version',
|
||||
status: 'fail',
|
||||
message: `${version} (requires 18+)`,
|
||||
fix: 'Install Node.js 20 LTS from https://nodejs.org',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function checkEnvironment(): Promise<CheckResult[]> {
|
||||
const results: CheckResult[] = [];
|
||||
|
||||
// Check for .env file
|
||||
const fs = await import('fs/promises');
|
||||
try {
|
||||
await fs.access('.env');
|
||||
results.push({ name: 'Environment File', status: 'pass', message: '.env file found' });
|
||||
} catch {
|
||||
results.push({
|
||||
name: 'Environment File',
|
||||
status: 'warn',
|
||||
message: 'No .env file found',
|
||||
fix: 'Copy .env.example to .env and configure',
|
||||
});
|
||||
}
|
||||
|
||||
// Check critical environment variables
|
||||
const criticalVars = ['ANTHROPIC_API_KEY', 'OPENROUTER_API_KEY', 'OPENAI_API_KEY'];
|
||||
const hasApiKey = criticalVars.some((v) => process.env[v]);
|
||||
|
||||
if (hasApiKey) {
|
||||
results.push({ name: 'LLM API Key', status: 'pass', message: 'At least one LLM API key configured' });
|
||||
} else {
|
||||
results.push({
|
||||
name: 'LLM API Key',
|
||||
status: 'warn',
|
||||
message: 'No LLM API key found',
|
||||
fix: 'Set ANTHROPIC_API_KEY or OPENROUTER_API_KEY in .env',
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async function checkDependencies(): Promise<CheckResult[]> {
|
||||
const results: CheckResult[] = [];
|
||||
|
||||
// Check if package.json exists
|
||||
const fs = await import('fs/promises');
|
||||
try {
|
||||
const pkg = JSON.parse(await fs.readFile('package.json', 'utf-8'));
|
||||
const hasRuvbot = pkg.dependencies?.['@ruvector/ruvbot'] || pkg.devDependencies?.['@ruvector/ruvbot'];
|
||||
|
||||
if (hasRuvbot) {
|
||||
results.push({ name: 'RuvBot Package', status: 'pass', message: '@ruvector/ruvbot installed' });
|
||||
}
|
||||
} catch {
|
||||
// Not in a project directory, skip this check
|
||||
}
|
||||
|
||||
// Check for optional dependencies
|
||||
const optionalDeps = [
|
||||
{ name: '@slack/bolt', desc: 'Slack integration' },
|
||||
{ name: 'pg', desc: 'PostgreSQL support' },
|
||||
{ name: 'ioredis', desc: 'Redis caching' },
|
||||
];
|
||||
|
||||
for (const dep of optionalDeps) {
|
||||
try {
|
||||
await import(dep.name);
|
||||
results.push({ name: dep.desc, status: 'pass', message: `${dep.name} available` });
|
||||
} catch {
|
||||
results.push({
|
||||
name: dep.desc,
|
||||
status: 'warn',
|
||||
message: `${dep.name} not installed (optional)`,
|
||||
fix: `npm install ${dep.name}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async function checkDatabase(): Promise<CheckResult> {
|
||||
const storageType = process.env.RUVBOT_STORAGE_TYPE || 'sqlite';
|
||||
|
||||
if (storageType === 'sqlite') {
|
||||
const fs = await import('fs/promises');
|
||||
const dbPath = process.env.RUVBOT_SQLITE_PATH || './data/ruvbot.db';
|
||||
|
||||
try {
|
||||
await fs.access(dbPath);
|
||||
return { name: 'Database (SQLite)', status: 'pass', message: `Database found at ${dbPath}` };
|
||||
} catch {
|
||||
return {
|
||||
name: 'Database (SQLite)',
|
||||
status: 'warn',
|
||||
message: `Database not found at ${dbPath}`,
|
||||
fix: 'Run `ruvbot init` to create database',
|
||||
};
|
||||
}
|
||||
} else if (storageType === 'postgres') {
|
||||
const dbUrl = process.env.DATABASE_URL;
|
||||
if (!dbUrl) {
|
||||
return {
|
||||
name: 'Database (PostgreSQL)',
|
||||
status: 'fail',
|
||||
message: 'DATABASE_URL not configured',
|
||||
fix: 'Set DATABASE_URL in .env',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const { default: pg } = await import('pg');
|
||||
const client = new pg.Client(dbUrl);
|
||||
await client.connect();
|
||||
await client.query('SELECT 1');
|
||||
await client.end();
|
||||
return { name: 'Database (PostgreSQL)', status: 'pass', message: 'Connection successful' };
|
||||
} catch (error: any) {
|
||||
return {
|
||||
name: 'Database (PostgreSQL)',
|
||||
status: 'fail',
|
||||
message: `Connection failed: ${error.message}`,
|
||||
fix: 'Check DATABASE_URL and ensure PostgreSQL is running',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { name: 'Database', status: 'pass', message: `Using ${storageType} storage` };
|
||||
}
|
||||
|
||||
async function checkLLMProviders(): Promise<CheckResult[]> {
|
||||
const results: CheckResult[] = [];
|
||||
|
||||
// Check Anthropic
|
||||
if (process.env.ANTHROPIC_API_KEY) {
|
||||
try {
|
||||
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'x-api-key': process.env.ANTHROPIC_API_KEY,
|
||||
'anthropic-version': '2023-06-01',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'claude-3-haiku-20240307',
|
||||
max_tokens: 1,
|
||||
messages: [{ role: 'user', content: 'hi' }],
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok || response.status === 400) {
|
||||
// 400 means API key is valid but request is bad (expected with minimal request)
|
||||
results.push({ name: 'Anthropic API', status: 'pass', message: 'API key valid' });
|
||||
} else if (response.status === 401) {
|
||||
results.push({
|
||||
name: 'Anthropic API',
|
||||
status: 'fail',
|
||||
message: 'Invalid API key',
|
||||
fix: 'Check ANTHROPIC_API_KEY in .env',
|
||||
});
|
||||
} else {
|
||||
results.push({ name: 'Anthropic API', status: 'warn', message: `Status: ${response.status}` });
|
||||
}
|
||||
} catch (error: any) {
|
||||
results.push({
|
||||
name: 'Anthropic API',
|
||||
status: 'fail',
|
||||
message: `Connection failed: ${error.message}`,
|
||||
fix: 'Check network connectivity',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check OpenRouter
|
||||
if (process.env.OPENROUTER_API_KEY) {
|
||||
try {
|
||||
const response = await fetch('https://openrouter.ai/api/v1/models', {
|
||||
headers: { Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}` },
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
results.push({ name: 'OpenRouter API', status: 'pass', message: 'API key valid' });
|
||||
} else {
|
||||
results.push({
|
||||
name: 'OpenRouter API',
|
||||
status: 'fail',
|
||||
message: 'Invalid API key',
|
||||
fix: 'Check OPENROUTER_API_KEY in .env',
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
results.push({
|
||||
name: 'OpenRouter API',
|
||||
status: 'fail',
|
||||
message: `Connection failed: ${error.message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
results.push({
|
||||
name: 'LLM Providers',
|
||||
status: 'warn',
|
||||
message: 'No LLM providers configured',
|
||||
fix: 'Set ANTHROPIC_API_KEY or OPENROUTER_API_KEY',
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async function checkMemorySystem(): Promise<CheckResult> {
|
||||
const memoryPath = process.env.RUVBOT_MEMORY_PATH || './data/memory';
|
||||
const fs = await import('fs/promises');
|
||||
|
||||
try {
|
||||
await fs.access(memoryPath);
|
||||
const stats = await fs.stat(memoryPath);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
return { name: 'Memory System', status: 'pass', message: `Memory directory exists at ${memoryPath}` };
|
||||
}
|
||||
} catch {
|
||||
return {
|
||||
name: 'Memory System',
|
||||
status: 'warn',
|
||||
message: `Memory directory not found at ${memoryPath}`,
|
||||
fix: 'Run `ruvbot init` to create directories',
|
||||
};
|
||||
}
|
||||
|
||||
return { name: 'Memory System', status: 'pass', message: 'Ready' };
|
||||
}
|
||||
|
||||
async function checkSecurity(): Promise<CheckResult> {
|
||||
const aidefenceEnabled = process.env.RUVBOT_AIDEFENCE_ENABLED !== 'false';
|
||||
const piiEnabled = process.env.RUVBOT_PII_DETECTION !== 'false';
|
||||
const auditEnabled = process.env.RUVBOT_AUDIT_LOG !== 'false';
|
||||
|
||||
const features = [];
|
||||
if (aidefenceEnabled) features.push('AI Defense');
|
||||
if (piiEnabled) features.push('PII Detection');
|
||||
if (auditEnabled) features.push('Audit Logging');
|
||||
|
||||
if (features.length === 0) {
|
||||
return {
|
||||
name: 'Security Configuration',
|
||||
status: 'warn',
|
||||
message: 'All security features disabled',
|
||||
fix: 'Enable RUVBOT_AIDEFENCE_ENABLED=true in .env',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'Security Configuration',
|
||||
status: 'pass',
|
||||
message: `Enabled: ${features.join(', ')}`,
|
||||
};
|
||||
}
|
||||
|
||||
async function checkPlugins(): Promise<CheckResult> {
|
||||
const pluginsEnabled = process.env.RUVBOT_PLUGINS_ENABLED !== 'false';
|
||||
const pluginsDir = process.env.RUVBOT_PLUGINS_DIR || './plugins';
|
||||
|
||||
if (!pluginsEnabled) {
|
||||
return { name: 'Plugin System', status: 'pass', message: 'Disabled' };
|
||||
}
|
||||
|
||||
const fs = await import('fs/promises');
|
||||
try {
|
||||
const files = await fs.readdir(pluginsDir);
|
||||
const plugins = files.filter((f) => f.endsWith('.js') || f.endsWith('.ts'));
|
||||
return {
|
||||
name: 'Plugin System',
|
||||
status: 'pass',
|
||||
message: `${plugins.length} plugin(s) found in ${pluginsDir}`,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
name: 'Plugin System',
|
||||
status: 'warn',
|
||||
message: `Plugin directory not found at ${pluginsDir}`,
|
||||
fix: `mkdir -p ${pluginsDir}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function checkDiskSpace(): Promise<CheckResult> {
|
||||
try {
|
||||
const os = await import('os');
|
||||
const { execSync } = await import('child_process');
|
||||
|
||||
// Get disk space (works on Unix-like systems)
|
||||
const df = execSync('df -h . 2>/dev/null || echo "N/A"').toString().trim();
|
||||
const lines = df.split('\n');
|
||||
|
||||
if (lines.length > 1) {
|
||||
const parts = lines[1].split(/\s+/);
|
||||
const available = parts[3];
|
||||
const usePercent = parts[4];
|
||||
|
||||
const useNum = parseInt(usePercent, 10);
|
||||
if (useNum > 90) {
|
||||
return {
|
||||
name: 'Disk Space',
|
||||
status: 'fail',
|
||||
message: `${usePercent} used, ${available} available`,
|
||||
fix: 'Free up disk space',
|
||||
};
|
||||
} else if (useNum > 80) {
|
||||
return {
|
||||
name: 'Disk Space',
|
||||
status: 'warn',
|
||||
message: `${usePercent} used, ${available} available`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: 'Disk Space',
|
||||
status: 'pass',
|
||||
message: `${available} available`,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Disk check not available
|
||||
}
|
||||
|
||||
return { name: 'Disk Space', status: 'pass', message: 'Check not available on this platform' };
|
||||
}
|
||||
|
||||
export default createDoctorCommand;
|
||||
1
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/index.d.ts.map
vendored
Normal file
1
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/index.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAC7E,OAAO,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAC7E,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC"}
|
||||
1
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/index.js.map
vendored
Normal file
1
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/index.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;;AAEH,yCAAkD;AAAzC,gHAAA,mBAAmB,OAAA;AAC5B,yCAAkD;AAAzC,gHAAA,mBAAmB,OAAA;AAC5B,6CAAsD;AAA7C,oHAAA,qBAAqB,OAAA;AAC9B,2CAAoD;AAA3C,kHAAA,oBAAoB,OAAA;AAC7B,uCAAgD;AAAvC,8GAAA,kBAAkB,OAAA;AAC3B,6CAA6E;AAApE,oHAAA,qBAAqB,OAAA;AAAE,oHAAA,qBAAqB,OAAA;AACrD,+CAA6E;AAApE,sHAAA,sBAAsB,OAAA;AAAE,mHAAA,mBAAmB,OAAA;AACpD,yCAAsD;AAA7C,oHAAA,uBAAuB,OAAA"}
|
||||
14
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/index.ts
vendored
Normal file
14
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/index.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* CLI Commands Index
|
||||
*
|
||||
* Exports all CLI command modules
|
||||
*/
|
||||
|
||||
export { createDoctorCommand } from './doctor.js';
|
||||
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';
|
||||
11
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/memory.d.ts
vendored
Normal file
11
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/memory.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Memory Command - Vector memory management
|
||||
*
|
||||
* Note: Full memory operations require initialized MemoryManager with
|
||||
* vector index and embedder. This CLI provides basic operations and
|
||||
* demonstrates the memory system capabilities.
|
||||
*/
|
||||
import { Command } from 'commander';
|
||||
export declare function createMemoryCommand(): Command;
|
||||
export default createMemoryCommand;
|
||||
//# sourceMappingURL=memory.d.ts.map
|
||||
1
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/memory.d.ts.map
vendored
Normal file
1
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/memory.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["memory.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIpC,wBAAgB,mBAAmB,IAAI,OAAO,CA4K7C;AAED,eAAe,mBAAmB,CAAC"}
|
||||
180
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/memory.js
vendored
Normal file
180
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/memory.js
vendored
Normal file
@@ -0,0 +1,180 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Memory Command - Vector memory management
|
||||
*
|
||||
* Note: Full memory operations require initialized MemoryManager with
|
||||
* vector index and embedder. This CLI provides basic operations and
|
||||
* demonstrates the memory system capabilities.
|
||||
*/
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createMemoryCommand = createMemoryCommand;
|
||||
const commander_1 = require("commander");
|
||||
const chalk_1 = __importDefault(require("chalk"));
|
||||
function createMemoryCommand() {
|
||||
const memory = new commander_1.Command('memory');
|
||||
memory.description('Memory management commands');
|
||||
// Stats command (doesn't require initialization)
|
||||
memory
|
||||
.command('stats')
|
||||
.description('Show memory configuration')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
// Get stats from environment/config
|
||||
const stats = {
|
||||
configured: true,
|
||||
dimensions: parseInt(process.env.RUVBOT_EMBEDDING_DIM || '384', 10),
|
||||
maxVectors: parseInt(process.env.RUVBOT_MAX_VECTORS || '100000', 10),
|
||||
indexType: 'HNSW',
|
||||
hnswM: parseInt(process.env.RUVBOT_HNSW_M || '16', 10),
|
||||
efConstruction: parseInt(process.env.RUVBOT_HNSW_EF_CONSTRUCTION || '200', 10),
|
||||
memoryPath: process.env.RUVBOT_MEMORY_PATH || './data/memory',
|
||||
};
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(stats, null, 2));
|
||||
return;
|
||||
}
|
||||
console.log(chalk_1.default.bold('\n📊 Memory Configuration\n'));
|
||||
console.log('─'.repeat(40));
|
||||
console.log(`Dimensions: ${chalk_1.default.cyan(stats.dimensions)}`);
|
||||
console.log(`Max Vectors: ${chalk_1.default.cyan(stats.maxVectors.toLocaleString())}`);
|
||||
console.log(`Index Type: ${chalk_1.default.cyan(stats.indexType)}`);
|
||||
console.log(`HNSW M: ${chalk_1.default.cyan(stats.hnswM)}`);
|
||||
console.log(`EF Construction: ${chalk_1.default.cyan(stats.efConstruction)}`);
|
||||
console.log(`Memory Path: ${chalk_1.default.cyan(stats.memoryPath)}`);
|
||||
console.log('─'.repeat(40));
|
||||
console.log(chalk_1.default.gray('\nNote: Start RuvBot server for full memory operations'));
|
||||
}
|
||||
catch (error) {
|
||||
console.error(chalk_1.default.red(`Stats failed: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
// Store command
|
||||
memory
|
||||
.command('store')
|
||||
.description('Store content in memory (requires running server)')
|
||||
.requiredOption('-c, --content <content>', 'Content to store')
|
||||
.option('-t, --tags <tags>', 'Comma-separated tags')
|
||||
.option('-i, --importance <importance>', 'Importance score (0-1)', '0.5')
|
||||
.action(async (options) => {
|
||||
console.log(chalk_1.default.yellow('\n⚠ Memory store requires a running RuvBot server'));
|
||||
console.log(chalk_1.default.gray('\nTo store memory programmatically:'));
|
||||
console.log(chalk_1.default.cyan(`
|
||||
import { RuvBot } from '@ruvector/ruvbot';
|
||||
|
||||
const bot = new RuvBot(config);
|
||||
await bot.start();
|
||||
|
||||
const entry = await bot.memory.store('${options.content}', {
|
||||
tags: [${(options.tags || '').split(',').map((t) => `'${t.trim()}'`).join(', ')}],
|
||||
importance: ${options.importance}
|
||||
});
|
||||
`));
|
||||
console.log(chalk_1.default.gray('Or use the REST API:'));
|
||||
console.log(chalk_1.default.cyan(`
|
||||
curl -X POST http://localhost:3000/api/memory \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{"content": "${options.content}", "tags": [${(options.tags || '').split(',').map((t) => `"${t.trim()}"`).join(', ')}]}'
|
||||
`));
|
||||
});
|
||||
// Search command
|
||||
memory
|
||||
.command('search')
|
||||
.description('Search memory (requires running server)')
|
||||
.requiredOption('-q, --query <query>', 'Search query')
|
||||
.option('-l, --limit <limit>', 'Maximum results', '10')
|
||||
.option('--threshold <threshold>', 'Similarity threshold (0-1)', '0.5')
|
||||
.action(async (options) => {
|
||||
console.log(chalk_1.default.yellow('\n⚠ Memory search requires a running RuvBot server'));
|
||||
console.log(chalk_1.default.gray('\nTo search memory programmatically:'));
|
||||
console.log(chalk_1.default.cyan(`
|
||||
const results = await bot.memory.search('${options.query}', {
|
||||
topK: ${options.limit},
|
||||
threshold: ${options.threshold}
|
||||
});
|
||||
`));
|
||||
console.log(chalk_1.default.gray('Or use the REST API:'));
|
||||
console.log(chalk_1.default.cyan(`
|
||||
curl "http://localhost:3000/api/memory/search?q=${encodeURIComponent(options.query)}&limit=${options.limit}"
|
||||
`));
|
||||
});
|
||||
// Export command
|
||||
memory
|
||||
.command('export')
|
||||
.description('Export memory to file (requires running server)')
|
||||
.requiredOption('-o, --output <path>', 'Output file path')
|
||||
.option('--format <format>', 'Format: json, jsonl', 'json')
|
||||
.action(async (options) => {
|
||||
console.log(chalk_1.default.yellow('\n⚠ Memory export requires a running RuvBot server'));
|
||||
console.log(chalk_1.default.gray('\nTo export memory:'));
|
||||
console.log(chalk_1.default.cyan(`
|
||||
const data = await bot.memory.export();
|
||||
await fs.writeFile('${options.output}', JSON.stringify(data, null, 2));
|
||||
`));
|
||||
});
|
||||
// Import command
|
||||
memory
|
||||
.command('import')
|
||||
.description('Import memory from file (requires running server)')
|
||||
.requiredOption('-i, --input <path>', 'Input file path')
|
||||
.action(async (options) => {
|
||||
console.log(chalk_1.default.yellow('\n⚠ Memory import requires a running RuvBot server'));
|
||||
console.log(chalk_1.default.gray('\nTo import memory:'));
|
||||
console.log(chalk_1.default.cyan(`
|
||||
const data = JSON.parse(await fs.readFile('${options.input}', 'utf-8'));
|
||||
const count = await bot.memory.import(data);
|
||||
console.log('Imported', count, 'entries');
|
||||
`));
|
||||
});
|
||||
// Clear command
|
||||
memory
|
||||
.command('clear')
|
||||
.description('Clear all memory (DANGEROUS - requires running server)')
|
||||
.option('-y, --yes', 'Skip confirmation')
|
||||
.action(async (options) => {
|
||||
if (!options.yes) {
|
||||
console.log(chalk_1.default.red('\n⚠ DANGER: This will clear ALL memory entries!'));
|
||||
console.log(chalk_1.default.yellow('Use --yes flag to confirm'));
|
||||
return;
|
||||
}
|
||||
console.log(chalk_1.default.yellow('\n⚠ Memory clear requires a running RuvBot server'));
|
||||
console.log(chalk_1.default.gray('\nTo clear memory:'));
|
||||
console.log(chalk_1.default.cyan(`
|
||||
await bot.memory.clear();
|
||||
`));
|
||||
});
|
||||
// Info command
|
||||
memory
|
||||
.command('info')
|
||||
.description('Show memory system information')
|
||||
.action(async () => {
|
||||
console.log(chalk_1.default.bold('\n🧠 RuvBot Memory System\n'));
|
||||
console.log('─'.repeat(50));
|
||||
console.log(chalk_1.default.cyan('Features:'));
|
||||
console.log(' • HNSW vector indexing (150x-12,500x faster search)');
|
||||
console.log(' • Semantic similarity search');
|
||||
console.log(' • Multi-source memory (conversation, learning, skill, user)');
|
||||
console.log(' • Importance-based eviction');
|
||||
console.log(' • TTL support for temporary memories');
|
||||
console.log(' • Tag-based filtering');
|
||||
console.log('');
|
||||
console.log(chalk_1.default.cyan('Supported Embeddings:'));
|
||||
console.log(' • MiniLM-L6-v2 (384 dimensions, default)');
|
||||
console.log(' • Custom embedders via WASM');
|
||||
console.log('');
|
||||
console.log(chalk_1.default.cyan('Configuration (via .env):'));
|
||||
console.log(' RUVBOT_EMBEDDING_DIM=384');
|
||||
console.log(' RUVBOT_MAX_VECTORS=100000');
|
||||
console.log(' RUVBOT_HNSW_M=16');
|
||||
console.log(' RUVBOT_HNSW_EF_CONSTRUCTION=200');
|
||||
console.log(' RUVBOT_MEMORY_PATH=./data/memory');
|
||||
console.log('─'.repeat(50));
|
||||
});
|
||||
return memory;
|
||||
}
|
||||
exports.default = createMemoryCommand;
|
||||
//# sourceMappingURL=memory.js.map
|
||||
1
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/memory.js.map
vendored
Normal file
1
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/memory.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
187
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/memory.ts
vendored
Normal file
187
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/memory.ts
vendored
Normal file
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* Memory Command - Vector memory management
|
||||
*
|
||||
* Note: Full memory operations require initialized MemoryManager with
|
||||
* vector index and embedder. This CLI provides basic operations and
|
||||
* demonstrates the memory system capabilities.
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
|
||||
export function createMemoryCommand(): Command {
|
||||
const memory = new Command('memory');
|
||||
memory.description('Memory management commands');
|
||||
|
||||
// Stats command (doesn't require initialization)
|
||||
memory
|
||||
.command('stats')
|
||||
.description('Show memory configuration')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
// Get stats from environment/config
|
||||
const stats = {
|
||||
configured: true,
|
||||
dimensions: parseInt(process.env.RUVBOT_EMBEDDING_DIM || '384', 10),
|
||||
maxVectors: parseInt(process.env.RUVBOT_MAX_VECTORS || '100000', 10),
|
||||
indexType: 'HNSW',
|
||||
hnswM: parseInt(process.env.RUVBOT_HNSW_M || '16', 10),
|
||||
efConstruction: parseInt(process.env.RUVBOT_HNSW_EF_CONSTRUCTION || '200', 10),
|
||||
memoryPath: process.env.RUVBOT_MEMORY_PATH || './data/memory',
|
||||
};
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(stats, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.bold('\n📊 Memory Configuration\n'));
|
||||
console.log('─'.repeat(40));
|
||||
console.log(`Dimensions: ${chalk.cyan(stats.dimensions)}`);
|
||||
console.log(`Max Vectors: ${chalk.cyan(stats.maxVectors.toLocaleString())}`);
|
||||
console.log(`Index Type: ${chalk.cyan(stats.indexType)}`);
|
||||
console.log(`HNSW M: ${chalk.cyan(stats.hnswM)}`);
|
||||
console.log(`EF Construction: ${chalk.cyan(stats.efConstruction)}`);
|
||||
console.log(`Memory Path: ${chalk.cyan(stats.memoryPath)}`);
|
||||
console.log('─'.repeat(40));
|
||||
console.log(chalk.gray('\nNote: Start RuvBot server for full memory operations'));
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red(`Stats failed: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Store command
|
||||
memory
|
||||
.command('store')
|
||||
.description('Store content in memory (requires running server)')
|
||||
.requiredOption('-c, --content <content>', 'Content to store')
|
||||
.option('-t, --tags <tags>', 'Comma-separated tags')
|
||||
.option('-i, --importance <importance>', 'Importance score (0-1)', '0.5')
|
||||
.action(async (options) => {
|
||||
console.log(chalk.yellow('\n⚠ Memory store requires a running RuvBot server'));
|
||||
console.log(chalk.gray('\nTo store memory programmatically:'));
|
||||
console.log(chalk.cyan(`
|
||||
import { RuvBot } from '@ruvector/ruvbot';
|
||||
|
||||
const bot = new RuvBot(config);
|
||||
await bot.start();
|
||||
|
||||
const entry = await bot.memory.store('${options.content}', {
|
||||
tags: [${(options.tags || '').split(',').map((t: string) => `'${t.trim()}'`).join(', ')}],
|
||||
importance: ${options.importance}
|
||||
});
|
||||
`));
|
||||
console.log(chalk.gray('Or use the REST API:'));
|
||||
console.log(chalk.cyan(`
|
||||
curl -X POST http://localhost:3000/api/memory \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{"content": "${options.content}", "tags": [${(options.tags || '').split(',').map((t: string) => `"${t.trim()}"`).join(', ')}]}'
|
||||
`));
|
||||
});
|
||||
|
||||
// Search command
|
||||
memory
|
||||
.command('search')
|
||||
.description('Search memory (requires running server)')
|
||||
.requiredOption('-q, --query <query>', 'Search query')
|
||||
.option('-l, --limit <limit>', 'Maximum results', '10')
|
||||
.option('--threshold <threshold>', 'Similarity threshold (0-1)', '0.5')
|
||||
.action(async (options) => {
|
||||
console.log(chalk.yellow('\n⚠ Memory search requires a running RuvBot server'));
|
||||
console.log(chalk.gray('\nTo search memory programmatically:'));
|
||||
console.log(chalk.cyan(`
|
||||
const results = await bot.memory.search('${options.query}', {
|
||||
topK: ${options.limit},
|
||||
threshold: ${options.threshold}
|
||||
});
|
||||
`));
|
||||
console.log(chalk.gray('Or use the REST API:'));
|
||||
console.log(chalk.cyan(`
|
||||
curl "http://localhost:3000/api/memory/search?q=${encodeURIComponent(options.query)}&limit=${options.limit}"
|
||||
`));
|
||||
});
|
||||
|
||||
// Export command
|
||||
memory
|
||||
.command('export')
|
||||
.description('Export memory to file (requires running server)')
|
||||
.requiredOption('-o, --output <path>', 'Output file path')
|
||||
.option('--format <format>', 'Format: json, jsonl', 'json')
|
||||
.action(async (options) => {
|
||||
console.log(chalk.yellow('\n⚠ Memory export requires a running RuvBot server'));
|
||||
console.log(chalk.gray('\nTo export memory:'));
|
||||
console.log(chalk.cyan(`
|
||||
const data = await bot.memory.export();
|
||||
await fs.writeFile('${options.output}', JSON.stringify(data, null, 2));
|
||||
`));
|
||||
});
|
||||
|
||||
// Import command
|
||||
memory
|
||||
.command('import')
|
||||
.description('Import memory from file (requires running server)')
|
||||
.requiredOption('-i, --input <path>', 'Input file path')
|
||||
.action(async (options) => {
|
||||
console.log(chalk.yellow('\n⚠ Memory import requires a running RuvBot server'));
|
||||
console.log(chalk.gray('\nTo import memory:'));
|
||||
console.log(chalk.cyan(`
|
||||
const data = JSON.parse(await fs.readFile('${options.input}', 'utf-8'));
|
||||
const count = await bot.memory.import(data);
|
||||
console.log('Imported', count, 'entries');
|
||||
`));
|
||||
});
|
||||
|
||||
// Clear command
|
||||
memory
|
||||
.command('clear')
|
||||
.description('Clear all memory (DANGEROUS - requires running server)')
|
||||
.option('-y, --yes', 'Skip confirmation')
|
||||
.action(async (options) => {
|
||||
if (!options.yes) {
|
||||
console.log(chalk.red('\n⚠ DANGER: This will clear ALL memory entries!'));
|
||||
console.log(chalk.yellow('Use --yes flag to confirm'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.yellow('\n⚠ Memory clear requires a running RuvBot server'));
|
||||
console.log(chalk.gray('\nTo clear memory:'));
|
||||
console.log(chalk.cyan(`
|
||||
await bot.memory.clear();
|
||||
`));
|
||||
});
|
||||
|
||||
// Info command
|
||||
memory
|
||||
.command('info')
|
||||
.description('Show memory system information')
|
||||
.action(async () => {
|
||||
console.log(chalk.bold('\n🧠 RuvBot Memory System\n'));
|
||||
console.log('─'.repeat(50));
|
||||
console.log(chalk.cyan('Features:'));
|
||||
console.log(' • HNSW vector indexing (150x-12,500x faster search)');
|
||||
console.log(' • Semantic similarity search');
|
||||
console.log(' • Multi-source memory (conversation, learning, skill, user)');
|
||||
console.log(' • Importance-based eviction');
|
||||
console.log(' • TTL support for temporary memories');
|
||||
console.log(' • Tag-based filtering');
|
||||
console.log('');
|
||||
console.log(chalk.cyan('Supported Embeddings:'));
|
||||
console.log(' • MiniLM-L6-v2 (384 dimensions, default)');
|
||||
console.log(' • Custom embedders via WASM');
|
||||
console.log('');
|
||||
console.log(chalk.cyan('Configuration (via .env):'));
|
||||
console.log(' RUVBOT_EMBEDDING_DIM=384');
|
||||
console.log(' RUVBOT_MAX_VECTORS=100000');
|
||||
console.log(' RUVBOT_HNSW_M=16');
|
||||
console.log(' RUVBOT_HNSW_EF_CONSTRUCTION=200');
|
||||
console.log(' RUVBOT_MEMORY_PATH=./data/memory');
|
||||
console.log('─'.repeat(50));
|
||||
});
|
||||
|
||||
return memory;
|
||||
}
|
||||
|
||||
export default createMemoryCommand;
|
||||
12
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/plugins.d.ts
vendored
Normal file
12
vendor/ruvector/npm/packages/ruvbot/src/cli/commands/plugins.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Plugins Command - Plugin management
|
||||
*
|
||||
* Commands:
|
||||
* plugins list List installed plugins
|
||||
* plugins create Create a new plugin scaffold
|
||||
* plugins info Show plugin system information
|
||||
*/
|
||||
import { Command } from 'commander';
|
||||
export declare function createPluginsCommand(): Command;
|
||||
export default createPluginsCommand;
|
||||
//# sourceMappingURL=plugins.d.ts.map
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user